diff --git a/index.js b/index.js index cf050d99..b40ad98b 100644 --- a/index.js +++ b/index.js @@ -8,7 +8,14 @@ import notifee from '@notifee/react-native' import messaging from '@react-native-firebase/messaging' import {AppRegistry} from 'react-native' import { rootStoreInstance, setupRootStore } from './src/models' -import {WalletTask, NotificationService, NWC_CHANNEL_ID, TASK_QUEUE_CHANNEL_ID} from './src/services' +import { + WalletTask, + NotificationService, + SWAP_ALL_TASK, + TEST_TASK, + SYNC_STATE_WITH_ALL_MINTS_TASK, + HANDLE_NWC_REQUEST_TASK +} from './src/services' import { log } from './src/services/logService' import App from './src/App' import {name as appName} from './app.json' @@ -17,38 +24,44 @@ function BootstrapApp() { return } -// Process nwc commands as long running headless tasks +// long running tasks notifee.registerForegroundService(async (notification) => { return new Promise(async (resolve) => { log.trace('[registerForegroundService] Foreground service running for task:', notification) try { - if(notification.data.task === 'handleNwcRequestFromNotification') { + if(notification.data.task === HANDLE_NWC_REQUEST_TASK) { + log.debug(`[registerForegroundService] Submitting task ${HANDLE_NWC_REQUEST_TASK} to the queue.`) + const {nwcStore} = rootStoreInstance - + // if an app is in killed state, state is not loaded if(nwcStore.all.length === 0) { await setupRootStore(rootStoreInstance) - } + } - nwcStore.handleNwcRequestFromNotification(notification.data.data).catch((e) => { - log.error('[registerForegroundService]', e.message) - }) + WalletTask.handleNwcRequestQueue({requestEvent: notification.data.data}) } - if(notification.data.task === 'syncSpendableStateWithMints') { - WalletTask.syncSpendableStateWithMints() + if(notification.data.task === SYNC_STATE_WITH_ALL_MINTS_TASK) { + log.debug(`[registerForegroundService] Submitting task ${SYNC_STATE_WITH_ALL_MINTS_TASK} to the queue.`) + + WalletTask.syncStateWithAllMintsQueue({isPending: false}) } - if(notification.data.task === 'sendAll') { - WalletTask.sendAll() + + if(notification.data.task === SWAP_ALL_TASK) { + log.debug(`[registerForegroundService] Submitting task ${SWAP_ALL_TASK} to the queue.`) + + WalletTask.swapAllQueue() } - /*if(notification.android.channelId === TASK_QUEUE_CHANNEL_ID) { - const tasksArray = notification.data.tasksToRun.split('|') - - }*/ + if(notification.data.task === TEST_TASK) { + log.debug(`[registerForegroundService] Submitting task ${TEST_TASK} to the queue.`) + + WalletTask.testQueue() + } } catch(e) { log.error('[registerForegroundService]', e.message) diff --git a/output.log b/output.log index b21a667b..46b134b1 100644 Binary files a/output.log and b/output.log differ diff --git a/package.json b/package.json index 588f9d63..5fcce1a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minibits_wallet", - "version": "0.1.10-beta.20", + "version": "0.1.10-beta.21", "private": true, "scripts": { "android:clean": "cd android && ./gradlew clean", @@ -18,7 +18,7 @@ "adb:inspect": "adb shell input keyevent 82", "postinstall": "patch-package --exclude 'nothing'" }, - "dependencies": { + "dependencies": { "@cashu/cashu-ts": "minibits-cash/cashu-ts#2b51a7ba7a8461a0040359eecda37b98b603692c", "@fortawesome/fontawesome-svg-core": "^6.6.0", "@fortawesome/free-brands-svg-icons": "^6.6.0", @@ -76,11 +76,11 @@ "react-native-svg": "^15.6.0", "react-native-tab-view": "^3.5.2", "react-native-url-polyfill": "^2.0.0", - "simple-js-task-queue": "^0.2.2", + "taskon": "^0.3.0", "text-encoding-polyfill": "^0.6.7", "util": "^0.12.5" }, - "devDependencies": { + "devDependencies": { "@babel/core": "^7.20.0", "@babel/plugin-proposal-export-namespace-from": "^7.18.9", "@babel/preset-env": "^7.20.0", diff --git a/src/components/Loading.tsx b/src/components/Loading.tsx index b6eee651..c393bb1b 100644 --- a/src/components/Loading.tsx +++ b/src/components/Loading.tsx @@ -18,8 +18,6 @@ export function Loading(props: ViewProps & { statusMessage?: string, textStyle?: const $loading = (shiftedUp = false) => ({ flex: 1, - width: '100%', - height: '100%', alignItems: 'center', justifyContent: 'center', backgroundColor: colors.dark.background, diff --git a/src/models/NwcStore.ts b/src/models/NwcStore.ts index 8b042e11..d7cd67a8 100644 --- a/src/models/NwcStore.ts +++ b/src/models/NwcStore.ts @@ -18,6 +18,7 @@ import { NostrClient, NostrEvent, NostrUnsignedEvent, + SyncQueue, TransactionTaskResult, WalletTask } from '../services' @@ -35,6 +36,8 @@ import { PaymentRequest } from './PaymentRequest' import { PaymentRequests } from './PaymentRequestsStore' import { MINIBITS_MINT_URL } from '@env' import { MintBalance } from './Mint' +import { transferTask } from '../services/wallet/transferTask' +import { topupTask } from '../services/wallet/topupTask' type NwcError = { result_type: string, @@ -169,6 +172,7 @@ export const NwcConnectionModel = types.model('NwcConnection', { } // notify errors + if((nwcResponse as NwcError).error) { let body = '' if(nwcResponse.result_type === 'pay_invoice') { @@ -189,147 +193,27 @@ export const NwcConnectionModel = types.model('NwcConnection', { if(nwcResponse.result_type === 'lookup_invoice') { body = 'Lookup invoice error: ' - } + } + + yield NotificationService.createLocalNotification( + `${self.name} - Nostr Wallet Connect`, + body + (nwcResponse as NwcError).error.message, + nwcPngUrl + ) + + } - const enabled = yield NotificationService.areNotificationsEnabled() - if(enabled) { - yield NotificationService.createLocalNotification( - `${self.name} - Nostr Wallet Connect`, - body + (nwcResponse as NwcError).error.message, - nwcPngUrl - ) - } - } - - NostrClient.publish( + yield NostrClient.publish( responseEvent, self.connectionRelays ) - NotificationService.stopForegroundService() - + yield NotificationService.stopForegroundService() }), })) -.actions(self => ({ - handleTransferTaskResult: flow(function* handleTransferTaskResult(result: TransactionTaskResult) { - log.debug('[handleTransferTaskResult] Got transfer task result', { - connection: self.name, - meltQuote: result.meltQuote?.quote, - txId: result.transaction?.id, - }) - - if(result.meltQuote?.quote === self.lastMeltQuoteId) { - log.error('Meltquote has been already handled, skipping...', { - meltQuoteId: self.lastMeltQuoteId, - caller: 'handleTransferTaskResult' - }) - return - } - - self.setLastMeltQuoteId(result.meltQuote?.quote) - - let nwcResponse: NwcResponse | NwcError - - if(result.transaction?.status === TransactionStatus.COMPLETED) { - const updatedLimit = self.remainingDailyLimit - - (result.transaction.amount + result.transaction.fee) - - nwcResponse = { - result_type: 'pay_invoice', - result: { - preimage: result.preimage, - } - } as NwcResponse - - log.trace('[handleTransferTaskResult] Updating remainingLimit', { - connection: self.name, - beforeUpdate: self.remainingDailyLimit, - afterUpdate: updatedLimit - }) - - self.setRemainingDailyLimit(updatedLimit) - - // notify completed payment - const enabled = yield NotificationService.areNotificationsEnabled() - if(enabled) { - yield NotificationService.createLocalNotification( - `${self.name} - Nostr Wallet Connect`, - `Paid ${result.transaction.amount} SAT${result.transaction.fee > 0 ? ', fee ' + result.transaction.fee + ' SAT' : ''}. Remaining today's limit is ${self.remainingDailyLimit} SAT`, - nwcPngUrl - ) - } - } else { - nwcResponse = { - result_type: 'pay_invoice', - error: { code: 'INTERNAL', message: result.message} - } as NwcError - } - - if(!result.nwcEvent) { - log.error('Missing nwcEvent.', {caller: 'handleTransferTaskResult'}) - NotificationService.stopForegroundService() - return - } - - yield self.sendResponse(nwcResponse, result.nwcEvent) - }), - handleTopupTaskResult: flow(function* handleTopupTaskResult(result: TransactionTaskResult) { - log.debug('Got topup task result', { - connection: self.name, - encodedInvoice: result.encodedInvoice, - caller: 'handleTopupTaskResult' - }) - - if(!result.nwcEvent) { - log.error('Missing nwcEvent.', {caller: 'handleTopupTaskResult'}) - return - } - - let nwcResponse: NwcResponse | NwcError - - if(result.paymentRequest) { - const pr: PaymentRequest = result.paymentRequest - - nwcResponse = { - result_type: 'make_invoice', - result: { - type: 'incoming', - invoice: pr.encodedInvoice, - description: pr.description, - payment_hash: pr.paymentHash, - amount: pr.invoicedAmount * 1000, - fees_paid: 0, - created_at: Math.floor(pr.createdAt!.getTime() / 1000), - expires_at: Math.floor(pr.expiresAt!.getTime() / 1000), - preimage: null, - settled_at: null - } as NwcTransaction - } as NwcResponse - - // notify created topup - const enabled = yield NotificationService.areNotificationsEnabled() - if(enabled) { - yield NotificationService.stopForegroundService() - - yield NotificationService.createLocalNotification( - `${self.name} - Nostr Wallet Connect`, - `Invoice for ${result.paymentRequest.invoicedAmount} SATS has been created.`, - nwcPngUrl - ) - } - } else { - nwcResponse = { - result_type: 'make_invoice', - error: { code: 'INTERNAL', message: result.message} - } as NwcError - } - - yield self.sendResponse(nwcResponse, result.nwcEvent) - }) -})) .actions(self => ({ handleGetInfo (nwcRequest: NwcRequest): NwcResponse { - const nwcResponse = { + const nwcResponse: NwcResponse = { result_type: nwcRequest.method, result: { alias: 'Minibits', @@ -369,7 +253,7 @@ export const NwcConnectionModel = types.model('NwcConnection', { } as NwcTransaction }) - const nwcResponse = { + const nwcResponse: NwcResponse = { result_type: nwcRequest.method, result: { transactions @@ -389,7 +273,7 @@ export const NwcConnectionModel = types.model('NwcConnection', { resultBalanceMsat = 0 } - const nwcResponse = { + const nwcResponse: NwcResponse = { result_type: nwcRequest.method, result: { balance: resultBalanceMsat @@ -417,14 +301,56 @@ export const NwcConnectionModel = types.model('NwcConnection', { } as NwcError } - yield WalletTask.topup( + const result = yield topupTask( mintBalance, roundUp(amontMsat / 1000, 0), 'sat', description, undefined, requestEvent - ) + ) + + log.debug('Got topup task result', { + connection: self.name, + encodedInvoice: result.encodedInvoice, + caller: 'handleTopupTaskResult' + }) + + let nwcResponse: NwcResponse | NwcError + + if(result.paymentRequest) { + const pr: PaymentRequest = result.paymentRequest + + nwcResponse = { + result_type: 'make_invoice', + result: { + type: 'incoming', + invoice: pr.encodedInvoice, + description: pr.description, + payment_hash: pr.paymentHash, + amount: pr.invoicedAmount * 1000, + fees_paid: 0, + created_at: Math.floor(pr.createdAt!.getTime() / 1000), + expires_at: Math.floor(pr.expiresAt!.getTime() / 1000), + preimage: null, + settled_at: null + } as NwcTransaction + } as NwcResponse + + yield NotificationService.createLocalNotification( + `${self.name} - Nostr Wallet Connect`, + `Invoice for ${result.paymentRequest.invoicedAmount} SATS has been created.`, + nwcPngUrl + ) + + } else { + nwcResponse = { + result_type: 'make_invoice', + error: { code: 'INTERNAL', message: result.message} + } as NwcError + } + + return nwcResponse }), handleLookupInvoice(nwcRequest: NwcRequest) { const paymentRequestsStore = self.getPaymentRequestsStore() @@ -490,13 +416,11 @@ export const NwcConnectionModel = types.model('NwcConnection', { const feeReserve = Math.max(MIN_LIGHTNING_FEE, amountToPay * LIGHTNING_FEE_PERCENT / 100) const totalAmountToPay = amountToPay + feeReserve - let mintBalance: MintBalance | undefined = undefined - let isMinibitsMintSelected: boolean = false + let mintBalance: MintBalance | undefined = undefined const minibitsBalance = proofsStore.getMintBalance(MINIBITS_MINT_URL) if(minibitsBalance && minibitsBalance.balances.sat! >= totalAmountToPay) { - mintBalance = minibitsBalance - isMinibitsMintSelected = true + mintBalance = minibitsBalance } else { mintBalance = proofsStore.getMintBalanceWithMaxBalance('sat') } @@ -525,7 +449,7 @@ export const NwcConnectionModel = types.model('NwcConnection', { encoded, ) - WalletTask.transfer( + const result = yield transferTask( mintBalance, amountToPay, 'sat', @@ -536,11 +460,51 @@ export const NwcConnectionModel = types.model('NwcConnection', { requestEvent ) + if(result.meltQuote?.quote === self.lastMeltQuoteId) { + throw new AppError(Err.ALREADY_EXISTS_ERROR, 'Already processed', {quote: result.meltQuote?.quote}) + } + + self.setLastMeltQuoteId(result.meltQuote?.quote) + + let nwcResponse: NwcResponse | NwcError + + if(result.transaction?.status === TransactionStatus.COMPLETED) { + const updatedLimit = self.remainingDailyLimit - + (result.transaction.amount + result.transaction.fee) + + nwcResponse = { + result_type: 'pay_invoice', + result: { + preimage: result.preimage, + } + } as NwcResponse + + log.trace('[handleTransferTaskResult] Updating remainingLimit', { + connection: self.name, + beforeUpdate: self.remainingDailyLimit, + afterUpdate: updatedLimit + }) + + self.setRemainingDailyLimit(updatedLimit) + + yield NotificationService.createLocalNotification( + `${self.name} - Nostr Wallet Connect`, + `Paid ${result.transaction.amount} SAT${result.transaction.fee > 0 ? ', fee ' + result.transaction.fee + ' SAT' : ''}. Remaining today's limit is ${self.remainingDailyLimit} SAT`, + nwcPngUrl + ) + + } else { + nwcResponse = { + result_type: 'pay_invoice', + error: { code: 'INTERNAL', message: result.message} + } as NwcError + } + + return nwcResponse + } catch (e: any) { log.error(`[NwcConnection.handlePayInvoice] ${e.message}`) - - yield NotificationService.stopForegroundService() - + return { result_type: nwcRequest.method, error: { code: 'INTERNAL', message: e.message} @@ -549,7 +513,7 @@ export const NwcConnectionModel = types.model('NwcConnection', { }) })) .actions(self => ({ - handleRequest: flow(function* handleRequest(requestEvent: NostrEvent, decryptedNwcRequest?: NwcRequest) { + handleNwcRequestTask: flow(function* handleNwcRequestTask(requestEvent: NostrEvent, decryptedNwcRequest?: NwcRequest) { let nwcRequest: NwcRequest if(!decryptedNwcRequest) { const decryptedContent = yield NostrClient.decryptNip04(requestEvent.pubkey, requestEvent.content) @@ -575,25 +539,12 @@ export const NwcConnectionModel = types.model('NwcConnection', { break case 'make_invoice': nwcResponse = yield self.handleMakeInvoice(nwcRequest, requestEvent) as Promise - - // no early error, topup initiated, register topup result event handler and exit - if(!nwcResponse) { - EventEmitter.on('ev_topupTask_result', self.handleTopupTaskResult) - return - } break case 'lookup_invoice': nwcResponse = self.handleLookupInvoice(nwcRequest) break - case 'pay_invoice': - // only early errors are immediately returned + case 'pay_invoice': nwcResponse = yield self.handlePayInvoice(nwcRequest, requestEvent) as Promise - - // no early error, transfer initiated, register transfer result event handler and exit - if(!nwcResponse) { - EventEmitter.on('ev_transferTask_result', self.handleTransferTaskResult) - return - } break default: const message = `NWC method ${nwcRequest.method} is unknown or not yet supported.` @@ -711,7 +662,7 @@ export const NwcStoreModel = types log.debug('[remove]', 'Connection removed from NwcStore') } }, - receiveNwcEvents () { + receiveNwcEventsQueue () { log.trace('[receiveNwcEvents] start listening for NWC events', { walletPubkey: self.walletPubkey }) @@ -754,8 +705,13 @@ export const NwcStoreModel = types if(!targetConnection) { throw new AppError(Err.VALIDATION_ERROR, 'Missing connection matching event pubkey', {pubkey: event.pubkey}) } - // dispatch to correct connection - targetConnection.handleRequest(event) + + // dispatch to correct connection, process over sync queue + const now = new Date().getTime() + SyncQueue.addTask( + `handleNwcRequestTask-${now}`, + async () => await targetConnection.handleNwcRequestTask(event) + ) }, oneose() { log.trace('[receiveNwcEvents]', `Eose: Got ${eventsBatch.length} NWC events`) @@ -768,7 +724,7 @@ export const NwcStoreModel = types return } }, - handleNwcRequestFromNotification: flow(function* handleNwcRequestFromNotification(event: NostrEvent, decryptedNwcRequest: NwcRequest) { + handleNwcRequestTask: flow(function* handleNwcRequestTask(event: NostrEvent, decryptedNwcRequest?: NwcRequest) { // find connection the nwc request is sent to const targetConnection = self.nwcConnections.find(c => c.connectionPubkey === event.pubkey @@ -776,21 +732,31 @@ export const NwcStoreModel = types if(!targetConnection) { log.error('[handleNwcRequestFromNotification] Missing connection matching event pubkey.', {pubkey: event.pubkey}) - // notify created topup - const enabled = yield NotificationService.areNotificationsEnabled() - if(enabled) { - yield NotificationService.stopForegroundService() - - yield NotificationService.createLocalNotification( - `Nostr Wallet Connect error`, - `Your wallet has received a NWC command, but could not find related NWC connection to handle it.`, - nwcPngUrl - ) - } + + yield NotificationService.stopForegroundService() + yield NotificationService.createLocalNotification( + `Nostr Wallet Connect error`, + `Your wallet has received a NWC command, but could not find related NWC connection to handle it.`, + nwcPngUrl + ) + return } - yield targetConnection.handleRequest(event, decryptedNwcRequest) + if(!event) { + log.error('[handleNwcRequestFromNotification] Missing connection matching event pubkey.') + + yield NotificationService.stopForegroundService() + yield NotificationService.createLocalNotification( + `Nostr Wallet Connect error`, + `Your wallet has received a NWC command, but could not retrieve the required data.`, + nwcPngUrl + ) + + return + } + + yield targetConnection.handleNwcRequestTask(event, decryptedNwcRequest) }) })) .views(self => ({ diff --git a/src/screens/ExportBackupScreen.tsx b/src/screens/ExportBackupScreen.tsx index 48b5ef25..82ee747b 100644 --- a/src/screens/ExportBackupScreen.tsx +++ b/src/screens/ExportBackupScreen.tsx @@ -1,4 +1,3 @@ -import {observer} from 'mobx-react-lite' import React, {FC, useState, useEffect} from 'react' import { TextStyle, @@ -36,13 +35,11 @@ import { ProofsStoreSnapshot } from '../models/ProofsStore' import { getSnapshot } from 'mobx-state-tree' import { ContactsStoreSnapshot } from '../models/ContactsStore' import { MintsStoreSnapshot } from '../models/MintsStore' -import { Database, NotificationService, TASK_QUEUE_CHANNEL_ID, TASK_QUEUE_CHANNEL_NAME, TransactionTaskResult, WalletTask, WalletTaskResult } from '../services' -import { Transaction, TransactionStatus } from '../models/Transaction' +import { SWAP_ALL_TASK, TASK_QUEUE_CHANNEL_ID, TASK_QUEUE_CHANNEL_NAME, WalletTask, WalletTaskResult } from '../services' +import { TransactionStatus } from '../models/Transaction' import { ResultModalInfo } from './Wallet/ResultModalInfo' import { verticalScale } from '@gocodingnow/rn-size-matters' import { Token, getDecodedToken, getEncodedToken } from '@cashu/cashu-ts' -import { encodeCBOR } from '@cashu/cashu-ts/src/cbor' -import { encodeUint8toBase64Url } from '@cashu/cashu-ts/src/base64' import { minibitsPngIcon } from '../components/MinibitsIcon' interface ExportBackupScreenProps extends SettingsStackScreenProps<'ExportBackup'> {} @@ -50,581 +47,528 @@ interface ExportBackupScreenProps extends SettingsStackScreenProps<'ExportBackup const OPTIMIZE_FROM_PROOFS_COUNT = 5 export const ExportBackupScreen: FC = - function ExportBackup(_props) { + function ExportBackup(_props) { - const { navigation } = _props - const { - mintsStore, - contactsStore, - proofsStore - } = useStores() + const { navigation } = _props + const { + mintsStore, + contactsStore, + proofsStore + } = useStores() - useHeader({ - leftIcon: 'faArrowLeft', - onLeftPress: () => navigation.goBack(), - }) + useHeader({ + leftIcon: 'faArrowLeft', + onLeftPress: () => navigation.goBack(), + }) - const [info, setInfo] = useState('') - const [error, setError] = useState() - const [orphanedProofs, setOrphanedProofs] = useState([]) - const [isLoading, setIsLoading] = useState(false) - const [isSendAllSentToQueue, setIsSendAllSentToQueue] = useState(false) - const [isReceiveBatchSentToQueue, setIsReceiveBatchSentToQueue] = useState(false) - const [totalProofsCount, setTotalProofsCount] = useState(0) - const [totalSentProofsCount, setTotalSentProofsCount] = useState(0) - const [totalReceiveErrorCount, setTotalReceiveErrorCount] = useState(0) - const [totalReceiveCompleteCount, setTotalReceiveCompleteCount] = useState(0) - const [isEcashInBackup, setIsEcashInBackup] = useState(true) - const [isMintsInBackup, setIsMintsInBackup] = useState(true) - const [isContactsInBackup, setIsContactsInBackup] = useState(true) - const [isResultModalVisible, setIsResultModalVisible] = useState(false) - const [resultModalInfo, setResultModalInfo] = useState< - {status: TransactionStatus; title?: string, message: string} | undefined - >() - - - useEffect(() => { - const loadProofs = async () => { - setIsLoading(true) - try { - // full refresh of proofs from DB in case the state is broken - await proofsStore.loadProofsFromDatabase() - - const orphaned = proofsStore.allProofs.filter(proof => !proof.mintUrl) + const [info, setInfo] = useState('') + const [error, setError] = useState() + const [orphanedProofs, setOrphanedProofs] = useState([]) + const [isLoading, setIsLoading] = useState(false) + const [isSwapAllSentToQueue, setIsSwapAllSentToQueue] = useState(false) + const [totalProofsCount, setTotalProofsCount] = useState(0) + const [isEcashInBackup, setIsEcashInBackup] = useState(true) + const [isMintsInBackup, setIsMintsInBackup] = useState(true) + const [isContactsInBackup, setIsContactsInBackup] = useState(true) + const [isResultModalVisible, setIsResultModalVisible] = useState(false) + const [resultModalInfo, setResultModalInfo] = useState< + {status: TransactionStatus; title?: string, message: string} | undefined + >() + + + useEffect(() => { + const loadProofs = async () => { + setIsLoading(true) + try { + // full refresh of proofs from DB in case the state is broken + await proofsStore.loadProofsFromDatabase() + + const orphaned = proofsStore.allProofs.filter(proof => !proof.mintUrl) - if (orphaned.length > 0) { - setOrphanedProofs(orphaned) - setInfo(`Found ${orphaned.length} orphaned proofs not belonging to any active mint. Those won't be included to the backup, but you can copy them separately.`) - } + if (orphaned.length > 0) { + setOrphanedProofs(orphaned) + setInfo(`Found ${orphaned.length} orphaned proofs not belonging to any active mint. Those won't be included to the backup, but you can copy them separately.`) + } - // log.trace('[loadProofs]', {refreshedProofs: proofsStore.proofs}) - setTotalProofsCount(proofsStore.proofsCount) - setIsLoading(false) - } catch (e: any) { - handleError(e) - } - } - - loadProofs() - return () => {} - }, []) - - - useEffect(() => { - const handleSendAllResult = async (result: TransactionTaskResult) => { - log.trace('[handleSendAllResults] event handler triggered') - - if (!isSendAllSentToQueue) { return false } - - // runs per each mint and unit - if (result.transaction && result.transaction.status === TransactionStatus.PENDING) { - - // now we batch receive the pending encoded token - // this forces the proofs swap with the mint for standard denomination amounts - const encodedTokenToReceive: string = result.encodedTokenToSend - const tokenToReceive = getDecodedToken(encodedTokenToReceive) - const tokenAmount = CashuUtils.getProofsAmount(tokenToReceive.proofs) - const proofsCount = tokenToReceive.proofs.length - - setTotalSentProofsCount(prev => prev + proofsCount) - setIsReceiveBatchSentToQueue(true) - - WalletTask.receiveBatch( - tokenToReceive, - tokenAmount, - tokenToReceive.memo as string, - encodedTokenToReceive - ) - } + // log.trace('[loadProofs]', {refreshedProofs: proofsStore.proofs}) + setTotalProofsCount(proofsStore.proofsCount) + setIsLoading(false) + } catch (e: any) { + handleError(e) + } } - - if(isSendAllSentToQueue) { - EventEmitter.on('ev_sendTask_result', handleSendAllResult) - } - - return () => { - EventEmitter.off('ev_sendTask_result', handleSendAllResult) - } - }, [isSendAllSentToQueue]) + + loadProofs() + return () => {} + }, []) useEffect(() => { // runs for every receive in a batch - const handleReceiveTaskResult = async (result: TransactionTaskResult) => { - log.trace('handleReceiveTaskResult event handler triggered') - - const {error} = result - - if (error) { - setTotalReceiveErrorCount(prev => prev + 1) - } else { - setTotalReceiveCompleteCount(prev => prev + 1) + const handleSwapAllTaskResult = async (result: WalletTaskResult) => { + if(!isSwapAllSentToQueue) { + return } + + setIsLoading(false) + setResultModalInfo({ + status: result.errors.length > 0 ? TransactionStatus.ERROR : TransactionStatus.COMPLETED, + message: result.message + }) + + if(result.finalProofsCount && result.finalProofsCount > 0) { + setTotalProofsCount(result.finalProofsCount) + } + + setIsResultModalVisible(true) } - if(isReceiveBatchSentToQueue) { - EventEmitter.on('ev_receiveTask_result', handleReceiveTaskResult) + if(isSwapAllSentToQueue) { + EventEmitter.on(`ev_${SWAP_ALL_TASK}_result`, handleSwapAllTaskResult) } return () => { - EventEmitter.off('ev_receiveTask_result', handleReceiveTaskResult) - NotificationService.stopForegroundService() + EventEmitter.off(`ev_${SWAP_ALL_TASK}_result`, handleSwapAllTaskResult) } - }, [isReceiveBatchSentToQueue]) + }, [isSwapAllSentToQueue]) - useEffect(() => { - // runs for every receive in a batch - const showProofOptimizationResult = async () => { - log.trace('handleReceiveTaskResult event handler triggered') - - setIsLoading(false) - - const currentProofsCount = proofsStore.proofsCount - - let message = `Original proofs count: ${totalSentProofsCount}, Optimized proofs count: ${currentProofsCount}` - - if (totalReceiveErrorCount > 0) { - message += `, Errors: ${totalReceiveErrorCount}` - } - - setResultModalInfo({ - status: totalReceiveErrorCount > 0 ? TransactionStatus.ERROR : TransactionStatus.COMPLETED, - message, - }) - - setIsResultModalVisible(true) - } - - if(totalReceiveCompleteCount > 0 || totalReceiveErrorCount > 0) { - showProofOptimizationResult() - } - - }, [totalReceiveErrorCount, totalReceiveCompleteCount]) - - - const toggleResultModal = () => { - setIsResultModalVisible(previousState => !previousState) - WalletTask.syncPendingStateWithMints() - } - - - - const toggleBackupEcashSwitch = () => - setIsEcashInBackup(previousState => !previousState) - - - const toggleBackupMintsSwitch = () => - setIsMintsInBackup(previousState => !previousState) - - - const toggleBackupContanctsSwitch = () => - setIsContactsInBackup(previousState => !previousState) - - - const optimizeProofAmountsStart = function () { - Alert.alert( - 'Optimize ecash proofs', - 'Do you want to swap your wallet ecash for proofs with optimal denominations? The size of your backup will decrease.', - [ - { - text: translate('common.cancel'), - style: 'cancel', - onPress: () => { /* Action canceled */ }, - }, - { - text: translate('common.confirm'), - onPress: async () => { - // Moves all wallet proofs to pending in transactions - // split by mints and by units and in offline mode. - // Supports batching in case proofs count is above limit. - setIsLoading(true) - setIsSendAllSentToQueue(true) - - // long task background processing - await notifee.displayNotification({ - title: TASK_QUEUE_CHANNEL_NAME, - body: 'Processing pending transactions...', - android: { - channelId: TASK_QUEUE_CHANNEL_ID, - asForegroundService: true, - largeIcon: minibitsPngIcon, - importance: AndroidImportance.HIGH, - progress: { - indeterminate: true, - } - }, - data: {task: 'sendAll'} - }) - // WalletTask.sendAll() - } - } - ] - ) - } - - const copyBackup = function () { - try { - setIsLoading(true) - - let exportedProofsStore: ProofsStoreSnapshot = { - proofs: [], - pendingProofs: [], - pendingByMintSecrets: [] - } - - let exportedMintsStore: MintsStoreSnapshot = { - mints: [], - blockedMintUrls: [], - counterBackups: [] - } - - let exportedContactsStore: ContactsStoreSnapshot = { - contacts: [], - publicPubkey: undefined, - selectedContact: undefined, - receivedEventIds: [], - lastPendingReceivedCheck: undefined - } - - if(isEcashInBackup) { - // proofsStore is emptied in snapshot postprocess! - const proofsSnapshot = getSnapshot(proofsStore.proofs) - - // Do not include orphaned proofs as they can not be imported without mintUrl - const cleaned = proofsSnapshot.filter(p => p.mintUrl && p.mintUrl.length > 0) - - exportedProofsStore = { - proofs: cleaned, - pendingProofs: getSnapshot(proofsStore.pendingProofs) || [], - pendingByMintSecrets: getSnapshot(proofsStore.pendingByMintSecrets) - } - } - - if(isMintsInBackup) { - exportedMintsStore = JSON.parse(JSON.stringify(getSnapshot(mintsStore))) - - exportedMintsStore.mints.forEach((mint: any) => { - mint.keys = []; - }) - - //log.trace({exportedMintsStore}) - } - - if(isContactsInBackup) { - exportedContactsStore = getSnapshot(contactsStore) - } - - const exportedSnapshot = { - proofsStore: exportedProofsStore, - mintsStore: exportedMintsStore, - contactsStore: exportedContactsStore, - } - - log.trace({exportedSnapshot}) - - const prefix = 'minibits' - const version = 'A' - - // CBOR - WIP, not working - // const encodedData = encodeCBOR(exportedSnapshot) - // const base64Data = encodeUint8toBase64Url(encodedData) - - // Simple BASE64 - const base64Data = btoa(JSON.stringify(exportedSnapshot)) - - const base64Encoded = prefix + version + base64Data - - Clipboard.setString(base64Encoded) - setIsLoading(false) - - } catch (e: any) { - setInfo(`Could not encode and copy wallet backup: ${e.message}`) - setIsLoading(false) - } - } - - - const groupProofsByMint = function (proofs: Proof[]) { - return proofs.reduce((acc: Record, proof) => { - - const proofMint = CashuUtils.getMintFromProof(proof, mintsStore.allMints) - // Check if there's already an array for this keyset, if not, create one - if(!proofMint) { - return acc - } - if (!acc[proofMint.mintUrl]) { - acc[proofMint.mintUrl] = [] - } - - // Push the object into the array corresponding to its keyset - acc[proofMint.mintUrl].push(proof) - return acc - }, {}) - } + const toggleResultModal = () => { + setIsResultModalVisible(previousState => !previousState) + WalletTask.syncStateWithAllMintsQueue({isPending: true}) + } - const groupProofsByKeysets = function (proofsByMint: Proof[]) { - return proofsByMint.reduce((acc: Record, proof) => { - // Check if there's already an array for this keyset, if not, create one - if (!acc[proof.id as string]) { - acc[proof.id] = [] - } - - // Push the object into the array corresponding to its keyset - acc[proof.id].push(proof) - return acc; - }, {}) - } + + const toggleBackupEcashSwitch = () => + setIsEcashInBackup(previousState => !previousState) + + + const toggleBackupMintsSwitch = () => + setIsMintsInBackup(previousState => !previousState) + + + const toggleBackupContanctsSwitch = () => + setIsContactsInBackup(previousState => !previousState) - const copyEncodedTokens = function () { - try { + const optimizeProofAmountsStart = function () { + Alert.alert( + 'Optimize ecash proofs', + 'Do you want to swap your wallet ecash for proofs with optimal denominations? The size of your backup will decrease.', + [ + { + text: translate('common.cancel'), + style: 'cancel', + onPress: () => { /* Action canceled */ }, + }, + { + text: translate('common.confirm'), + onPress: async () => { + // Moves all wallet proofs to pending in transactions + // split by mints and by units and in offline mode. + // Supports batching in case proofs count is above limit. + setIsLoading(true) - const encodedTokens: string[] = [] + setIsSwapAllSentToQueue(true) + + await notifee.displayNotification({ + title: TASK_QUEUE_CHANNEL_NAME, + body: 'Optimizing ecash proofs denominations...', + android: { + channelId: TASK_QUEUE_CHANNEL_ID, + asForegroundService: true, + largeIcon: minibitsPngIcon, + importance: AndroidImportance.HIGH, + progress: { + indeterminate: true, + } + }, + data: {task: SWAP_ALL_TASK} + }) + + /* await notifee.displayNotification({ + title: TEST_CHANNEL_NAME, + body: 'Test task is running...', + android: { + channelId: TEST_CHANNEL_ID, + asForegroundService: true, + largeIcon: minibitsPngIcon, + importance: AndroidImportance.HIGH, + progress: { + indeterminate: true, + } + }, + data: {task: TEST_TASK_FN} + }) */ + } + } + ] + ) + } + + const copyBackup = function () { + try { + setIsLoading(true) - if (mintsStore.allMints.length === 0) { - setInfo(translate("missingMintsForProofsUserMessage")) + let exportedProofsStore: ProofsStoreSnapshot = { + proofs: [], + pendingProofs: [], + pendingByMintSecrets: [] + } + + let exportedMintsStore: MintsStoreSnapshot = { + mints: [], + blockedMintUrls: [], + counterBackups: [] + } + + let exportedContactsStore: ContactsStoreSnapshot = { + contacts: [], + publicPubkey: undefined, + selectedContact: undefined, + receivedEventIds: [], + lastPendingReceivedCheck: undefined + } + + if(isEcashInBackup) { + // proofsStore is emptied in snapshot postprocess! + const proofsSnapshot = getSnapshot(proofsStore.proofs) + + // Do not include orphaned proofs as they can not be imported without mintUrl + const cleaned = proofsSnapshot.filter(p => p.mintUrl && p.mintUrl.length > 0) + + exportedProofsStore = { + proofs: cleaned, + pendingProofs: getSnapshot(proofsStore.pendingProofs) || [], + pendingByMintSecrets: getSnapshot(proofsStore.pendingByMintSecrets) + } + } + + if(isMintsInBackup) { + exportedMintsStore = JSON.parse(JSON.stringify(getSnapshot(mintsStore))) + + exportedMintsStore.mints.forEach((mint: any) => { + mint.keys = []; + }) + + //log.trace({exportedMintsStore}) + } + + if(isContactsInBackup) { + exportedContactsStore = getSnapshot(contactsStore) + } + + const exportedSnapshot = { + proofsStore: exportedProofsStore, + mintsStore: exportedMintsStore, + contactsStore: exportedContactsStore, + } + + log.trace({exportedSnapshot}) + + const prefix = 'minibits' + const version = 'A' + + // CBOR - WIP, not working + // const encodedData = encodeCBOR(exportedSnapshot) + // const base64Data = encodeUint8toBase64Url(encodedData) + + // Simple BASE64 + const base64Data = btoa(JSON.stringify(exportedSnapshot)) + + const base64Encoded = prefix + version + base64Data + + Clipboard.setString(base64Encoded) + setIsLoading(false) + + } catch (e: any) { + setInfo(`Could not encode and copy wallet backup: ${e.message}`) + setIsLoading(false) + } + } + + + const groupProofsByMint = function (proofs: Proof[]) { + return proofs.reduce((acc: Record, proof) => { + + const proofMint = CashuUtils.getMintFromProof(proof, mintsStore.allMints) + // Check if there's already an array for this keyset, if not, create one + if(!proofMint) { + return acc + } + + if (!acc[proofMint.mintUrl]) { + acc[proofMint.mintUrl] = [] + } + + // Push the object into the array corresponding to its keyset + acc[proofMint.mintUrl].push(proof) + return acc + }, {}) + } + + const groupProofsByKeysets = function (proofsByMint: Proof[]) { + return proofsByMint.reduce((acc: Record, proof) => { + // Check if there's already an array for this keyset, if not, create one + if (!acc[proof.id as string]) { + acc[proof.id] = [] + } + + // Push the object into the array corresponding to its keyset + acc[proof.id].push(proof) + return acc; + }, {}) + } + + + const copyEncodedTokens = function () { + try { + setIsLoading(true) + const encodedTokens: string[] = [] + + if (mintsStore.allMints.length === 0) { + setInfo(translate("missingMintsForProofsUserMessage")) + } + + const groupedByMint = groupProofsByMint(proofsStore.proofs) + + for (const mint in groupedByMint) { + + const proofsByMint = groupedByMint[mint] + + if(proofsByMint.length === 0) { + continue } - const groupedByMint = groupProofsByMint(proofsStore.proofs) + const groupedByKeyset = groupProofsByKeysets(proofsByMint) - for (const mint in groupedByMint) { + for (const keysetId in groupedByKeyset) { + const proofsByKeysetId = groupedByKeyset[keysetId] + const proofsToExport: CashuProof[] = [] + + for (const p of proofsByKeysetId) { + // clean private params + const proofToExport: CashuProof = { + id: p.id, + amount: p.amount, + secret: p.secret, + C: p.C + } + + proofsToExport.push(proofToExport) + } + + const tokenByKeysetId: Token = { + mint, + proofs: proofsToExport, + unit: proofsByKeysetId[0].unit + } - const proofsByMint = groupedByMint[mint] + log.trace('[copyEncodedTokens]', {tokenByKeysetId}) - if(proofsByMint.length === 0) { - continue - } + const encodedByMint = getEncodedToken(tokenByKeysetId) + encodedTokens.push(encodedByMint) + } + } + + Clipboard.setString(JSON.stringify(encodedTokens)) + setIsLoading(false) - const groupedByKeyset = groupProofsByKeysets(proofsByMint) - - for (const keysetId in groupedByKeyset) { - const proofsByKeysetId = groupedByKeyset[keysetId] - const proofsToExport: CashuProof[] = [] - - for (const p of proofsByKeysetId) { - // clean private params - const proofToExport: CashuProof = { - id: p.id, - amount: p.amount, - secret: p.secret, - C: p.C - } - - proofsToExport.push(proofToExport) - } - - const tokenByKeysetId: Token = { - mint, - proofs: proofsToExport, - unit: proofsByKeysetId[0].unit - } - - log.trace('[copyEncodedTokens]', {tokenByKeysetId}) - - const encodedByMint = getEncodedToken(tokenByKeysetId) - encodedTokens.push(encodedByMint) - } - } - - Clipboard.setString(JSON.stringify(encodedTokens)) - setIsLoading(false) - - } catch (e: any) { - handleError(e) - } - } + } catch (e: any) { + handleError(e) + } + } - const copyOrphanedProofs = function() { - log.trace({orphanedProofs}) - if(orphanedProofs.length > 0) { - Clipboard.setString(JSON.stringify(orphanedProofs)) - } - } + const copyOrphanedProofs = function() { + log.trace({orphanedProofs}) + if(orphanedProofs.length > 0) { + Clipboard.setString(JSON.stringify(orphanedProofs)) + } + } - const handleError = function (e: AppError): void { - setIsLoading(false) - setError(e) - } + const handleError = function (e: AppError): void { + setIsLoading(false) + setError(e) + } - - const headerBg = useThemeColor('header') - const hint = useThemeColor('textDim') - const headerTitle = useThemeColor('headerTitle') + + const headerBg = useThemeColor('header') + const hint = useThemeColor('textDim') + const headerTitle = useThemeColor('headerTitle') - return ( - - - - - - - {totalProofsCount > 0 && ( - + + + + + + {totalProofsCount > 0 && ( + + {proofsStore.proofsCount > OPTIMIZE_FROM_PROOFS_COUNT && ( +