diff --git a/PushNotificationManager.tsx b/PushNotificationManager.tsx index 0eaf5576e1..9ac21dd2ed 100644 --- a/PushNotificationManager.tsx +++ b/PushNotificationManager.tsx @@ -33,7 +33,7 @@ export default class PushNotificationManager extends React.Component { console.log('Notification Received - Foreground', notification); // Don't display redeem notification if auto-redeem is on if ( - stores.settingsStore.settings?.lightningAddress + stores.settingsStore.settings?.lightningAddressGlobal ?.automaticallyAccept && JSON.stringify(notification.payload).includes( 'Redeem within the next 24 hours' diff --git a/package.json b/package.json index 7181ca2773..aa1e4bad64 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "/node_modules" ], "transformIgnorePatterns": [ - "node_modules/(?!(react-native|@react-native|react-native-blob-util|react-native-randombytes|dateformat)/)" + "node_modules/(?!(react-native|@react-native|react-native-blob-util|react-native-randombytes|dateformat|uuid)/)" ], "testPathIgnorePatterns": [ "check-styles.test.ts" @@ -140,6 +140,7 @@ "tty-browserify": "0.0.0", "url": "0.10.3", "util": "0.10.4", + "uuid": "9.0.1", "vm-browserify": "0.0.4" }, "devDependencies": { diff --git a/stores/LightningAddressStore.ts b/stores/LightningAddressStore.ts index 620da9185b..54914ad173 100644 --- a/stores/LightningAddressStore.ts +++ b/stores/LightningAddressStore.ts @@ -1,6 +1,6 @@ import { Platform } from 'react-native'; import { action, observable } from 'mobx'; -import ReactNativeBlobUtil from 'react-native-blob-util'; +import ReactNativeBlobUtil, { FetchBlobResponse } from 'react-native-blob-util'; import { Notifications } from 'react-native-notifications'; import BigNumber from 'bignumber.js'; @@ -30,12 +30,12 @@ const LNURL_HOST = 'https://zeuspay.com/api'; const LNURL_SOCKET_HOST = 'https://zeuspay.com'; const LNURL_SOCKET_PATH = '/stream'; -export const LEGACY_ADDRESS_ACTIVATED_STRING = 'olympus-lightning-address'; export const LEGACY_HASHES_STORAGE_STRING = 'olympus-lightning-address-hashes'; -export const ADDRESS_ACTIVATED_STRING = 'zeuspay-lightning-address'; export const HASHES_STORAGE_STRING = 'zeuspay-lightning-address-hashes'; +export type LightningAddressServiceStatus = true | false | 'unknown'; + export default class LightningAddressStore { @observable public lightningAddress: string; @observable public lightningAddressHandle: string; @@ -95,24 +95,7 @@ export default class LightningAddressStore { return this.preimageMap; }; - @action - public getLightningAddressActivated = async () => { - this.loading = true; - const lightningAddressActivated = await Storage.getItem( - ADDRESS_ACTIVATED_STRING - ); - - if (lightningAddressActivated) { - this.lightningAddressActivated = Boolean(lightningAddressActivated); - this.loading = false; - return this.lightningAddressActivated; - } else { - this.loading = false; - } - }; - setLightningAddress = async (handle: string, domain: string) => { - await Storage.setItem(ADDRESS_ACTIVATED_STRING, true); this.lightningAddressActivated = true; this.lightningAddressHandle = handle; this.lightningAddressDomain = domain; @@ -149,7 +132,9 @@ export default class LightningAddressStore { const nostrSignatures: any = []; if (preimages) { const nostrPrivateKey = - this.settingsStore?.settings?.lightningAddress?.nostrPrivateKey; + this.settingsStore.settings.lightningAddressByPubkey?.[ + this.nodeInfoStore.nodeInfo.identity_pubkey + ]?.nostrPrivateKey; for (let i = 0; i < preimages.length; i++) { const preimage = preimages[i]; const hash = sha256 @@ -340,37 +325,39 @@ export default class LightningAddressStore { .then(async (response: any) => { const data = response.json(); const status = response.info().status; - const { - handle, - domain, - created_at, - success - } = data; + const { handle, created_at, success } = + data; if (status === 200 && success) { if (handle) { - this.setLightningAddress( - handle, - domain + await this.settingsStore.updateSettings( + { + lightningAddressGlobal: + { + automaticallyAccept: + true, + allowComments: + true, + nostrRelays: + relays, + notifications: 1 + }, + lightningAddressByPubkey: + { + [this + .nodeInfoStore + .nodeInfo + .identity_pubkey]: + { + enabled: + true, + nostrPrivateKey + } + } + } ); } - await this.settingsStore.updateSettings( - { - lightningAddress: { - enabled: true, - automaticallyAccept: - true, - automaticallyRequestOlympusChannels: - false, // deprecated - allowComments: true, - nostrPrivateKey, - nostrRelays: relays, - notifications: 1 - } - } - ); - // ensure push credentials are in place // right after creation this.updatePushCredentials(); @@ -595,6 +582,38 @@ export default class LightningAddressStore { domain; if (handle && domain) { this.lightningAddress = `${handle}@${domain}`; + + // If backend could not be reached earlier, we set "enabled: true" now + if ( + this.settingsStore.settings + .lightningAddressByPubkey?.[ + this.nodeInfoStore + .nodeInfo + .identity_pubkey + ].enabled === false + ) { + await this.settingsStore.updateSettings( + { + lightningAddressByPubkey: + { + ...this + .settingsStore + .settings + .lightningAddressByPubkey, + [this + .nodeInfoStore + .nodeInfo + .identity_pubkey]: + { + enabled: + true, + nostrPrivateKey: + '' + } + } + } + ); + } } if ( @@ -761,7 +780,7 @@ export default class LightningAddressStore { const hashpk = getPublicKey(hash); await Promise.all( - this.settingsStore.settings.lightningAddress.nostrRelays.map( + this.settingsStore.settings.lightningAddressGlobal.nostrRelays.map( async (relayItem) => { const relay = relayInit(relayItem); relay.on('connect', () => { @@ -978,7 +997,7 @@ export default class LightningAddressStore { memo: comment ? `ZEUS Pay: ${comment}` : 'ZEUS Pay', preimage, private: - this.settingsStore?.settings?.lightningAddress + this.settingsStore?.settings?.lightningAddressGlobal ?.routeHints || false }) .then((result: any) => { @@ -1032,9 +1051,9 @@ export default class LightningAddressStore { @action public redeemAllOpenPayments = async () => { this.redeemingAll = true; - const attestationLevel = this.settingsStore?.settings?.lightningAddress - ?.automaticallyAcceptAttestationLevel - ? this.settingsStore.settings.lightningAddress + const attestationLevel = this.settingsStore?.settings + ?.lightningAddressGlobal?.automaticallyAcceptAttestationLevel + ? this.settingsStore.settings.lightningAddressGlobal .automaticallyAcceptAttestationLevel : 2; @@ -1107,9 +1126,9 @@ export default class LightningAddressStore { const { hash, amount_msat, comment } = data; const attestationLevel = this.settingsStore?.settings - ?.lightningAddress + ?.lightningAddressGlobal ?.automaticallyAcceptAttestationLevel - ? this.settingsStore.settings.lightningAddress + ? this.settingsStore.settings.lightningAddressGlobal .automaticallyAcceptAttestationLevel : 2; @@ -1164,6 +1183,58 @@ export default class LightningAddressStore { } }; + public checkLightningAddressExists = + async (): Promise => { + const timeout = (ms: number) => + new Promise((_, reject) => + setTimeout(() => reject(new Error('Request timed out')), ms) + ); + + try { + const authResponse = (await Promise.race([ + ReactNativeBlobUtil.fetch( + 'POST', + `${LNURL_HOST}/lnurl/auth`, + { 'Content-Type': 'application/json' }, + JSON.stringify({ + pubkey: this.nodeInfoStore.nodeInfo.identity_pubkey + }) + ), + timeout(10000) // 10 seconds should be enough + ])) as FetchBlobResponse; + + const authData = authResponse.json(); + if (authResponse.info().status !== 200) return 'unknown'; + + const { verification } = authData; + const data = await BackendUtils.signMessage(verification); + const signature = data.zbase || data.signature; + + const statusResponse = (await Promise.race([ + ReactNativeBlobUtil.fetch( + 'POST', + `${LNURL_HOST}/lnurl/status`, + { 'Content-Type': 'application/json' }, + JSON.stringify({ + pubkey: this.nodeInfoStore.nodeInfo.identity_pubkey, + message: verification, + signature + }) + ), + timeout(10000) // 10 seconds should be enough + ])) as FetchBlobResponse; + + const statusData = statusResponse.json(); + if (statusResponse.info().status !== 200) return 'unknown'; + + const { handle, domain } = statusData; + return Boolean(handle && domain); + } catch (error) { + console.error('Lightning Address check failed:', error); + return 'unknown'; + } + }; + @action public reset = () => { this.loading = false; diff --git a/stores/SettingsStore.ts b/stores/SettingsStore.ts index 4210b51b39..3f8bbfd791 100644 --- a/stores/SettingsStore.ts +++ b/stores/SettingsStore.ts @@ -34,6 +34,7 @@ export interface Node { nickname?: string; dismissCustodialWarning: boolean; photo?: string; + uuid?: string; // LNC pairingPhrase?: string; mailboxServer?: string; @@ -108,7 +109,7 @@ interface ChannelsSettings { simpleTaprootChannel: boolean; } -interface LightningAddressSettings { +interface LegacyLightningAddressSettings { enabled: boolean; automaticallyAccept: boolean; automaticallyAcceptAttestationLevel: number; @@ -120,6 +121,24 @@ interface LightningAddressSettings { notifications: number; } +interface GlobalLightningAddressSettings { + automaticallyAccept: boolean; + automaticallyAcceptAttestationLevel: number; + routeHints: boolean; + allowComments: boolean; + nostrRelays: Array; + notifications: number; +} + +interface NodeSpecificLightningAddressSettings { + enabled: boolean; + nostrPrivateKey: string; +} + +interface PubkeyLightningAddressMap { + [pubkey: string]: NodeSpecificLightningAddressSettings; +} + interface Bolt12AddressSettings { localPart: string; } @@ -181,7 +200,9 @@ export interface Settings { lsps1Token: string; lsps1ShowPurchaseButton: boolean; // Lightning Address - lightningAddress: LightningAddressSettings; + lightningAddress?: LegacyLightningAddressSettings; + lightningAddressGlobal: GlobalLightningAddressSettings; + lightningAddressByPubkey: PubkeyLightningAddressMap; bolt12Address: Bolt12AddressSettings; selectNodeOnStartup: boolean; } @@ -1200,17 +1221,17 @@ export default class SettingsStore { lsps1Token: '', lsps1ShowPurchaseButton: true, // Lightning Address - lightningAddress: { - enabled: false, + lightningAddressGlobal: { automaticallyAccept: true, automaticallyAcceptAttestationLevel: 2, - automaticallyRequestOlympusChannels: false, // deprecated routeHints: false, allowComments: true, - nostrPrivateKey: '', nostrRelays: DEFAULT_NOSTR_RELAYS, notifications: 0 }, + // lightningAddressByPubkey settings will be added + // for each node's pubkey when connecting to that node. + lightningAddressByPubkey: {}, bolt12Address: { localPart: '' }, @@ -1223,6 +1244,7 @@ export default class SettingsStore { @observable olympians: Array; @observable gods: Array; @observable mortals: Array; + @observable public currentNodeUuid: string | undefined; @observable host: string; @observable port: string; @observable url: string; @@ -1235,6 +1257,9 @@ export default class SettingsStore { @observable public connecting = true; @observable public lurkerExposed = false; private lurkerTimeout: ReturnType | null = null; + @observable private isMigrating = false; + @observable public migrationPromise: Promise | null = null; + private migrationResolve: (() => void) | null = null; // LNDHub @observable username: string; @observable password: string; @@ -1391,6 +1416,14 @@ export default class SettingsStore { return this.macaroonHex || this.accessKey ? true : false; } + private initMigrationPromise() { + if (!this.migrationPromise) { + this.migrationPromise = new Promise((resolve) => { + this.migrationResolve = resolve; + }); + } + } + @action public async getSettings(silentUpdate: boolean = false) { if (!silentUpdate) this.loading = true; @@ -1400,7 +1433,10 @@ export default class SettingsStore { if (modernSettings) { console.log('attempting to load modern settings'); this.settings = JSON.parse(modernSettings); - } else { + } else if (!this.isMigrating) { + this.isMigrating = true; + this.initMigrationPromise(); + console.log('attempting to load legacy settings'); // Retrieve the settings @@ -1421,12 +1457,16 @@ export default class SettingsStore { } else { console.log('No legacy settings stored'); } + + if (this.migrationResolve) this.migrationResolve(); + this.isMigrating = false; } const node: any = this.settings?.nodes?.length && this.settings?.nodes[this.settings.selectedNode || 0]; if (node) { + this.currentNodeUuid = node.uuid; this.host = node.host; this.port = node.port; this.url = node.url; diff --git a/stores/storeInstances.ts b/stores/storeInstances.ts index c00f42075d..96efec3251 100644 --- a/stores/storeInstances.ts +++ b/stores/storeInstances.ts @@ -3,3 +3,4 @@ export const fiatStore = stores.fiatStore; export const notesStore = stores.notesStore; export const settingsStore = stores.settingsStore; export const nodeInfoStore = stores.nodeInfoStore; +export const lightningAddressStore = stores.lightningAddressStore; diff --git a/utils/MigrationUtils.test.ts b/utils/MigrationUtils.test.ts index aa5aa63b69..b3702cc1cb 100644 --- a/utils/MigrationUtils.test.ts +++ b/utils/MigrationUtils.test.ts @@ -3,66 +3,157 @@ jest.mock('react-native-encrypted-storage', () => ({ getItem: jest.fn(), setItem: jest.fn() })); -jest.mock('../stores/Stores', () => ({ +jest.mock('react-native-randombytes', () => ({ + randomBytes: jest.fn() +})); +jest.mock('react-native-notifications', () => ({ + Notifications: { + registerRemoteNotifications: jest.fn(), + events: jest.fn(() => ({ + registerRemoteNotificationsRegistered: jest.fn(), + registerRemoteNotificationsRegistrationFailed: jest.fn(), + registerNotificationReceived: jest.fn(), + registerNotificationOpened: jest.fn() + })) + } +})); +jest.mock('react-native-ping', () => ({ + default: { + start: jest.fn(), + stop: jest.fn(), + getPingStats: jest.fn() + } +})); +jest.mock('react-native-device-info', () => ({ + default: { + getVersion: jest.fn(), + getBuildNumber: jest.fn(), + getModel: jest.fn(), + getSystemVersion: jest.fn(), + getUniqueId: jest.fn() + } +})); +jest.mock('react-native-securerandom', () => ({ + generateSecureRandom: jest.fn() +})); +jest.mock('react-native', () => ({ + NativeModules: { + LndMobile: { + addListener: jest.fn(), + removeListeners: jest.fn() + } + }, + Platform: { + OS: 'android' + }, + NativeEventEmitter: jest.fn(), + DeviceEventEmitter: { + addListener: jest.fn() + } +})); +jest.mock('../storage', () => ({ + getItem: jest.fn(), + setItem: jest.fn() +})); + +jest.mock('../stores/storeInstances', () => ({ settingsStore: { - setSettings: jest.fn() + setSettings: jest.fn(), + settings: {}, + migrationPromise: Promise.resolve(), + currentNodeUuid: 'uuid1' + }, + lightningAddressStore: { + checkLightningAddressExists: jest.fn() + }, + nodeInfoStore: { + nodeInfo: { + identity_pubkey: 'pubkey123' + } } })); jest.mock('../stores/ChannelBackupStore', () => ({})); -jest.mock('../stores/LightningAddressStore', () => ({})); jest.mock('../stores/LSPStore', () => ({})); -jest.mock('../utils/BackendUtils', () => ({})); +jest.mock('../stores/SettingsStore', () => { + class MockSettingsStore { + setSettings = jest.fn(); + settings = {}; + migrationPromise = Promise.resolve(); + } -jest.mock('../stores/SettingsStore', () => ({ - DEFAULT_FIAT_RATES_SOURCE: 'Zeus', - DEFAULT_FIAT: 'USD', - DEFAULT_LSP_MAINNET: 'https://0conf.lnolymp.us', - DEFAULT_LSP_TESTNET: 'https://testnet-0conf.lnolymp.us', - DEFAULT_NOSTR_RELAYS: [ - 'wss://relay.damus.io', - 'wss://nostr.land', - 'wss://nostr.wine', - 'wss://nos.lol', - 'wss://relay.snort.social' - ], - DEFAULT_NEUTRINO_PEERS_MAINNET: [ - 'btcd1.lnolymp.us', - 'btcd2.lnolymp.us', - 'btcd-mainnet.lightning.computer', - 'node.eldamar.icu', - 'noad.sathoarder.com' - ], - DEFAULT_NEUTRINO_PEERS_TESTNET: [ - 'testnet.lnolymp.us', - 'btcd-testnet.lightning.computer', - 'testnet.blixtwallet.com' - ], - DEFAULT_LSPS1_HOST_MAINNET: '45.79.192.236:9735', - DEFAULT_LSPS1_HOST_TESTNET: '139.144.22.237:9735', - DEFAULT_LSPS1_PUBKEY_MAINNET: - '031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581', - DEFAULT_LSPS1_PUBKEY_TESTNET: - '03e84a109cd70e57864274932fc87c5e6434c59ebb8e6e7d28532219ba38f7f6df', - DEFAULT_LSPS1_REST_MAINNET: 'https://lsps1.lnolymp.us', - DEFAULT_LSPS1_REST_TESTNET: 'https://testnet-lsps1.lnolymp.us', - DEFAULT_SPEEDLOADER: 'https://egs.lnze.us/', - DEFAULT_NOSTR_RELAYS_2023: [ - 'wss://nostr.mutinywallet.com', - 'wss://relay.damus.io', - 'wss://nostr.lnproxy.org' - ], - DEFAULT_SLIDE_TO_PAY_THRESHOLD: 10000, - STORAGE_KEY: 'zeus-settings-v2', - LEGACY_CURRENCY_CODES_KEY: 'currency-codes', - CURRENCY_CODES_KEY: 'zeus-currency-codes', - PosEnabled: { - Disabled: 'disabled', - Square: 'square', - Standalone: 'standalone' + return { + DEFAULT_FIAT_RATES_SOURCE: 'Zeus', + DEFAULT_FIAT: 'USD', + DEFAULT_LSP_MAINNET: 'https://0conf.lnolymp.us', + DEFAULT_LSP_TESTNET: 'https://testnet-0conf.lnolymp.us', + DEFAULT_NOSTR_RELAYS: [ + 'wss://relay.damus.io', + 'wss://nostr.land', + 'wss://nostr.wine', + 'wss://nos.lol', + 'wss://relay.snort.social' + ], + DEFAULT_NEUTRINO_PEERS_MAINNET: [ + 'btcd1.lnolymp.us', + 'btcd2.lnolymp.us', + 'btcd-mainnet.lightning.computer', + 'node.eldamar.icu', + 'noad.sathoarder.com' + ], + DEFAULT_NEUTRINO_PEERS_TESTNET: [ + 'testnet.lnolymp.us', + 'btcd-testnet.lightning.computer', + 'testnet.blixtwallet.com' + ], + DEFAULT_LSPS1_HOST_MAINNET: '45.79.192.236:9735', + DEFAULT_LSPS1_HOST_TESTNET: '139.144.22.237:9735', + DEFAULT_LSPS1_PUBKEY_MAINNET: + '031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581', + DEFAULT_LSPS1_PUBKEY_TESTNET: + '03e84a109cd70e57864274932fc87c5e6434c59ebb8e6e7d28532219ba38f7f6df', + DEFAULT_LSPS1_REST_MAINNET: 'https://lsps1.lnolymp.us', + DEFAULT_LSPS1_REST_TESTNET: 'https://testnet-lsps1.lnolymp.us', + DEFAULT_SPEEDLOADER: 'https://egs.lnze.us/', + DEFAULT_NOSTR_RELAYS_2023: [ + 'wss://nostr.mutinywallet.com', + 'wss://relay.damus.io', + 'wss://nostr.lnproxy.org' + ], + DEFAULT_SLIDE_TO_PAY_THRESHOLD: 10000, + STORAGE_KEY: 'zeus-settings-v2', + LEGACY_CURRENCY_CODES_KEY: 'currency-codes', + CURRENCY_CODES_KEY: 'zeus-currency-codes', + PosEnabled: { + Disabled: 'disabled', + Square: 'square', + Standalone: 'standalone' + }, + default: MockSettingsStore + }; +}); +jest.mock('../stores/Stores', () => ({ + default: { + settingsStore: new (jest.fn(() => ({ + setSettings: jest.fn(), + settings: {}, + migrationPromise: Promise.resolve() + })))(), + modalStore: {}, + offersStore: {}, + fiatStore: {} } })); -import MigrationUtils from './MigrationUtils'; +jest.mock('../utils/BackendUtils', () => ({})); + +import migrationUtils from './MigrationUtils'; +import Storage from '../storage'; +import { Settings } from '../stores/SettingsStore'; +import { + settingsStore, + lightningAddressStore, + nodeInfoStore +} from '../stores/storeInstances'; describe('MigrationUtils', () => { const defaultSettings = { @@ -122,17 +213,17 @@ describe('MigrationUtils', () => { speedloader: 'https://egs.lnze.us/' }; - describe('MigrationUtils', () => { + describe('MigrationUtils Legacy Settings', () => { it('handles empty settings', async () => { await expect( - MigrationUtils.legacySettingsMigrations('{}') + migrationUtils.legacySettingsMigrations('{}') ).resolves.toEqual({ ...defaultSettings }); }); it('handles mod1', async () => { await expect( - MigrationUtils.legacySettingsMigrations( + migrationUtils.legacySettingsMigrations( JSON.stringify({ requestSimpleTaproot: false, fiatRatesSource: 'Yadio' @@ -145,7 +236,7 @@ describe('MigrationUtils', () => { }); it('handles mod2', async () => { await expect( - MigrationUtils.legacySettingsMigrations( + migrationUtils.legacySettingsMigrations( JSON.stringify({ lspMainnet: 'https://lsp-preview.lnolymp.us', lspTestnet: 'https://testnet-lsp.lnolymp.us' @@ -157,7 +248,7 @@ describe('MigrationUtils', () => { }); it('handles mod3', async () => { await expect( - MigrationUtils.legacySettingsMigrations( + migrationUtils.legacySettingsMigrations( JSON.stringify({ neutrinoPeersMainnet: [ 'btcd1.lnolymp.us', @@ -174,7 +265,7 @@ describe('MigrationUtils', () => { }); it('handles mod7', async () => { await expect( - MigrationUtils.legacySettingsMigrations( + migrationUtils.legacySettingsMigrations( JSON.stringify({ bimodalPathfinding: true }) @@ -186,7 +277,7 @@ describe('MigrationUtils', () => { }); it('handles mod8', async () => { await expect( - MigrationUtils.legacySettingsMigrations( + migrationUtils.legacySettingsMigrations( JSON.stringify({ lightningAddress: { nostrRelays: [ @@ -211,7 +302,7 @@ describe('MigrationUtils', () => { }); it('migrates old POS squareEnabled setting to posEnabled', async () => { await expect( - MigrationUtils.legacySettingsMigrations( + migrationUtils.legacySettingsMigrations( JSON.stringify({ pos: { squareEnabled: true @@ -227,4 +318,168 @@ describe('MigrationUtils', () => { }); }); }); + + describe('migrateLightningAddressSettings', () => { + beforeEach(() => { + jest.clearAllMocks(); + Storage.getItem = jest.fn(); + Storage.setItem = jest.fn(); + settingsStore.setSettings = jest.fn(); + lightningAddressStore.checkLightningAddressExists = jest.fn(); + }); + + it('creates global settings at first run and puts only those node UUIDs on todo list that need migration', async () => { + settingsStore.settings = { + nodes: [ + { implementation: 'embedded-lnd', uuid: 'uuid1' }, + { implementation: 'lnd', uuid: 'uuid2' }, + { implementation: 'lndhub', uuid: 'uuid3' } + ], + lightningAddress: { + automaticallyAccept: true, + automaticallyAcceptAttestationLevel: 2, + routeHints: false, + allowComments: true, + nostrRelays: ['relay1', 'relay2'], + notifications: 0, + nostrPrivateKey: 'test-key' + } + } as Settings; + + // simulate first run + (Storage.getItem as jest.Mock).mockResolvedValue(null); + + const result = + await migrationUtils.migrateLightningAddressSettings(); + + expect(result).toBe(true); + expect(Storage.setItem).toHaveBeenCalledWith( + 'lightning-address-settings-split-2025', + ['uuid1', 'uuid2'] + ); + expect(settingsStore.setSettings).toHaveBeenCalledWith( + expect.objectContaining({ + lightningAddressGlobal: { + automaticallyAccept: true, + automaticallyAcceptAttestationLevel: 2, + routeHints: false, + allowComments: true, + nostrRelays: ['relay1', 'relay2'], + notifications: 0 + } + }) + ); + }); + + it('creates global settings at first run and deletes legacy settings when there are only nodes that do not need migration', async () => { + settingsStore.settings = { + nodes: [ + { implementation: 'lndhub', uuid: 'uuid1' }, + { implementation: 'cln-rest', uuid: 'uuid2' } + ], + lightningAddress: { + automaticallyAccept: true + } + } as Settings; + + // simulate first run + (Storage.getItem as jest.Mock).mockResolvedValue(null); + + const result = + await migrationUtils.migrateLightningAddressSettings(); + + expect(result).toBe(true); + expect(settingsStore.setSettings).toHaveBeenCalledWith( + expect.objectContaining({ + lightningAddressGlobal: { + automaticallyAccept: true, + automaticallyAcceptAttestationLevel: undefined, + routeHints: undefined, + allowComments: undefined, + nostrRelays: undefined, + notifications: undefined + } + }) + ); + expect(settingsStore.setSettings).toHaveBeenCalledWith( + expect.not.objectContaining({ + lightningAddress: expect.anything() + }) + ); + }); + + it('creates global settings at first run and returns true when checkLightningAddressExists()', async () => { + settingsStore.currentNodeUuid = 'uuid1'; + + // simulate first run + (Storage.getItem as jest.Mock).mockResolvedValue(null); + ( + lightningAddressStore.checkLightningAddressExists as jest.Mock + ).mockRejectedValue(new Error('error')); + + const result = + await migrationUtils.migrateLightningAddressSettings(); + + expect(settingsStore.setSettings).toHaveBeenCalledWith( + expect.objectContaining({ + lightningAddressGlobal: expect.any(Object) + }) + ); + expect(result).toBe(true); + }); + + it('creates lightningAddressByPubkey settings for current node, removes node from migration todo list and deletes legacy settings when it was the last node to migrate', async () => { + settingsStore.settings = { + lightningAddress: { + nostrPrivateKey: 'privateKey123' + } + } as Settings; + settingsStore.currentNodeUuid = 'uuid1'; + nodeInfoStore.nodeInfo = { identity_pubkey: 'pubkey123' }; + + (Storage.getItem as jest.Mock).mockResolvedValue( + JSON.stringify(['uuid1']) + ); + + ( + lightningAddressStore.checkLightningAddressExists as jest.Mock + ).mockResolvedValue(true); + + const result = + await migrationUtils.migrateLightningAddressSettings(); + + expect(result).toBe(true); + expect(settingsStore.setSettings).toHaveBeenCalledWith( + expect.objectContaining({ + lightningAddressByPubkey: { + pubkey123: { + enabled: true, + nostrPrivateKey: 'privateKey123' + } + } + }) + ); + expect(Storage.setItem).toHaveBeenCalledWith( + 'lightning-address-settings-split-2025', + [] + ); + expect(settingsStore.setSettings).toHaveBeenCalledWith( + expect.not.objectContaining({ + lightningAddress: expect.anything() + }) + ); + }); + + it('skips migration when all nodes are migrated', async () => { + (Storage.getItem as jest.Mock).mockResolvedValue( + JSON.stringify([]) + ); + + const result = + await migrationUtils.migrateLightningAddressSettings(); + + expect(result).toBe(false); + expect(settingsStore.setSettings).not.toHaveBeenCalled(); + }); + }); }); diff --git a/utils/MigrationUtils.ts b/utils/MigrationUtils.ts index 6e6bd34b01..a423e383b8 100644 --- a/utils/MigrationUtils.ts +++ b/utils/MigrationUtils.ts @@ -1,4 +1,8 @@ -import stores from '../stores/Stores'; +import { + settingsStore, + nodeInfoStore, + lightningAddressStore +} from '../stores/storeInstances'; import { Settings, DEFAULT_FIAT_RATES_SOURCE, @@ -36,9 +40,7 @@ import { LAST_CHANNEL_BACKUP_TIME } from '../stores/ChannelBackupStore'; import { - LEGACY_ADDRESS_ACTIVATED_STRING, LEGACY_HASHES_STORAGE_STRING, - ADDRESS_ACTIVATED_STRING, HASHES_STORAGE_STRING } from '../stores/LightningAddressStore'; import { @@ -65,6 +67,7 @@ import { LNC_STORAGE_KEY, hash } from '../backends/LNC/credentialStore'; import EncryptedStorage from 'react-native-encrypted-storage'; import Storage from '../storage'; +import { v4 as uuidv4 } from 'uuid'; class MigrationsUtils { public async legacySettingsMigrations(settings: string) { @@ -119,7 +122,7 @@ class MigrationsUtils { const mod = await EncryptedStorage.getItem(MOD_KEY); if (!mod) { newSettings.requestSimpleTaproot = true; - stores.settingsStore.setSettings(JSON.stringify(newSettings)); + settingsStore.setSettings(JSON.stringify(newSettings)); await EncryptedStorage.setItem(MOD_KEY, 'true'); } @@ -132,7 +135,7 @@ class MigrationsUtils { if (newSettings?.lspTestnet === 'https://testnet-lsp.lnolymp.us') { newSettings.lspTestnet = DEFAULT_LSP_TESTNET; } - stores.settingsStore.setSettings(JSON.stringify(newSettings)); + settingsStore.setSettings(JSON.stringify(newSettings)); await EncryptedStorage.setItem(MOD_KEY2, 'true'); } @@ -153,7 +156,7 @@ class MigrationsUtils { newSettings.neutrinoPeersMainnet = DEFAULT_NEUTRINO_PEERS_MAINNET; } - stores.settingsStore.setSettings(JSON.stringify(newSettings)); + settingsStore.setSettings(JSON.stringify(newSettings)); await EncryptedStorage.setItem(MOD_KEY3, 'true'); } @@ -187,7 +190,7 @@ class MigrationsUtils { newSettings.lsps1ShowPurchaseButton = true; } - stores.settingsStore.setSettings(JSON.stringify(newSettings)); + settingsStore.setSettings(JSON.stringify(newSettings)); await EncryptedStorage.setItem(MOD_KEY4, 'true'); } @@ -204,7 +207,7 @@ class MigrationsUtils { } } - stores.settingsStore.setSettings(JSON.stringify(newSettings)); + settingsStore.setSettings(JSON.stringify(newSettings)); await EncryptedStorage.setItem(MOD_KEY5, 'true'); } @@ -216,7 +219,7 @@ class MigrationsUtils { newSettings.customSpeedloader = ''; } - stores.settingsStore.setSettings(JSON.stringify(newSettings)); + settingsStore.setSettings(JSON.stringify(newSettings)); await EncryptedStorage.setItem(MOD_KEY6, 'true'); } @@ -229,7 +232,7 @@ class MigrationsUtils { newSettings.bimodalPathfinding = false; } - stores.settingsStore.setSettings(JSON.stringify(newSettings)); + settingsStore.setSettings(JSON.stringify(newSettings)); await EncryptedStorage.setItem(MOD_KEY7, 'true'); } @@ -243,10 +246,29 @@ class MigrationsUtils { newSettings.lightningAddress.nostrRelays = DEFAULT_NOSTR_RELAYS; } - stores.settingsStore.setSettings(JSON.stringify(newSettings)); + settingsStore.setSettings(JSON.stringify(newSettings)); await EncryptedStorage.setItem(MOD_KEY8, 'true'); } + const MOD_KEY9 = 'add-node-uuids'; + const mod9 = await EncryptedStorage.getItem(MOD_KEY9); + if (!mod9 && newSettings.nodes) { + console.log('Starting UUID addition for nodes'); + newSettings.nodes = newSettings.nodes.map((node: any) => { + if (!node.uuid) { + return { + ...node, + uuid: uuidv4() + }; + } + return node; + }); + + settingsStore.setSettings(JSON.stringify(newSettings)); + await EncryptedStorage.setItem(MOD_KEY9, 'true'); + console.log('UUID addition completed'); + } + // migrate old POS squareEnabled setting to posEnabled if (newSettings?.pos?.squareEnabled) { newSettings.pos.posEnabled = PosEnabled.Square; @@ -360,42 +382,21 @@ class MigrationsUtils { // Lightning address migration const lightningAddressMigration = (async () => { try { - let activatedSuccess: any = true; - let hashesSuccess: any = true; - - const activated = await EncryptedStorage.getItem( - LEGACY_ADDRESS_ACTIVATED_STRING - ); - if (activated) { - console.log( - 'Attemping lightning address activated migration' - ); - activatedSuccess = await Storage.setItem( - ADDRESS_ACTIVATED_STRING, - activated - ); - console.log( - 'Lightning address activated migration status', - activatedSuccess - ); - } - const hashes = await EncryptedStorage.getItem( LEGACY_HASHES_STORAGE_STRING ); if (hashes) { console.log('Attemping lightning address hashes migration'); - hashesSuccess = await Storage.setItem( + const writeSuccess = await Storage.setItem( HASHES_STORAGE_STRING, hashes ); console.log( 'Lightning address hashes migration status', - hashesSuccess + writeSuccess ); + return writeSuccess; } - - return activatedSuccess && hashesSuccess; } catch (error) { console.error( 'Error loading lightning address data from encrypted storage', @@ -699,6 +700,141 @@ class MigrationsUtils { const results = await Promise.all(migrationTasks); return results.every((result) => result === true); } + + public migrateLightningAddressSettings = async (): Promise => { + console.log('migrateLightningAddressSettings() started'); + // We need to wait until other migrations are done + await settingsStore.migrationPromise; + + const newSettings = JSON.parse( + JSON.stringify(settingsStore.settings) + ) as Settings; + + let globalSettingsChanged = false; + + const MOD_KEY10 = 'lightning-address-settings-split-2025'; + const mod10String = await Storage.getItem(MOD_KEY10); + let mod10Array = mod10String ? JSON.parse(mod10String) : []; + + if (!mod10String) { + // At first run we need to + // - add UUIDs from all nodes but lndhub and cln-rest to MOD_KEY10 + // - set up global settings from legacy settings + const nodesToMigrate = newSettings.nodes + ?.filter( + (node: any) => + node.implementation === 'embedded-lnd' || + node.implementation === 'lnd' || + node.implementation === 'lightning-node-connect' + ) + .map((node: any) => node.uuid); + + await Storage.setItem(MOD_KEY10, nodesToMigrate); + mod10Array = nodesToMigrate; + + // --- Set up global settings from legacy settings + if (newSettings.lightningAddress) { + newSettings.lightningAddressGlobal = { + automaticallyAccept: + newSettings.lightningAddress.automaticallyAccept, + automaticallyAcceptAttestationLevel: + newSettings.lightningAddress + .automaticallyAcceptAttestationLevel, + routeHints: newSettings.lightningAddress.routeHints, + allowComments: newSettings.lightningAddress.allowComments, + nostrRelays: newSettings.lightningAddress.nostrRelays, + notifications: newSettings.lightningAddress.notifications + }; + globalSettingsChanged = true; + console.log( + 'Global settings created:', + newSettings.lightningAddressGlobal + ); + + if (!nodesToMigrate?.length) { + delete newSettings.lightningAddress; + await settingsStore.setSettings(newSettings); + console.log( + 'No nodes to migrate, legacy settings deleted. Migration completed.' + ); + return true; + } + } + } else if ((mod10Array as string[]).length === 0) { + console.log( + 'Lightning Address migration skipped - all nodes migrated' + ); + return false; + } + + const currentNodeUuid = settingsStore.currentNodeUuid; + console.log('Current node uuid:', currentNodeUuid); + + // Run migration if this node's UUID is still in mod10Array + if ( + newSettings.lightningAddress && + mod10Array.includes(currentNodeUuid) + ) { + // --- Set up node-specific settings --- + // First we check if Lightning Address exists for this node, so we can + // - set correct flag "enabled" per pubkey + // - use existing nostr priv key + const currentPubkey = nodeInfoStore.nodeInfo.identity_pubkey; + console.log( + 'Starting Lightning Address migration for pubkey:', + currentPubkey + ); + + try { + const hasLightningAddress = + await lightningAddressStore.checkLightningAddressExists(); + newSettings.lightningAddressByPubkey = { + ...newSettings.lightningAddressByPubkey, + [currentPubkey]: { + enabled: hasLightningAddress === true, + nostrPrivateKey: + hasLightningAddress === true + ? newSettings.lightningAddress.nostrPrivateKey + : '' + } + }; + console.log( + 'Node-specific settings created:', + newSettings.lightningAddressByPubkey[currentPubkey] + ); + + const remainingUuids = mod10Array.filter( + (uuid: string) => uuid !== currentNodeUuid + ); + if (remainingUuids.length === 0) { + delete newSettings.lightningAddress; + console.log('All nodes migrated, legacy settings deleted'); + } + await Storage.setItem(MOD_KEY10, remainingUuids); + + await settingsStore.setSettings(newSettings); + console.log( + 'Migration completed and marked as done for pubkey:', + currentPubkey + ); + return true; + } catch (error) { + // If checkLightningAddressExists() throws error, but ln address global settings + // were changed, we still need to call getSettings() in Wallet.tsx + console.log('Migration error details:', { + error, + errorMessage: (error as Error).message, + errorStack: (error as Error).stack + }); + return globalSettingsChanged; + } + } else { + console.log( + 'Migration skipped because because UUID is not in mod10Array or no legacy settings exist' + ); + } + return false; + }; } const migrationsUtils = new MigrationsUtils(); diff --git a/views/Activity/ActivityFilter.tsx b/views/Activity/ActivityFilter.tsx index 166b9f8e70..34886142bd 100644 --- a/views/Activity/ActivityFilter.tsx +++ b/views/Activity/ActivityFilter.tsx @@ -12,6 +12,7 @@ import { themeColor } from '../../utils/ThemeUtils'; import ActivityStore, { DEFAULT_FILTERS } from '../../stores/ActivityStore'; import SettingsStore from '../../stores/SettingsStore'; +import NodeInfoStore from '../../stores/NodeInfoStore'; import Header from '../../components/Header'; import Screen from '../../components/Screen'; @@ -22,6 +23,7 @@ interface ActivityFilterProps { navigation: StackNavigationProp; ActivityStore: ActivityStore; SettingsStore: SettingsStore; + NodeInfoStore: NodeInfoStore; } interface ActivityFilterState { @@ -31,7 +33,7 @@ interface ActivityFilterState { workingEndDate: any; } -@inject('ActivityStore', 'SettingsStore') +@inject('ActivityStore', 'SettingsStore', 'NodeInfoStore') @observer export default class ActivityFilter extends React.Component< ActivityFilterProps, @@ -230,7 +232,10 @@ export default class ActivityFilter extends React.Component< value: zeusPay, var: 'zeusPay', type: 'Toggle', - condition: SettingsStore.settings.lightningAddress.enabled + condition: + SettingsStore.settings.lightningAddressByPubkey[ + this.props.NodeInfoStore.nodeInfo.identity_pubkey + ]?.enabled }, { label: localeString('general.unconfirmed'), diff --git a/views/Receive.tsx b/views/Receive.tsx index 442807a5b5..86c48867c7 100644 --- a/views/Receive.tsx +++ b/views/Receive.tsx @@ -230,7 +230,12 @@ export default class Receive extends React.Component< const settings = await getSettings(); - if (settings?.lightningAddress?.enabled && !lightningAddressHandle) { + if ( + settings?.lightningAddressByPubkey[ + NodeInfoStore.nodeInfo.identity_pubkey + ]?.enabled && + !lightningAddressHandle + ) { status(); } diff --git a/views/Settings/LightningAddress/LightningAddressSettings.tsx b/views/Settings/LightningAddress/LightningAddressSettings.tsx index b6372ee50e..acbba32832 100644 --- a/views/Settings/LightningAddress/LightningAddressSettings.tsx +++ b/views/Settings/LightningAddress/LightningAddressSettings.tsx @@ -17,6 +17,7 @@ import SettingsStore, { AUTOMATIC_ATTESTATION_KEYS } from '../../../stores/SettingsStore'; import LightningAddressStore from '../../../stores/LightningAddressStore'; +import NodeInfoStore from '../../../stores/NodeInfoStore'; import { localeString } from '../../../utils/LocaleUtils'; import { themeColor } from '../../../utils/ThemeUtils'; @@ -25,6 +26,7 @@ interface LightningAddressSettingsProps { navigation: StackNavigationProp; SettingsStore: SettingsStore; LightningAddressStore: LightningAddressStore; + NodeInfoStore: NodeInfoStore; } interface LightningAddressSettingsState { @@ -37,7 +39,7 @@ interface LightningAddressSettingsState { notifications: number; } -@inject('SettingsStore', 'LightningAddressStore') +@inject('SettingsStore', 'LightningAddressStore', 'NodeInfoStore') @observer export default class LightningAddressSettings extends React.Component< LightningAddressSettingsProps, @@ -54,27 +56,24 @@ export default class LightningAddressSettings extends React.Component< }; async UNSAFE_componentWillMount() { - const { SettingsStore } = this.props; - const { settings } = SettingsStore; + const { SettingsStore, NodeInfoStore } = this.props; + const lightningAddressGlobal = + SettingsStore.settings.lightningAddressGlobal; this.setState({ - automaticallyAccept: settings.lightningAddress?.automaticallyAccept - ? true - : false, - automaticallyAcceptAttestationLevel: settings.lightningAddress - ?.automaticallyAcceptAttestationLevel - ? settings.lightningAddress.automaticallyAcceptAttestationLevel - : 2, - routeHints: settings.lightningAddress?.routeHints ? true : false, - allowComments: settings.lightningAddress?.allowComments - ? true - : false, - nostrPrivateKey: settings.lightningAddress?.nostrPrivateKey || '', - nostrRelays: settings.lightningAddress?.nostrRelays || [], - notifications: - settings.lightningAddress?.notifications !== undefined - ? settings.lightningAddress.notifications - : 1 + // Global settings + automaticallyAccept: lightningAddressGlobal.automaticallyAccept, + automaticallyAcceptAttestationLevel: + lightningAddressGlobal.automaticallyAcceptAttestationLevel, + routeHints: lightningAddressGlobal.routeHints, + allowComments: lightningAddressGlobal.allowComments, + nostrRelays: lightningAddressGlobal.nostrRelays, + notifications: lightningAddressGlobal.notifications, + // Node-specific settings + nostrPrivateKey: + SettingsStore.settings.lightningAddressByPubkey?.[ + NodeInfoStore.nodeInfo.identity_pubkey + ].nostrPrivateKey ?? '' }); } @@ -85,12 +84,10 @@ export default class LightningAddressSettings extends React.Component< automaticallyAcceptAttestationLevel, routeHints, allowComments, - nostrPrivateKey, nostrRelays, notifications } = this.state; const { updateSettings, settings }: any = SettingsStore; - const enabled = settings?.lightningAddress?.enabled; const { loading, update, error_msg } = LightningAddressStore; return ( @@ -150,18 +147,10 @@ export default class LightningAddressSettings extends React.Component< !automaticallyAccept }); await updateSettings({ - lightningAddress: { - enabled, + lightningAddressGlobal: { + ...settings.lightningAddressGlobal, automaticallyAccept: - !automaticallyAccept, - automaticallyAcceptAttestationLevel, - automaticallyRequestOlympusChannels: - false, // deprecated - routeHints, - allowComments, - nostrPrivateKey, - nostrRelays, - notifications + !automaticallyAccept } }); }} @@ -182,18 +171,10 @@ export default class LightningAddressSettings extends React.Component< value }); await updateSettings({ - lightningAddress: { - enabled, - automaticallyAccept, + lightningAddressGlobal: { + ...settings.lightningAddressGlobal, automaticallyAcceptAttestationLevel: - value, - automaticallyRequestOlympusChannels: - false, // deprecated - routeHints, - allowComments, - nostrPrivateKey, - nostrRelays, - notifications + value } }); }} @@ -236,17 +217,9 @@ export default class LightningAddressSettings extends React.Component< routeHints: !routeHints }); await updateSettings({ - lightningAddress: { - enabled, - automaticallyAccept, - automaticallyAcceptAttestationLevel, - automaticallyRequestOlympusChannels: - false, // deprecated - routeHints: !routeHints, - allowComments, - nostrPrivateKey, - nostrRelays, - notifications + lightningAddressGlobal: { + ...settings.lightningAddressGlobal, + routeHints: !routeHints } }); }} @@ -287,18 +260,10 @@ export default class LightningAddressSettings extends React.Component< !allowComments }); await updateSettings({ - lightningAddress: { - enabled, - automaticallyAccept, - automaticallyAcceptAttestationLevel, - automaticallyRequestOlympusChannels: - false, // deprecated - routeHints, + lightningAddressGlobal: { + ...settings.lightningAddressGlobal, allowComments: - !allowComments, - nostrPrivateKey, - nostrRelays, - notifications + !allowComments } }); }); @@ -322,16 +287,8 @@ export default class LightningAddressSettings extends React.Component< notifications: value }); await updateSettings({ - lightningAddress: { - enabled, - automaticallyAccept, - automaticallyAcceptAttestationLevel, - automaticallyRequestOlympusChannels: - false, // deprecated - routeHints, - allowComments, - nostrPrivateKey, - nostrRelays, + lightningAddressGlobal: { + ...settings.lightningAddressGlobal, notifications: value } }); diff --git a/views/Settings/LightningAddress/NostrKeys.tsx b/views/Settings/LightningAddress/NostrKeys.tsx index f2a1b09794..a92c387d66 100644 --- a/views/Settings/LightningAddress/NostrKeys.tsx +++ b/views/Settings/LightningAddress/NostrKeys.tsx @@ -23,6 +23,7 @@ import { import { Row } from '../../../components/layout/Row'; import SettingsStore from '../../../stores/SettingsStore'; +import NodeInfoStore from '../../../stores/NodeInfoStore'; import LightningAddressStore from '../../../stores/LightningAddressStore'; import { localeString } from '../../../utils/LocaleUtils'; @@ -36,6 +37,7 @@ import Edit from '../../../assets/images/SVG/Pen.svg'; interface NostrKeyProps { navigation: StackNavigationProp; SettingsStore: SettingsStore; + NodeInfoStore: NodeInfoStore; LightningAddressStore: LightningAddressStore; route: Route<'NostrKey', { setup: boolean; nostrPrivateKey: string }>; } @@ -51,7 +53,7 @@ interface NostrKeyState { revealSensitive: boolean; } -@inject('SettingsStore', 'LightningAddressStore') +@inject('SettingsStore', 'NodeInfoStore', 'LightningAddressStore') @observer export default class NostrKey extends React.Component< NostrKeyProps, @@ -89,7 +91,10 @@ export default class NostrKey extends React.Component< const setup = route.params?.setup; const nostrPrivateKey = route.params?.nostrPrivateKey ?? - (settings?.lightningAddress?.nostrPrivateKey || ''); + (settings?.lightningAddressByPubkey[ + this.props.NodeInfoStore.nodeInfo.identity_pubkey + ]?.nostrPrivateKey || + ''); let nostrPublicKey, nostrNsec, nostrNpub; if (nostrPrivateKey) { @@ -111,7 +116,12 @@ export default class NostrKey extends React.Component< } render() { - const { navigation, LightningAddressStore, SettingsStore } = this.props; + const { + navigation, + LightningAddressStore, + SettingsStore, + NodeInfoStore + } = this.props; const { existingNostrPrivateKey, nostrPrivateKey, @@ -124,14 +134,6 @@ export default class NostrKey extends React.Component< } = this.state; const { update, error_msg, loading } = LightningAddressStore; const { updateSettings, settings } = SettingsStore; - const { lightningAddress } = settings; - const { - enabled, - automaticallyAccept, - allowComments, - nostrRelays, - notifications - } = lightningAddress; const VisibilityButton = () => ( @@ -356,7 +358,8 @@ export default class NostrKey extends React.Component< ); } else { const relays = - settings.lightningAddress + settings + .lightningAddressGlobal .nostrRelays; const relays_sig = bytesToHex( schnorr.sign( @@ -384,16 +387,15 @@ export default class NostrKey extends React.Component< editMode: false }); await updateSettings({ - lightningAddress: { - enabled, - automaticallyAccept, - automaticallyRequestOlympusChannels: - false, // deprecated - allowComments, - nostrPrivateKey, - nostrRelays, - notifications - } + lightningAddressByPubkey: + { + [NodeInfoStore + .nodeInfo + .identity_pubkey]: + { + nostrPrivateKey + } + } }); }); } catch (e) {} diff --git a/views/Settings/LightningAddress/NostrRelays.tsx b/views/Settings/LightningAddress/NostrRelays.tsx index e4a4f8414e..dc895a695a 100644 --- a/views/Settings/LightningAddress/NostrRelays.tsx +++ b/views/Settings/LightningAddress/NostrRelays.tsx @@ -68,7 +68,7 @@ export default class NostrRelays extends React.Component< setup, relays: relays ? relays - : settings.lightningAddress.nostrRelays || [] + : settings.lightningAddressGlobal.nostrRelays || [] }); } @@ -77,13 +77,7 @@ export default class NostrRelays extends React.Component< const { relays, addRelay, setup } = this.state; const { updateSettings, settings }: any = SettingsStore; const { lightningAddress } = settings; - const { - enabled, - automaticallyAccept, - allowComments, - nostrPrivateKey, - notifications - } = lightningAddress; + const { nostrPrivateKey } = lightningAddress; const { update, loading, error_msg } = LightningAddressStore; return ( @@ -207,17 +201,11 @@ export default class NostrRelays extends React.Component< addRelay: '' }); await updateSettings({ - lightningAddress: { - enabled, - automaticallyAccept, - automaticallyRequestOlympusChannels: - false, // deprecated - allowComments, - nostrPrivateKey, - nostrRelays: - newNostrRelays, - notifications - } + lightningAddressGlobal: + { + nostrRelays: + newNostrRelays + } }); }); } catch (e) {} @@ -298,17 +286,10 @@ export default class NostrRelays extends React.Component< ); await updateSettings( { - lightningAddress: + lightningAddressGlobal: { - enabled, - automaticallyAccept, - automaticallyRequestOlympusChannels: - false, // deprecated - allowComments, - nostrPrivateKey, nostrRelays: - newNostrRelays, - notifications + newNostrRelays } } ); diff --git a/views/Settings/LightningAddress/index.tsx b/views/Settings/LightningAddress/index.tsx index 10adcc40a0..e1838fc7e0 100644 --- a/views/Settings/LightningAddress/index.tsx +++ b/views/Settings/LightningAddress/index.tsx @@ -175,7 +175,7 @@ export default class LightningAddress extends React.Component< const { fontScale } = Dimensions.get('window'); const automaticallyAccept = - SettingsStore.settings?.lightningAddress?.automaticallyAccept; + SettingsStore.settings?.lightningAddressGlobal?.automaticallyAccept; const isReady = SettingsStore.implementation !== 'embedded-lnd' || !prepareToAutomaticallyAcceptStart || diff --git a/views/Settings/WalletConfiguration.tsx b/views/Settings/WalletConfiguration.tsx index 005ae8d339..e4bfd17c42 100644 --- a/views/Settings/WalletConfiguration.tsx +++ b/views/Settings/WalletConfiguration.tsx @@ -14,6 +14,7 @@ import cloneDeep from 'lodash/cloneDeep'; import differenceBy from 'lodash/differenceBy'; import { Route } from '@react-navigation/native'; import { StackNavigationProp } from '@react-navigation/stack'; +import { v4 as uuidv4 } from 'uuid'; import { hash, LNC_STORAGE_KEY } from '../../backends/LNC/credentialStore'; @@ -469,7 +470,8 @@ export default class WalletConfiguration extends React.Component< walletPassword, adminMacaroon, embeddedLndNetwork, - photo + photo, + uuid: uuidv4() }; let nodes: Node[]; @@ -587,6 +589,7 @@ export default class WalletConfiguration extends React.Component< const { index } = this.state; const { nodes } = settings; + const deletedNode = nodes![index!]; const newNodes: any = []; for (let i = 0; nodes && i < nodes.length; i++) { if (index !== i) { @@ -597,7 +600,17 @@ export default class WalletConfiguration extends React.Component< updateSettings({ nodes: newNodes, selectedNode: this.getNewSelectedNodeIndex(index, settings) - }).then(() => { + }).then(async () => { + // Remove UUID from migration todo list if it's on there + const MOD_KEY9 = 'lightning-address-pubkey-2025'; + const uuidArray = (await Storage.getItem(MOD_KEY9)) || []; + if (uuidArray.includes(deletedNode.uuid)) { + const remainingUuids = uuidArray.filter( + (uuid: string) => uuid !== deletedNode.uuid + ); + await Storage.setItem(MOD_KEY9, remainingUuids); + } + if (newNodes.length === 0) { navigation.navigate('IntroSplash'); } else { diff --git a/views/Wallet/Wallet.tsx b/views/Wallet/Wallet.tsx index 8c4e9e35d7..ea4140b929 100644 --- a/views/Wallet/Wallet.tsx +++ b/views/Wallet/Wallet.tsx @@ -49,6 +49,7 @@ import { import { localeString } from '../../utils/LocaleUtils'; import { protectedNavigation } from '../../utils/NavigationUtils'; import { isLightTheme, themeColor } from '../../utils/ThemeUtils'; +import MigrationUtils from '../../utils/MigrationUtils'; import Storage from '../../storage'; @@ -332,7 +333,6 @@ export default class Wallet extends React.Component { rescan, compactDb, recovery, - lightningAddress, embeddedTor, initialLoad } = settings; @@ -403,6 +403,13 @@ export default class Wallet extends React.Component { if (implementation === 'embedded-lnd') SyncStore.checkRecoveryStatus(); await NodeInfoStore.getNodeInfo(); + + const didMigrate = + await MigrationUtils.migrateLightningAddressSettings(); + if (didMigrate) { + await SettingsStore.getSettings(); + } + NodeInfoStore.getNetworkInfo(); if (BackendUtils.supportsAccounts()) UTXOsStore.listAccounts(); await BalanceStore.getCombinedBalance(false); @@ -456,6 +463,13 @@ export default class Wallet extends React.Component { try { await BackendUtils.checkPerms(); await NodeInfoStore.getNodeInfo(); + + const didMigrate = + await MigrationUtils.migrateLightningAddressSettings(); + if (didMigrate) { + await SettingsStore.getSettings(); + } + if (BackendUtils.supportsAccounts()) await UTXOsStore.listAccounts(); await BalanceStore.getCombinedBalance(); @@ -469,6 +483,13 @@ export default class Wallet extends React.Component { } else { try { await NodeInfoStore.getNodeInfo(); + + const didMigrate = + await MigrationUtils.migrateLightningAddressSettings(); + if (didMigrate) { + await SettingsStore.getSettings(); + } + if (BackendUtils.supportsAccounts()) { UTXOsStore.listAccounts(); } @@ -482,23 +503,30 @@ export default class Wallet extends React.Component { } } + if (BackendUtils.supportsCustomPreimages() && !NodeInfoStore.testnet) { + this.handlePubkeySpecificLightningAddressSettings(); + } + if ( - lightningAddress.enabled && + SettingsStore.settings.lightningAddressByPubkey?.[ + NodeInfoStore.nodeInfo.identity_pubkey + ]?.enabled && BackendUtils.supportsCustomPreimages() && !NodeInfoStore.testnet ) { if (connecting) { try { await LightningAddressStore.status(); - - if (lightningAddress.automaticallyAccept) { + if ( + SettingsStore.settings.lightningAddressGlobal + .automaticallyAccept + ) { LightningAddressStore.prepareToAutomaticallyAccept(); } - if ( // TODO add enum - SettingsStore.settings.lightningAddress - ?.notifications === 1 + SettingsStore.settings.lightningAddressGlobal + .notifications === 1 ) { LightningAddressStore.updatePushCredentials(); } @@ -549,6 +577,27 @@ export default class Wallet extends React.Component { } }; + private handlePubkeySpecificLightningAddressSettings = async () => { + const { LightningAddressStore, NodeInfoStore, SettingsStore } = + this.props; + const currentPubkey = NodeInfoStore.nodeInfo.identity_pubkey; + + if (!SettingsStore.settings.lightningAddressByPubkey?.[currentPubkey]) { + const hasLightningAddress = + await LightningAddressStore.checkLightningAddressExists(); + await SettingsStore.updateSettings({ + lightningAddressByPubkey: { + ...SettingsStore.settings.lightningAddressByPubkey, + [currentPubkey]: { + enabled: hasLightningAddress === true, + nostrPrivateKey: '' + } + } + }); + SettingsStore.getSettings(); + } + }; + render() { const Tab = createBottomTabNavigator(); const { diff --git a/yarn.lock b/yarn.lock index a5de4fb479..ed2dd76eed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10490,6 +10490,11 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== +uuid@9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== + uuid@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b"