Fixes: support for LNURL codes in uppercase, return correct type from NWC task

This commit is contained in:
minibits-cash
2025-01-24 12:20:17 +01:00
parent a5af9e0834
commit 58232c7d15
10 changed files with 81 additions and 56 deletions
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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')
+30 -14
View File
@@ -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(
`<b>Nostr Wallet Connect</b> 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(
`<b>Nostr Wallet Connect</b> 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 => ({
+8 -11
View File
@@ -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) {
+4 -1
View File
@@ -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<WalletScreenProps> = 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()
+1
View File
@@ -20,6 +20,7 @@ const CASHU_URI_PREFIXES = [
'cashu://',
'cashu:'
]
const CASHU_TOKEN_PREFIXES = [
'cashuA',
'cashuB'
+10 -12
View File
@@ -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
+15 -14
View File
@@ -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
+3 -1
View File
@@ -114,7 +114,9 @@ const getOrCreateKeyPair = async function (): Promise<KeyPair> {
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
+8 -1
View File
@@ -1711,7 +1711,14 @@ const handleClaimQueue = async function (): Promise<void> {
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})
}
}
}