From 98f6ff29cb3df7b356159c7e831c5024b4e2bc71 Mon Sep 17 00:00:00 2001 From: minibits-cash Date: Thu, 30 Jan 2025 22:25:40 +0100 Subject: [PATCH] Fixes: Can not init transactions after recovery from backup, fix retry of SEND transaction. --- index.js | 2 +- package.json | 2 +- src/models/WalletProfileStore.ts | 5 +++ src/screens/ImportBackupScreen.tsx | 38 +++++++++++++++----- src/screens/ScanScreen.tsx | 21 +++++++++++ src/screens/SeedRecoveryScreen.tsx | 11 +++--- src/screens/WalletScreen.tsx | 2 +- src/services/keyChain.ts | 2 +- src/services/notificationService.ts | 54 +++++++++++++++++++---------- src/services/wallet/sendTask.ts | 10 ++++-- 10 files changed, 108 insertions(+), 39 deletions(-) diff --git a/index.js b/index.js index 8df0f0d..37b1905 100644 --- a/index.js +++ b/index.js @@ -32,7 +32,7 @@ notifee.registerForegroundService(async (notification) => { try { if(notification.data.task === HANDLE_NWC_REQUEST_TASK) { - log.debug(`[registerForegroundService] Submitting task ${HANDLE_NWC_REQUEST_TASK} to the queue.`) + log.info(`[registerForegroundService] Submitting task ${HANDLE_NWC_REQUEST_TASK} to the queue.`) const {nwcStore} = rootStoreInstance // if an app is in killed state, state is not loaded diff --git a/package.json b/package.json index 2990c5a..53aa662 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minibits_wallet", - "version": "0.1.11-beta.5", + "version": "0.1.11-beta.6", "private": true, "scripts": { "android:clean": "cd android && ./gradlew clean", diff --git a/src/models/WalletProfileStore.ts b/src/models/WalletProfileStore.ts index d8ce7f3..0b2b47e 100644 --- a/src/models/WalletProfileStore.ts +++ b/src/models/WalletProfileStore.ts @@ -194,6 +194,11 @@ export const WalletProfileStoreModel = types }), setDevice: flow(function* setDevice(device: string) { try { + if(!self.pubkey) { + // skip call for new installs without a profile + return + } + yield MinibitsClient.updateDeviceToken(self.pubkey, {deviceToken: device}) self.device = device } catch (e: any) { diff --git a/src/screens/ImportBackupScreen.tsx b/src/screens/ImportBackupScreen.tsx index 4656085..8dbe016 100644 --- a/src/screens/ImportBackupScreen.tsx +++ b/src/screens/ImportBackupScreen.tsx @@ -22,11 +22,10 @@ import { } from '../components' import {useHeader} from '../utils/useHeader' import AppError, { Err } from '../utils/AppError' -import { Database, KeyChain, log, MinibitsClient, NostrClient } from '../services' +import { Database, KeyChain, log, MinibitsClient } from '../services' import Clipboard from '@react-native-clipboard/clipboard' -import { useStores } from '../models' +import { rootStoreInstance, useStores } from '../models' import {MnemonicInput} from './Recovery/MnemonicInput' -import { TransactionStatus } from '../models/Transaction' import { MINIBITS_MINT_URL, MINIBITS_NIP05_DOMAIN } from '@env' import { delay } from '../utils/utils' import { applySnapshot} from 'mobx-state-tree' @@ -36,6 +35,8 @@ import { translate } from '../i18n' import { ProofsStoreSnapshot } from '../models/ProofsStore' import { MintsStoreSnapshot } from '../models/MintsStore' import { ContactsStoreSnapshot } from '../models/ContactsStore' +import { CashuMint, MintActiveKeys } from '@cashu/cashu-ts' +import { MintUnit } from '../services/wallet/currency' if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) { @@ -224,12 +225,30 @@ export const ImportBackupScreen: FC> = obser // import wallet snapshot into the state // const rootStore = rootStoreInstance + + // hydrate mint keys back to the backup as they are stripped from backup + for (const mint of walletSnapshot.mintsStore.mints) { + const cashuMint = new CashuMint(mint.mintUrl) + const keysResult: MintActiveKeys = await cashuMint.getKeys() + const {keysets: keys} = keysResult + + for(const key of keys) { + if(!key.unit) { + key.unit = 'sat' + } + + log.trace('[importWallet] Hydrating keys for', {keysetId: key.id}) + mint.keys.push(key) + } + } applySnapshot(proofsStore, walletSnapshot.proofsStore) applySnapshot(mintsStore, walletSnapshot.mintsStore) applySnapshot(contactsStore, walletSnapshot.contactsStore) - // log.trace('After import', {rootStore}) + // log.trace('After import', {rootStore}) + const rootStore = rootStoreInstance + log.trace('After import and mint keys hydration', {mintsStore}) // import proofs into the db if(proofsStore.proofsCount > 0) { @@ -270,7 +289,11 @@ export const ImportBackupScreen: FC> = obser mnemonic } - keys.SEED = seed + keys.SEED = seed + + // save keys as we need them next for publishing the recovered profile to relays + await KeyChain.saveWalletKeys(keys) + walletStore.cleanCachedWalletKeys() if(isNewProfileNeeded) { @@ -290,10 +313,7 @@ export const ImportBackupScreen: FC> = obser ) } - // save keys after successful profile creation / recovery - await KeyChain.saveWalletKeys(keys) - - walletStore.cleanCachedWalletKeys() + userSettingsStore.setIsOnboarded(true) diff --git a/src/screens/ScanScreen.tsx b/src/screens/ScanScreen.tsx index 18af273..903ee64 100644 --- a/src/screens/ScanScreen.tsx +++ b/src/screens/ScanScreen.tsx @@ -22,6 +22,7 @@ import { useStores } from '../models' import { translate } from '../i18n' import { MintUnit } from '../services/wallet/currency' import { Mint } from '../models/Mint' +import { CashuUtils } from '../services/cashu/cashuUtils' const hasAndroidCameraPermission = async () => { const cameraPermission = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.CAMERA) @@ -202,6 +203,26 @@ export const ScanScreen: FC> = function ScanScree break } } + + const maybeCashuPaymentRequest = CashuUtils.findEncodedCashuPaymentRequest(incoming) + + if(maybeCashuPaymentRequest) { + try { + log.trace('Found Cashu Payment request instead of an invoice', maybeCashuPaymentRequest, 'onIncomingData') + const encodedPr = CashuUtils.extractEncodedCashuPaymentRequest(maybeCashuPaymentRequest) + + if(encodedPr) { + await IncomingParser.navigateWithIncomingData({ + type: IncomingDataType.CASHU_PAYMENT_REQUEST, + encoded: encodedPr + }, navigation, unit, mint && mint.mintUrl) + } + return + } catch (e3: any) { + handleError(e3) + break + } + } e.params = incoming handleError(e) diff --git a/src/screens/SeedRecoveryScreen.tsx b/src/screens/SeedRecoveryScreen.tsx index 7e4f35e..58855ce 100644 --- a/src/screens/SeedRecoveryScreen.tsx +++ b/src/screens/SeedRecoveryScreen.tsx @@ -510,7 +510,11 @@ export const SeedRecoveryScreen: FC> = obser mnemonic } - keys.SEED = seed + keys.SEED = seed + + // save keys as we need them next for publishing the recovered profile to relays + await KeyChain.saveWalletKeys(keys) + walletStore.cleanCachedWalletKeys() if(isNewProfileNeeded) { @@ -530,11 +534,6 @@ export const SeedRecoveryScreen: FC> = obser ) } - // save keys after successful profile creation / recovery - await KeyChain.saveWalletKeys(keys) - - walletStore.cleanCachedWalletKeys() - userSettingsStore.setIsOnboarded(true) if(!mintsStore.mintExists(MINIBITS_MINT_URL)) { diff --git a/src/screens/WalletScreen.tsx b/src/screens/WalletScreen.tsx index b2fbd51..96b68fe 100644 --- a/src/screens/WalletScreen.tsx +++ b/src/screens/WalletScreen.tsx @@ -124,7 +124,7 @@ export const WalletScreen: FC = observer( setUpdateSize(`${round(update.packageSize * 0.000001, 2)}MB`) setIsUpdateAvailable(true) toggleUpdateModal() - log.info('OTA Update available', update, 'checkForUpdate') + log.trace('OTA Update available', update, 'checkForUpdate') } } catch (e: any) { return false // silent diff --git a/src/services/keyChain.ts b/src/services/keyChain.ts index 24179ae..cff4a03 100644 --- a/src/services/keyChain.ts +++ b/src/services/keyChain.ts @@ -166,7 +166,7 @@ const saveWalletKeys = async function ( return keys } - log.warn('[getWalletKeys]', 'Did not find existing wallet keys in the KeyChain') + log.debug('[getWalletKeys]', 'Did not find existing wallet keys in the KeyChain') return undefined } catch (e: any) { throw new AppError(Err.KEYCHAIN_ERROR, e.message, e) diff --git a/src/services/notificationService.ts b/src/services/notificationService.ts index 54e914d..e5410c5 100644 --- a/src/services/notificationService.ts +++ b/src/services/notificationService.ts @@ -14,7 +14,7 @@ import { NwcRequest, nwcPngUrl } from '../models/NwcStore'; import { HANDLE_NWC_REQUEST_TASK, WalletTask, WalletTaskResult } from './walletService' import { SyncQueue } from './syncQueueService' import { delay } from '../utils/delay' -import TaskQueue from 'taskon' +import TaskQueue, { Task, TaskId, TaskStatus } from 'taskon' export type NotifyReceiveToLnurlData = { type: 'NotifyReceiveToLnurlData', @@ -50,6 +50,28 @@ const getNwcQueue = function () { } +const addNwcQueueTask = function (taskId: TaskId, task: Promise | any) { + const queue = getNwcQueue() + + log.info(`Adding new nwcQueue task ${taskId} to the queue`) + + queue + .addTask( + task, + taskId, _handleNwcQueueTaskStatusChange) + .then((result: any) => { + log.info(`nwcQueue task ${taskId} completed.`) + }) + +} + +const _handleNwcQueueTaskStatusChange = (status: TaskStatus) => { + log.trace( + `[_handleNwcQueueTaskStatusChange] The status of task changed to ${status}`, + ) +} + + const DEFAULT_CHANNEL_ID = 'default' const DEFAULT_CHANNEL_NAME = 'Minibits notifications' @@ -64,7 +86,7 @@ export const TEST_CHANNEL_NAME = 'Minibits test tasks' const initNotifications = async () => { let enabled = await areNotificationsEnabled() - log.trace(`[initNotifications] Push notifications are ${enabled ? 'enabled' : 'disabled'}.`) + log.debug(`[initNotifications] Push notifications are ${enabled ? 'enabled' : 'disabled'}.`) if(!enabled) return @@ -143,14 +165,12 @@ const onForegroundNotification = async function(remoteMessage: FirebaseMessaging // Process NWC request notified by FCM message by dedicated queue to avoid race condition // when starting foreground service if(remoteData.type === 'NotifyNwcRequestData') { - const nwcQueue = getNwcQueue() - nwcQueue - .addTask(async () => { - await _nwcRequestHandler(remoteData) - }) - .then((result) => { - log.trace('nwcQueue task completed.') - }) + const now = new Date().getTime() + + addNwcQueueTask( + `_nwcRequestHandler-${now}`, + async () => await _nwcRequestHandler(remoteData) + ) return } @@ -177,14 +197,12 @@ const onBackgroundNotification = async function(remoteMessage: FirebaseMessaging // Process NWC request notified by FCM message by dedicated queue to avoid race condition // when starting foreground service if(remoteData.type === 'NotifyNwcRequestData') { - const nwcQueue = getNwcQueue() - nwcQueue - .addTask(async () => { - await _nwcRequestHandler(remoteData) - }) - .then((result) => { - log.trace('nwcQueue task completed.') - }) + const now = new Date().getTime() + + addNwcQueueTask( + `_nwcRequestHandler-${now}`, + async () => await _nwcRequestHandler(remoteData) + ) return } diff --git a/src/services/wallet/sendTask.ts b/src/services/wallet/sendTask.ts index 3b56285..873e784 100644 --- a/src/services/wallet/sendTask.ts +++ b/src/services/wallet/sendTask.ts @@ -342,7 +342,7 @@ export const sendFromMintSync = async function ( amountWithFees, returnedAmount, transactionId - }) + }) const sendResult = await walletStore.send( mintUrl, @@ -420,7 +420,13 @@ export const sendFromMintSync = async function ( // try to clean spent proofs if that was the swap error cause if (e.params && e.params.message && e.params.message.includes('Token already spent')) { - log.error('[sendFromMintSync] Going to clean spent proofs from pending', {transactionId}) + log.error('[sendFromMintSync] Going to clean spent proofs from proofsToSendFrom and from pending', {transactionId}) + + await WalletTask.syncStateWithMintTask({ + proofsToSync: proofsToSendFrom, + mintUrl, + isPending: true + }) await WalletTask.syncStateWithMintTask({ proofsToSync: proofsStore.getByMint(mintUrl, {isPending: true, unit}),