diff --git a/package.json b/package.json index 6f5ba811..11643a35 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minibits_wallet", - "version": "0.1.11-beta.1", + "version": "0.1.11-beta.2", "private": true, "scripts": { "android:clean": "cd android && ./gradlew clean", diff --git a/src/App.tsx b/src/App.tsx index 4d426b9e..f41479fc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -58,7 +58,7 @@ interface AppProps { } function App(props: AppProps) { - const {userSettingsStore, relaysStore, walletProfileStore} = useStores() + const {userSettingsStore, relaysStore} = useStores() const {rehydrated} = useInitialRootStore(async() => { log.trace('[useInitialRootStore]', 'Root store rehydrated') diff --git a/src/models/NwcStore.ts b/src/models/NwcStore.ts index d7cd67a8..98c9ec84 100644 --- a/src/models/NwcStore.ts +++ b/src/models/NwcStore.ts @@ -10,17 +10,16 @@ import { import { NWCWalletResponse, NWCWalletInfo, NWCWalletRequest } from 'nostr-tools/kinds' import {withSetPropAction} from './helpers/withSetPropAction' import {log} from '../services/logService' -import EventEmitter from '../utils/eventEmitter' import { getRootStore } from './helpers/getRootStore' import { + HANDLE_NWC_REQUEST_TASK, KeyChain, KeyPair, NostrClient, NostrEvent, NostrUnsignedEvent, SyncQueue, - TransactionTaskResult, - WalletTask + WalletTaskResult } from '../services' import AppError, { Err } from '../utils/AppError' import { LightningUtils } from '../services/lightning/lightningUtils' @@ -206,9 +205,7 @@ export const NwcConnectionModel = types.model('NwcConnection', { yield NostrClient.publish( responseEvent, self.connectionRelays - ) - - yield NotificationService.stopForegroundService() + ) }), })) .actions(self => ({ @@ -559,6 +556,8 @@ export const NwcConnectionModel = types.model('NwcConnection', { // needs to be set before sendResponse but after switch / pay_invoice yield self.sendResponse(nwcResponse, requestEvent) + return nwcResponse + }), })) @@ -731,32 +730,49 @@ export const NwcStoreModel = types ) if(!targetConnection) { - log.error('[handleNwcRequestFromNotification] Missing connection matching event pubkey.', {pubkey: event.pubkey}) + const message = `Your wallet has received a NWC command, but could not find related NWC connection to handle it.` + log.error('[handleNwcRequestFromNotification]', message, {pubkey: event.pubkey}) 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.`, + message, nwcPngUrl ) - return + return { + taskFunction: HANDLE_NWC_REQUEST_TASK, + message, + error: new AppError(Err.WALLET_ERROR, message) + } as WalletTaskResult } - if(!event) { - log.error('[handleNwcRequestFromNotification] Missing connection matching event pubkey.') + if(!event) { + const message = `Your wallet has received a NWC command, but could not retrieve the required data.` + log.error('[handleNwcRequestFromNotification]', message) yield NotificationService.stopForegroundService() yield NotificationService.createLocalNotification( `Nostr Wallet Connect error`, - `Your wallet has received a NWC command, but could not retrieve the required data.`, + message, nwcPngUrl ) - return + return { + taskFunction: HANDLE_NWC_REQUEST_TASK, + message, + error: new AppError(Err.WALLET_ERROR, message) + } as WalletTaskResult } - yield targetConnection.handleNwcRequestTask(event, decryptedNwcRequest) + const nwcResponse: NwcResponse | NwcError = + yield targetConnection.handleNwcRequestTask(event, decryptedNwcRequest) + + return { + taskFunction: HANDLE_NWC_REQUEST_TASK, + message: (nwcResponse as NwcResponse).result || undefined , + error: (nwcResponse as NwcError).error ? new AppError(Err.WALLET_ERROR, (nwcResponse as NwcError).error.message) : undefined + } as WalletTaskResult }) })) .views(self => ({ diff --git a/src/models/WalletProfileStore.ts b/src/models/WalletProfileStore.ts index be97d88d..1108a905 100644 --- a/src/models/WalletProfileStore.ts +++ b/src/models/WalletProfileStore.ts @@ -42,8 +42,7 @@ export const WalletProfileStoreModel = types lud16: types.maybe(types.maybeNull(types.string)), device: types.maybe(types.maybeNull(types.string)), seedHash: types.maybe(types.maybeNull(types.string)), - isOwnProfile: types.optional(types.boolean, false), - // isBatchClaimOn: types.maybe(types.boolean), // legacy, not used + isOwnProfile: types.optional(types.boolean, false), }) .actions(self => ({ publishToRelays: flow(function* publishToRelays() { @@ -148,11 +147,15 @@ export const WalletProfileStoreModel = types let profileRecord: WalletProfileRecord self.seedHash = seedHash - log.trace('[create]', {seedHash, publicKey}) + log.trace('[create]', {seedHash, publicKey, walletId}) try { // creates new profile. If all params equal existing one, it is returned - profileRecord = yield MinibitsClient.createWalletProfile(publicKey, walletId, seedHash) + profileRecord = yield MinibitsClient.createWalletProfile(publicKey, walletId, seedHash) + self.hydrate(profileRecord) + + log.info('[create]', 'Wallet profile saved in WalletProfileStore', {self}) + return self } catch (e: any) { // Unlikely we might hit the same walletId so we retry with another one if(e.name === Err.ALREADY_EXISTS_ERROR) { @@ -167,14 +170,8 @@ export const WalletProfileStoreModel = types self.hydrate(profileRecord) return } - throw e - } - - self.hydrate(profileRecord) - - log.info('[create]', 'Wallet profile saved in WalletProfileStore', {self}) - return self + } }), updateName: flow(function* updateName(name: string) { diff --git a/src/screens/WalletScreen.tsx b/src/screens/WalletScreen.tsx index 9daca0c7..9ca6537e 100644 --- a/src/screens/WalletScreen.tsx +++ b/src/screens/WalletScreen.tsx @@ -42,7 +42,7 @@ import {useStores} from '../models' import {WalletStackScreenProps} from '../navigation' import {Mint, UnitBalance} from '../models/Mint' import {MintsByUnit} from '../models/MintsStore' -import {HANDLE_CLAIM_TASK, HANDLE_RECEIVED_EVENT_TASK, log, NostrClient, WalletTaskResult} from '../services' +import {HANDLE_CLAIM_TASK, HANDLE_RECEIVED_EVENT_TASK, KeyChain, log, NostrClient, WalletTaskResult} from '../services' import {Env} from '../utils/envtypes' import {Transaction} from '../models/Transaction' import {TransactionListItem} from './Transactions/TransactionListItem' @@ -170,6 +170,9 @@ export const WalletScreen: FC = observer( await addMint() } + // const nostrKeyPair = await KeyChain.loadNostrKeyPair() + // log.trace('[getInitialData] KEYS CHECK', {keychainPubkey: nostrKeyPair?.publicKey, profilePubkey: walletProfileStore.pubkey}) + // Only once on startup - Create websocket subscriptions to receive tokens or payment requests by NOSTR DMs WalletTask.receiveEventsFromRelaysQueue() diff --git a/src/services/cashu/cashuUtils.ts b/src/services/cashu/cashuUtils.ts index 08b11d26..cd806595 100644 --- a/src/services/cashu/cashuUtils.ts +++ b/src/services/cashu/cashuUtils.ts @@ -20,6 +20,7 @@ const CASHU_URI_PREFIXES = [ 'cashu://', 'cashu:' ] + const CASHU_TOKEN_PREFIXES = [ 'cashuA', 'cashuB' diff --git a/src/services/lightning/lightningUtils.ts b/src/services/lightning/lightningUtils.ts index f18f69cd..7bd2b523 100644 --- a/src/services/lightning/lightningUtils.ts +++ b/src/services/lightning/lightningUtils.ts @@ -5,6 +5,10 @@ import { log } from '../logService' import { roundUp, toNumber } from "../../utils/number" // TODO refactor all this into own module +const LIGHTNING_URI_PREFIXES = [ + 'lightning://', + 'lightning:', +] export type DecodedLightningInvoice = { paymentRequest: string @@ -40,17 +44,11 @@ const extractEncodedLightningInvoice = function (maybeInvoice: string) { let invoice: DecodedLightningInvoice let encodedInvoice: string = '' - if (maybeInvoice && maybeInvoice.startsWith('lightning:')) { + if (maybeInvoice && maybeInvoice.toLowerCase().startsWith('lightning:')) { - // URI token formats - const uriPrefixes = [ - 'lightning://', - 'lightning:', - ] - - for (const prefix of uriPrefixes) { - if (maybeInvoice && maybeInvoice.startsWith(prefix)) { - encodedInvoice = maybeInvoice.slice(prefix.length) + for (const prefix of LIGHTNING_URI_PREFIXES) { + if (maybeInvoice && maybeInvoice.toLowerCase().startsWith(prefix)) { + encodedInvoice = maybeInvoice.toLowerCase().slice(prefix.length) break // necessary } } @@ -59,8 +57,8 @@ const extractEncodedLightningInvoice = function (maybeInvoice: string) { return encodedInvoice } - if (maybeInvoice && maybeInvoice.startsWith('bitcoin:')) { - const url = new URL(maybeInvoice) + if (maybeInvoice && maybeInvoice.toLowerCase().startsWith('bitcoin:' || 'http')) { + const url = new URL(maybeInvoice.toLowerCase()) // Use URLSearchParams to get the value of the "lightning" parameter encodedInvoice = url.searchParams.get("lightning") as string invoice = decodeInvoice(encodedInvoice) // throws diff --git a/src/services/lnurl/lnurlUtils.ts b/src/services/lnurl/lnurlUtils.ts index 66ed0d51..e19c4518 100644 --- a/src/services/lnurl/lnurlUtils.ts +++ b/src/services/lnurl/lnurlUtils.ts @@ -4,6 +4,14 @@ import AppError, {Err} from '../../utils/AppError' import { log } from '../logService' import { LightningUtils, isLightningInvoice } from '../lightning/lightningUtils' +const LNURL_URI_PREFIXES = [ + 'lightning://', + 'lightning:', + 'lnurlw://', + 'lnurlw:', + 'lnurlp://', + 'lnurlp:', + ] const findEncodedLnurl = function (content: string) { const words = content.split(/\s+|\n+/) @@ -17,14 +25,7 @@ const findEncodedLnurlAddress = function (content: string) { return maybeAddress || null } -const lnurlUriPrefixes = [ - 'lightning://', - 'lightning:', - 'lnurlw://', - 'lnurlw:', - 'lnurlp://', - 'lnurlp:', -] + function isLnurlAddress(address: string) { // Regular expression for a basic email validation @@ -42,8 +43,8 @@ const extractEncodedLnurl = function (maybeLnurl: string) { return maybeLnurl } - if (maybeLnurl && maybeLnurl.startsWith('http')) { // e.g. lnbits withdraw extension links - const parsed = new URL(maybeLnurl) + if (maybeLnurl && maybeLnurl.toLowerCase().startsWith('http')) { // e.g. lnbits withdraw extension links + const parsed = new URL(maybeLnurl.toLowerCase()) encodedLnurl = parsed.searchParams.get('lightning') if(encodedLnurl) { @@ -53,9 +54,9 @@ const extractEncodedLnurl = function (maybeLnurl: string) { } } - for (const prefix of lnurlUriPrefixes) { - if (maybeLnurl && maybeLnurl.startsWith(prefix)) { - encodedLnurl = maybeLnurl.slice(prefix.length) + for (const prefix of LNURL_URI_PREFIXES) { + if (maybeLnurl && maybeLnurl.toLowerCase().startsWith(prefix)) { + encodedLnurl = maybeLnurl.toLowerCase().slice(prefix.length) break // necessary } } @@ -73,7 +74,7 @@ const extractEncodedLnurl = function (maybeLnurl: string) { function extractLnurlAddress(maybeAddress: string) { let address: string | null = null - for (const prefix of lnurlUriPrefixes) { + for (const prefix of LNURL_URI_PREFIXES) { if (maybeAddress && maybeAddress.startsWith(prefix)) { address = maybeAddress.slice(prefix.length) break; // necessary diff --git a/src/services/nostrService.ts b/src/services/nostrService.ts index 1f179d9c..731b97a4 100644 --- a/src/services/nostrService.ts +++ b/src/services/nostrService.ts @@ -114,7 +114,9 @@ const getOrCreateKeyPair = async function (): Promise { keyPair = KeyChain.generateNostrKeyPair() as KeyPair await KeyChain.saveNostrKeyPair(keyPair) - log.trace('[getOrCreateKeyPair]', 'Created and saved new NOSTR keypair') + log.info('[getOrCreateKeyPair]', 'Created and saved new NOSTR keypair', {pubkey: keyPair.publicKey}) + } else { + log.trace('[getOrCreateKeyPair]', 'Returning existing NOSTR keypair', {pubkey: keyPair.publicKey}) } return keyPair diff --git a/src/services/walletService.ts b/src/services/walletService.ts index de8f8906..f27fc620 100644 --- a/src/services/walletService.ts +++ b/src/services/walletService.ts @@ -1711,7 +1711,14 @@ const handleClaimQueue = async function (): Promise { const profile = await MinibitsClient.getWalletProfileBySeedHash(seedHash || recoveredSeedHash!) if(profile) { - walletProfileStore.hydrate(profile) + // make sure we did not lose and thus rotated nostr keys as well + const keyPair = await NostrClient.getOrCreateKeyPair() + + if(keyPair.publicKey === profile.pubkey) { + walletProfileStore.hydrate(profile) + } else { + throw new AppError(Err.KEYCHAIN_ERROR, 'Wallet public key does not match server profile, please reinstall wallet.', {walletPubkey: keyPair.publicKey, profilePubkey: profile.pubkey}) + } } }