Release v1.0.9 - Add wallet tab with Cashu and Lightning support
- Add wallet tab with NWC (Nostr Wallet Connect) Lightning support - Add Cashu ecash wallet with mint management, send/receive tokens - Add Cashu deposit feature (mint via Lightning invoice) - Add token viewer showing proof amounts and timestamps - Add refresh button with auto-refresh for spent proof detection - Add browser sync warning for Cashu users on welcome screen - Add Cashu onboarding info panel with storage considerations - Add settings page sync info note explaining how to change sync - Add backups page for vault snapshot management - Add About section to identity (You) page - Fix lint accessibility issues in wallet component Files modified: - projects/common/src/lib/services/nwc/* (new) - projects/common/src/lib/services/cashu/* (new) - projects/common/src/lib/services/storage/* (extended) - projects/chrome/src/app/components/home/wallet/* - projects/firefox/src/app/components/home/wallet/* - projects/chrome/src/app/components/welcome/* - projects/firefox/src/app/components/welcome/* - projects/chrome/src/app/components/home/settings/* - projects/firefox/src/app/components/home/settings/* - projects/chrome/src/app/components/home/identity/* - projects/firefox/src/app/components/home/identity/* - projects/chrome/src/app/components/home/backups/* (new) - projects/firefox/src/app/components/home/backups/* (new) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import {
|
||||
BrowserSyncData,
|
||||
CashuMint_ENCRYPTED,
|
||||
Identity_ENCRYPTED,
|
||||
NwcConnection_ENCRYPTED,
|
||||
Permission_ENCRYPTED,
|
||||
Relay_ENCRYPTED,
|
||||
} from './types';
|
||||
@@ -104,6 +106,38 @@ export abstract class BrowserSyncHandler {
|
||||
this.#browserSyncData.relays = Array.from(data.relays);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the NWC connections to the sync data storage.
|
||||
*
|
||||
* ATTENTION: In your implementation, make sure to call "setPartialData_NwcConnections(..)" at the end to update the in-memory data.
|
||||
*/
|
||||
abstract saveAndSetPartialData_NwcConnections(data: {
|
||||
nwcConnections: NwcConnection_ENCRYPTED[];
|
||||
}): Promise<void>;
|
||||
setPartialData_NwcConnections(data: {
|
||||
nwcConnections: NwcConnection_ENCRYPTED[];
|
||||
}) {
|
||||
if (!this.#browserSyncData) {
|
||||
return;
|
||||
}
|
||||
this.#browserSyncData.nwcConnections = Array.from(data.nwcConnections);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the Cashu mints to the sync data storage.
|
||||
*
|
||||
* ATTENTION: In your implementation, make sure to call "setPartialData_CashuMints(..)" at the end to update the in-memory data.
|
||||
*/
|
||||
abstract saveAndSetPartialData_CashuMints(data: {
|
||||
cashuMints: CashuMint_ENCRYPTED[];
|
||||
}): Promise<void>;
|
||||
setPartialData_CashuMints(data: { cashuMints: CashuMint_ENCRYPTED[] }) {
|
||||
if (!this.#browserSyncData) {
|
||||
return;
|
||||
}
|
||||
this.#browserSyncData.cashuMints = Array.from(data.cashuMints);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all data from the sync data storage.
|
||||
*/
|
||||
|
||||
361
projects/common/src/lib/services/storage/related/cashu.ts
Normal file
361
projects/common/src/lib/services/storage/related/cashu.ts
Normal file
@@ -0,0 +1,361 @@
|
||||
import {
|
||||
CryptoHelper,
|
||||
CashuMint_DECRYPTED,
|
||||
CashuMint_ENCRYPTED,
|
||||
CashuProof,
|
||||
StorageService,
|
||||
} from '@common';
|
||||
import { LockedVaultContext } from './identity';
|
||||
|
||||
/**
|
||||
* Validate a Cashu mint URL
|
||||
*/
|
||||
export function isValidMintUrl(url: string): boolean {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
return parsed.protocol === 'https:' || parsed.protocol === 'http:';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export const addCashuMint = async function (
|
||||
this: StorageService,
|
||||
data: {
|
||||
name: string;
|
||||
mintUrl: string;
|
||||
unit?: string;
|
||||
}
|
||||
): Promise<CashuMint_DECRYPTED> {
|
||||
this.assureIsInitialized();
|
||||
|
||||
// Validate the mint URL
|
||||
if (!isValidMintUrl(data.mintUrl)) {
|
||||
throw new Error('Invalid mint URL format');
|
||||
}
|
||||
|
||||
// Normalize URL (remove trailing slash)
|
||||
const normalizedUrl = data.mintUrl.replace(/\/$/, '');
|
||||
|
||||
// Check if a mint with the same URL already exists
|
||||
const existingMint = (
|
||||
this.getBrowserSessionHandler().browserSessionData?.cashuMints ?? []
|
||||
).find((x) => x.mintUrl === normalizedUrl);
|
||||
if (existingMint) {
|
||||
throw new Error(
|
||||
`A connection to this mint already exists: ${existingMint.name}`
|
||||
);
|
||||
}
|
||||
|
||||
const browserSessionData = this.getBrowserSessionHandler().browserSessionData;
|
||||
if (!browserSessionData) {
|
||||
throw new Error('Browser session data is undefined.');
|
||||
}
|
||||
|
||||
const decryptedMint: CashuMint_DECRYPTED = {
|
||||
id: CryptoHelper.v4(),
|
||||
name: data.name,
|
||||
mintUrl: normalizedUrl,
|
||||
unit: data.unit ?? 'sat',
|
||||
createdAt: new Date().toISOString(),
|
||||
proofs: [], // Start with no proofs
|
||||
cachedBalance: 0,
|
||||
cachedBalanceAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Initialize array if needed
|
||||
if (!browserSessionData.cashuMints) {
|
||||
browserSessionData.cashuMints = [];
|
||||
}
|
||||
|
||||
// Add the new mint to the session data
|
||||
browserSessionData.cashuMints.push(decryptedMint);
|
||||
this.getBrowserSessionHandler().saveFullData(browserSessionData);
|
||||
|
||||
// Encrypt the new mint and add it to the sync data
|
||||
const encryptedMint = await encryptCashuMint.call(this, decryptedMint);
|
||||
const encryptedMints = [
|
||||
...(this.getBrowserSyncHandler().browserSyncData?.cashuMints ?? []),
|
||||
encryptedMint,
|
||||
];
|
||||
|
||||
await this.getBrowserSyncHandler().saveAndSetPartialData_CashuMints({
|
||||
cashuMints: encryptedMints,
|
||||
});
|
||||
|
||||
return decryptedMint;
|
||||
};
|
||||
|
||||
export const deleteCashuMint = async function (
|
||||
this: StorageService,
|
||||
mintId: string
|
||||
): Promise<void> {
|
||||
this.assureIsInitialized();
|
||||
|
||||
if (!mintId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const browserSessionData = this.getBrowserSessionHandler().browserSessionData;
|
||||
const browserSyncData = this.getBrowserSyncHandler().browserSyncData;
|
||||
if (!browserSessionData || !browserSyncData) {
|
||||
throw new Error('Browser session or sync data is undefined.');
|
||||
}
|
||||
|
||||
// Remove from session data
|
||||
browserSessionData.cashuMints = (browserSessionData.cashuMints ?? []).filter(
|
||||
(x) => x.id !== mintId
|
||||
);
|
||||
await this.getBrowserSessionHandler().saveFullData(browserSessionData);
|
||||
|
||||
// Handle Sync data
|
||||
const encryptedMintId = await this.encrypt(mintId);
|
||||
await this.getBrowserSyncHandler().saveAndSetPartialData_CashuMints({
|
||||
cashuMints: (browserSyncData.cashuMints ?? []).filter(
|
||||
(x) => x.id !== encryptedMintId
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the proofs for a Cashu mint
|
||||
* This is called after send/receive operations
|
||||
*/
|
||||
export const updateCashuMintProofs = async function (
|
||||
this: StorageService,
|
||||
mintId: string,
|
||||
proofs: CashuProof[]
|
||||
): Promise<void> {
|
||||
this.assureIsInitialized();
|
||||
|
||||
const browserSessionData = this.getBrowserSessionHandler().browserSessionData;
|
||||
const browserSyncData = this.getBrowserSyncHandler().browserSyncData;
|
||||
if (!browserSessionData || !browserSyncData) {
|
||||
throw new Error('Browser session or sync data is undefined.');
|
||||
}
|
||||
|
||||
const sessionMint = (browserSessionData.cashuMints ?? []).find(
|
||||
(x) => x.id === mintId
|
||||
);
|
||||
const encryptedMintId = await this.encrypt(mintId);
|
||||
const syncMint = (browserSyncData.cashuMints ?? []).find(
|
||||
(x) => x.id === encryptedMintId
|
||||
);
|
||||
|
||||
if (!sessionMint || !syncMint) {
|
||||
throw new Error('Cashu mint not found for proofs update.');
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
// Calculate balance from proofs (sum of all proof amounts in satoshis)
|
||||
const balance = proofs.reduce((sum, p) => sum + p.amount, 0);
|
||||
|
||||
// Update session data
|
||||
sessionMint.proofs = proofs;
|
||||
sessionMint.cachedBalance = balance;
|
||||
sessionMint.cachedBalanceAt = now;
|
||||
await this.getBrowserSessionHandler().saveFullData(browserSessionData);
|
||||
|
||||
// Update sync data
|
||||
syncMint.proofs = await this.encrypt(JSON.stringify(proofs));
|
||||
syncMint.cachedBalance = await this.encrypt(balance.toString());
|
||||
syncMint.cachedBalanceAt = await this.encrypt(now);
|
||||
await this.getBrowserSyncHandler().saveAndSetPartialData_CashuMints({
|
||||
cashuMints: browserSyncData.cashuMints ?? [],
|
||||
});
|
||||
};
|
||||
|
||||
export const encryptCashuMint = async function (
|
||||
this: StorageService,
|
||||
mint: CashuMint_DECRYPTED
|
||||
): Promise<CashuMint_ENCRYPTED> {
|
||||
const encrypted: CashuMint_ENCRYPTED = {
|
||||
id: await this.encrypt(mint.id),
|
||||
name: await this.encrypt(mint.name),
|
||||
mintUrl: await this.encrypt(mint.mintUrl),
|
||||
unit: await this.encrypt(mint.unit),
|
||||
createdAt: await this.encrypt(mint.createdAt),
|
||||
proofs: await this.encrypt(JSON.stringify(mint.proofs)),
|
||||
};
|
||||
|
||||
if (mint.cachedBalance !== undefined) {
|
||||
encrypted.cachedBalance = await this.encrypt(mint.cachedBalance.toString());
|
||||
}
|
||||
if (mint.cachedBalanceAt) {
|
||||
encrypted.cachedBalanceAt = await this.encrypt(mint.cachedBalanceAt);
|
||||
}
|
||||
|
||||
return encrypted;
|
||||
};
|
||||
|
||||
export const decryptCashuMint = async function (
|
||||
this: StorageService,
|
||||
mint: CashuMint_ENCRYPTED,
|
||||
withLockedVault: LockedVaultContext | undefined = undefined
|
||||
): Promise<CashuMint_DECRYPTED> {
|
||||
if (typeof withLockedVault === 'undefined') {
|
||||
// Normal decryption with unlocked vault
|
||||
const proofsJson = await this.decrypt(mint.proofs, 'string');
|
||||
const decrypted: CashuMint_DECRYPTED = {
|
||||
id: await this.decrypt(mint.id, 'string'),
|
||||
name: await this.decrypt(mint.name, 'string'),
|
||||
mintUrl: await this.decrypt(mint.mintUrl, 'string'),
|
||||
unit: await this.decrypt(mint.unit, 'string'),
|
||||
createdAt: await this.decrypt(mint.createdAt, 'string'),
|
||||
proofs: JSON.parse(proofsJson) as CashuProof[],
|
||||
};
|
||||
|
||||
if (mint.cachedBalance) {
|
||||
decrypted.cachedBalance = await this.decrypt(mint.cachedBalance, 'number');
|
||||
}
|
||||
if (mint.cachedBalanceAt) {
|
||||
decrypted.cachedBalanceAt = await this.decrypt(
|
||||
mint.cachedBalanceAt,
|
||||
'string'
|
||||
);
|
||||
}
|
||||
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
// v2: Use pre-derived key
|
||||
if (withLockedVault.keyBase64) {
|
||||
const proofsJson = await this.decryptWithLockedVaultV2(
|
||||
mint.proofs,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.keyBase64
|
||||
);
|
||||
const decrypted: CashuMint_DECRYPTED = {
|
||||
id: await this.decryptWithLockedVaultV2(
|
||||
mint.id,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.keyBase64
|
||||
),
|
||||
name: await this.decryptWithLockedVaultV2(
|
||||
mint.name,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.keyBase64
|
||||
),
|
||||
mintUrl: await this.decryptWithLockedVaultV2(
|
||||
mint.mintUrl,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.keyBase64
|
||||
),
|
||||
unit: await this.decryptWithLockedVaultV2(
|
||||
mint.unit,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.keyBase64
|
||||
),
|
||||
createdAt: await this.decryptWithLockedVaultV2(
|
||||
mint.createdAt,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.keyBase64
|
||||
),
|
||||
proofs: JSON.parse(proofsJson) as CashuProof[],
|
||||
};
|
||||
|
||||
if (mint.cachedBalance) {
|
||||
decrypted.cachedBalance = await this.decryptWithLockedVaultV2(
|
||||
mint.cachedBalance,
|
||||
'number',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.keyBase64
|
||||
);
|
||||
}
|
||||
if (mint.cachedBalanceAt) {
|
||||
decrypted.cachedBalanceAt = await this.decryptWithLockedVaultV2(
|
||||
mint.cachedBalanceAt,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.keyBase64
|
||||
);
|
||||
}
|
||||
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
// v1: Use password (PBKDF2)
|
||||
const proofsJson = await this.decryptWithLockedVault(
|
||||
mint.proofs,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password!
|
||||
);
|
||||
const decrypted: CashuMint_DECRYPTED = {
|
||||
id: await this.decryptWithLockedVault(
|
||||
mint.id,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password!
|
||||
),
|
||||
name: await this.decryptWithLockedVault(
|
||||
mint.name,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password!
|
||||
),
|
||||
mintUrl: await this.decryptWithLockedVault(
|
||||
mint.mintUrl,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password!
|
||||
),
|
||||
unit: await this.decryptWithLockedVault(
|
||||
mint.unit,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password!
|
||||
),
|
||||
createdAt: await this.decryptWithLockedVault(
|
||||
mint.createdAt,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password!
|
||||
),
|
||||
proofs: JSON.parse(proofsJson) as CashuProof[],
|
||||
};
|
||||
|
||||
if (mint.cachedBalance) {
|
||||
decrypted.cachedBalance = await this.decryptWithLockedVault(
|
||||
mint.cachedBalance,
|
||||
'number',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password!
|
||||
);
|
||||
}
|
||||
if (mint.cachedBalanceAt) {
|
||||
decrypted.cachedBalanceAt = await this.decryptWithLockedVault(
|
||||
mint.cachedBalanceAt,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password!
|
||||
);
|
||||
}
|
||||
|
||||
return decrypted;
|
||||
};
|
||||
|
||||
export const decryptCashuMints = async function (
|
||||
this: StorageService,
|
||||
mints: CashuMint_ENCRYPTED[],
|
||||
withLockedVault: LockedVaultContext | undefined = undefined
|
||||
): Promise<CashuMint_DECRYPTED[]> {
|
||||
const decryptedMints: CashuMint_DECRYPTED[] = [];
|
||||
|
||||
for (const mint of mints) {
|
||||
const decryptedMint = await decryptCashuMint.call(
|
||||
this,
|
||||
mint,
|
||||
withLockedVault
|
||||
);
|
||||
decryptedMints.push(decryptedMint);
|
||||
}
|
||||
|
||||
return decryptedMints;
|
||||
};
|
||||
419
projects/common/src/lib/services/storage/related/nwc.ts
Normal file
419
projects/common/src/lib/services/storage/related/nwc.ts
Normal file
@@ -0,0 +1,419 @@
|
||||
import {
|
||||
CryptoHelper,
|
||||
NwcConnection_DECRYPTED,
|
||||
NwcConnection_ENCRYPTED,
|
||||
StorageService,
|
||||
} from '@common';
|
||||
import { LockedVaultContext } from './identity';
|
||||
|
||||
/**
|
||||
* Parse a nostr+walletconnect:// URL into its components
|
||||
*/
|
||||
export function parseNwcUrl(url: string): {
|
||||
walletPubkey: string;
|
||||
relayUrl: string;
|
||||
secret: string;
|
||||
lud16?: string;
|
||||
} | null {
|
||||
try {
|
||||
// Format: nostr+walletconnect://<pubkey>?relay=<url>&secret=<hex>&lud16=<optional>
|
||||
const match = url.match(/^nostr\+walletconnect:\/\/([a-f0-9]{64})\?(.+)$/i);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const walletPubkey = match[1].toLowerCase();
|
||||
const params = new URLSearchParams(match[2]);
|
||||
|
||||
const relayUrl = params.get('relay');
|
||||
const secret = params.get('secret');
|
||||
const lud16 = params.get('lud16') || undefined;
|
||||
|
||||
if (!relayUrl || !secret) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validate secret is 64-char hex
|
||||
if (!/^[a-f0-9]{64}$/i.test(secret)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
walletPubkey,
|
||||
relayUrl: decodeURIComponent(relayUrl),
|
||||
secret: secret.toLowerCase(),
|
||||
lud16,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const addNwcConnection = async function (
|
||||
this: StorageService,
|
||||
data: {
|
||||
name: string;
|
||||
connectionUrl: string;
|
||||
}
|
||||
): Promise<void> {
|
||||
this.assureIsInitialized();
|
||||
|
||||
// Parse the NWC URL
|
||||
const parsed = parseNwcUrl(data.connectionUrl);
|
||||
if (!parsed) {
|
||||
throw new Error('Invalid NWC URL format');
|
||||
}
|
||||
|
||||
// Check if a connection with the same wallet pubkey already exists
|
||||
const existingConnection = (
|
||||
this.getBrowserSessionHandler().browserSessionData?.nwcConnections ?? []
|
||||
).find((x) => x.walletPubkey === parsed.walletPubkey);
|
||||
if (existingConnection) {
|
||||
throw new Error(
|
||||
`A connection to this wallet already exists: ${existingConnection.name}`
|
||||
);
|
||||
}
|
||||
|
||||
const browserSessionData = this.getBrowserSessionHandler().browserSessionData;
|
||||
if (!browserSessionData) {
|
||||
throw new Error('Browser session data is undefined.');
|
||||
}
|
||||
|
||||
const decryptedConnection: NwcConnection_DECRYPTED = {
|
||||
id: CryptoHelper.v4(),
|
||||
name: data.name,
|
||||
connectionUrl: data.connectionUrl,
|
||||
walletPubkey: parsed.walletPubkey,
|
||||
relayUrl: parsed.relayUrl,
|
||||
secret: parsed.secret,
|
||||
lud16: parsed.lud16,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Initialize array if needed
|
||||
if (!browserSessionData.nwcConnections) {
|
||||
browserSessionData.nwcConnections = [];
|
||||
}
|
||||
|
||||
// Add the new connection to the session data
|
||||
browserSessionData.nwcConnections.push(decryptedConnection);
|
||||
this.getBrowserSessionHandler().saveFullData(browserSessionData);
|
||||
|
||||
// Encrypt the new connection and add it to the sync data
|
||||
const encryptedConnection = await encryptNwcConnection.call(
|
||||
this,
|
||||
decryptedConnection
|
||||
);
|
||||
const encryptedConnections = [
|
||||
...(this.getBrowserSyncHandler().browserSyncData?.nwcConnections ?? []),
|
||||
encryptedConnection,
|
||||
];
|
||||
|
||||
await this.getBrowserSyncHandler().saveAndSetPartialData_NwcConnections({
|
||||
nwcConnections: encryptedConnections,
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteNwcConnection = async function (
|
||||
this: StorageService,
|
||||
connectionId: string
|
||||
): Promise<void> {
|
||||
this.assureIsInitialized();
|
||||
|
||||
if (!connectionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const browserSessionData = this.getBrowserSessionHandler().browserSessionData;
|
||||
const browserSyncData = this.getBrowserSyncHandler().browserSyncData;
|
||||
if (!browserSessionData || !browserSyncData) {
|
||||
throw new Error('Browser session or sync data is undefined.');
|
||||
}
|
||||
|
||||
// Remove from session data
|
||||
browserSessionData.nwcConnections = (
|
||||
browserSessionData.nwcConnections ?? []
|
||||
).filter((x) => x.id !== connectionId);
|
||||
await this.getBrowserSessionHandler().saveFullData(browserSessionData);
|
||||
|
||||
// Handle Sync data
|
||||
const encryptedConnectionId = await this.encrypt(connectionId);
|
||||
await this.getBrowserSyncHandler().saveAndSetPartialData_NwcConnections({
|
||||
nwcConnections: (browserSyncData.nwcConnections ?? []).filter(
|
||||
(x) => x.id !== encryptedConnectionId
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
export const updateNwcConnectionBalance = async function (
|
||||
this: StorageService,
|
||||
connectionId: string,
|
||||
balanceMillisats: number
|
||||
): Promise<void> {
|
||||
this.assureIsInitialized();
|
||||
|
||||
const browserSessionData = this.getBrowserSessionHandler().browserSessionData;
|
||||
const browserSyncData = this.getBrowserSyncHandler().browserSyncData;
|
||||
if (!browserSessionData || !browserSyncData) {
|
||||
throw new Error('Browser session or sync data is undefined.');
|
||||
}
|
||||
|
||||
const sessionConnection = (browserSessionData.nwcConnections ?? []).find(
|
||||
(x) => x.id === connectionId
|
||||
);
|
||||
const encryptedConnectionId = await this.encrypt(connectionId);
|
||||
const syncConnection = (browserSyncData.nwcConnections ?? []).find(
|
||||
(x) => x.id === encryptedConnectionId
|
||||
);
|
||||
|
||||
if (!sessionConnection || !syncConnection) {
|
||||
throw new Error('NWC connection not found for balance update.');
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
// Update session data
|
||||
sessionConnection.cachedBalance = balanceMillisats;
|
||||
sessionConnection.cachedBalanceAt = now;
|
||||
await this.getBrowserSessionHandler().saveFullData(browserSessionData);
|
||||
|
||||
// Update sync data
|
||||
syncConnection.cachedBalance = await this.encrypt(balanceMillisats.toString());
|
||||
syncConnection.cachedBalanceAt = await this.encrypt(now);
|
||||
await this.getBrowserSyncHandler().saveAndSetPartialData_NwcConnections({
|
||||
nwcConnections: browserSyncData.nwcConnections ?? [],
|
||||
});
|
||||
};
|
||||
|
||||
export const encryptNwcConnection = async function (
|
||||
this: StorageService,
|
||||
connection: NwcConnection_DECRYPTED
|
||||
): Promise<NwcConnection_ENCRYPTED> {
|
||||
const encrypted: NwcConnection_ENCRYPTED = {
|
||||
id: await this.encrypt(connection.id),
|
||||
name: await this.encrypt(connection.name),
|
||||
connectionUrl: await this.encrypt(connection.connectionUrl),
|
||||
walletPubkey: await this.encrypt(connection.walletPubkey),
|
||||
relayUrl: await this.encrypt(connection.relayUrl),
|
||||
secret: await this.encrypt(connection.secret),
|
||||
createdAt: await this.encrypt(connection.createdAt),
|
||||
};
|
||||
|
||||
if (connection.lud16) {
|
||||
encrypted.lud16 = await this.encrypt(connection.lud16);
|
||||
}
|
||||
if (connection.cachedBalance !== undefined) {
|
||||
encrypted.cachedBalance = await this.encrypt(
|
||||
connection.cachedBalance.toString()
|
||||
);
|
||||
}
|
||||
if (connection.cachedBalanceAt) {
|
||||
encrypted.cachedBalanceAt = await this.encrypt(connection.cachedBalanceAt);
|
||||
}
|
||||
|
||||
return encrypted;
|
||||
};
|
||||
|
||||
export const decryptNwcConnection = async function (
|
||||
this: StorageService,
|
||||
connection: NwcConnection_ENCRYPTED,
|
||||
withLockedVault: LockedVaultContext | undefined = undefined
|
||||
): Promise<NwcConnection_DECRYPTED> {
|
||||
if (typeof withLockedVault === 'undefined') {
|
||||
// Normal decryption with unlocked vault
|
||||
const decrypted: NwcConnection_DECRYPTED = {
|
||||
id: await this.decrypt(connection.id, 'string'),
|
||||
name: await this.decrypt(connection.name, 'string'),
|
||||
connectionUrl: await this.decrypt(connection.connectionUrl, 'string'),
|
||||
walletPubkey: await this.decrypt(connection.walletPubkey, 'string'),
|
||||
relayUrl: await this.decrypt(connection.relayUrl, 'string'),
|
||||
secret: await this.decrypt(connection.secret, 'string'),
|
||||
createdAt: await this.decrypt(connection.createdAt, 'string'),
|
||||
};
|
||||
|
||||
if (connection.lud16) {
|
||||
decrypted.lud16 = await this.decrypt(connection.lud16, 'string');
|
||||
}
|
||||
if (connection.cachedBalance) {
|
||||
decrypted.cachedBalance = await this.decrypt(
|
||||
connection.cachedBalance,
|
||||
'number'
|
||||
);
|
||||
}
|
||||
if (connection.cachedBalanceAt) {
|
||||
decrypted.cachedBalanceAt = await this.decrypt(
|
||||
connection.cachedBalanceAt,
|
||||
'string'
|
||||
);
|
||||
}
|
||||
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
// v2: Use pre-derived key
|
||||
if (withLockedVault.keyBase64) {
|
||||
const decrypted: NwcConnection_DECRYPTED = {
|
||||
id: await this.decryptWithLockedVaultV2(
|
||||
connection.id,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.keyBase64
|
||||
),
|
||||
name: await this.decryptWithLockedVaultV2(
|
||||
connection.name,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.keyBase64
|
||||
),
|
||||
connectionUrl: await this.decryptWithLockedVaultV2(
|
||||
connection.connectionUrl,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.keyBase64
|
||||
),
|
||||
walletPubkey: await this.decryptWithLockedVaultV2(
|
||||
connection.walletPubkey,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.keyBase64
|
||||
),
|
||||
relayUrl: await this.decryptWithLockedVaultV2(
|
||||
connection.relayUrl,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.keyBase64
|
||||
),
|
||||
secret: await this.decryptWithLockedVaultV2(
|
||||
connection.secret,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.keyBase64
|
||||
),
|
||||
createdAt: await this.decryptWithLockedVaultV2(
|
||||
connection.createdAt,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.keyBase64
|
||||
),
|
||||
};
|
||||
|
||||
if (connection.lud16) {
|
||||
decrypted.lud16 = await this.decryptWithLockedVaultV2(
|
||||
connection.lud16,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.keyBase64
|
||||
);
|
||||
}
|
||||
if (connection.cachedBalance) {
|
||||
decrypted.cachedBalance = await this.decryptWithLockedVaultV2(
|
||||
connection.cachedBalance,
|
||||
'number',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.keyBase64
|
||||
);
|
||||
}
|
||||
if (connection.cachedBalanceAt) {
|
||||
decrypted.cachedBalanceAt = await this.decryptWithLockedVaultV2(
|
||||
connection.cachedBalanceAt,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.keyBase64
|
||||
);
|
||||
}
|
||||
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
// v1: Use password (PBKDF2)
|
||||
const decrypted: NwcConnection_DECRYPTED = {
|
||||
id: await this.decryptWithLockedVault(
|
||||
connection.id,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password!
|
||||
),
|
||||
name: await this.decryptWithLockedVault(
|
||||
connection.name,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password!
|
||||
),
|
||||
connectionUrl: await this.decryptWithLockedVault(
|
||||
connection.connectionUrl,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password!
|
||||
),
|
||||
walletPubkey: await this.decryptWithLockedVault(
|
||||
connection.walletPubkey,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password!
|
||||
),
|
||||
relayUrl: await this.decryptWithLockedVault(
|
||||
connection.relayUrl,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password!
|
||||
),
|
||||
secret: await this.decryptWithLockedVault(
|
||||
connection.secret,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password!
|
||||
),
|
||||
createdAt: await this.decryptWithLockedVault(
|
||||
connection.createdAt,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password!
|
||||
),
|
||||
};
|
||||
|
||||
if (connection.lud16) {
|
||||
decrypted.lud16 = await this.decryptWithLockedVault(
|
||||
connection.lud16,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password!
|
||||
);
|
||||
}
|
||||
if (connection.cachedBalance) {
|
||||
decrypted.cachedBalance = await this.decryptWithLockedVault(
|
||||
connection.cachedBalance,
|
||||
'number',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password!
|
||||
);
|
||||
}
|
||||
if (connection.cachedBalanceAt) {
|
||||
decrypted.cachedBalanceAt = await this.decryptWithLockedVault(
|
||||
connection.cachedBalanceAt,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password!
|
||||
);
|
||||
}
|
||||
|
||||
return decrypted;
|
||||
};
|
||||
|
||||
export const decryptNwcConnections = async function (
|
||||
this: StorageService,
|
||||
connections: NwcConnection_ENCRYPTED[],
|
||||
withLockedVault: LockedVaultContext | undefined = undefined
|
||||
): Promise<NwcConnection_DECRYPTED[]> {
|
||||
const decryptedConnections: NwcConnection_DECRYPTED[] = [];
|
||||
|
||||
for (const connection of connections) {
|
||||
const decryptedConnection = await decryptNwcConnection.call(
|
||||
this,
|
||||
connection,
|
||||
withLockedVault
|
||||
);
|
||||
decryptedConnections.push(decryptedConnection);
|
||||
}
|
||||
|
||||
return decryptedConnections;
|
||||
};
|
||||
@@ -8,7 +8,9 @@ import {
|
||||
deriveKeyArgon2,
|
||||
} from '@common';
|
||||
import { Buffer } from 'buffer';
|
||||
import { decryptCashuMints, encryptCashuMint } from './cashu';
|
||||
import { decryptIdentities, encryptIdentity, LockedVaultContext } from './identity';
|
||||
import { decryptNwcConnections, encryptNwcConnection } from './nwc';
|
||||
import { decryptPermissions } from './permission';
|
||||
import { decryptRelays, encryptRelay } from './relay';
|
||||
|
||||
@@ -34,6 +36,8 @@ export const createNewVault = async function (
|
||||
identities: [],
|
||||
permissions: [],
|
||||
relays: [],
|
||||
nwcConnections: [],
|
||||
cashuMints: [],
|
||||
selectedIdentityId: null,
|
||||
};
|
||||
await this.getBrowserSessionHandler().saveFullData(sessionData);
|
||||
@@ -47,6 +51,8 @@ export const createNewVault = async function (
|
||||
identities: [],
|
||||
permissions: [],
|
||||
relays: [],
|
||||
nwcConnections: [],
|
||||
cashuMints: [],
|
||||
selectedIdentityId: null,
|
||||
};
|
||||
await this.getBrowserSyncHandler().saveAndSetFullData(syncData);
|
||||
@@ -133,6 +139,22 @@ export const unlockVault = async function (
|
||||
);
|
||||
console.log('[vault] Decrypted', decryptedRelays.length, 'relays');
|
||||
|
||||
console.log('[vault] Decrypting NWC connections...');
|
||||
const decryptedNwcConnections = await decryptNwcConnections.call(
|
||||
this,
|
||||
browserSyncData.nwcConnections ?? [],
|
||||
withLockedVault
|
||||
);
|
||||
console.log('[vault] Decrypted', decryptedNwcConnections.length, 'NWC connections');
|
||||
|
||||
console.log('[vault] Decrypting Cashu mints...');
|
||||
const decryptedCashuMints = await decryptCashuMints.call(
|
||||
this,
|
||||
browserSyncData.cashuMints ?? [],
|
||||
withLockedVault
|
||||
);
|
||||
console.log('[vault] Decrypted', decryptedCashuMints.length, 'Cashu mints');
|
||||
|
||||
console.log('[vault] Decrypting selectedIdentityId...');
|
||||
let decryptedSelectedIdentityId: string | null = null;
|
||||
if (browserSyncData.selectedIdentityId !== null) {
|
||||
@@ -163,6 +185,8 @@ export const unlockVault = async function (
|
||||
identities: decryptedIdentities,
|
||||
selectedIdentityId: decryptedSelectedIdentityId,
|
||||
relays: decryptedRelays,
|
||||
nwcConnections: decryptedNwcConnections,
|
||||
cashuMints: decryptedCashuMints,
|
||||
};
|
||||
|
||||
console.log('[vault] Saving session data...');
|
||||
@@ -234,6 +258,20 @@ async function migrateVaultV1ToV2(
|
||||
encryptedPermissions.push(encryptedPermission);
|
||||
}
|
||||
|
||||
// Re-encrypt NWC connections
|
||||
const encryptedNwcConnections = [];
|
||||
for (const nwcConnection of browserSessionData.nwcConnections ?? []) {
|
||||
const encrypted = await encryptNwcConnection.call(this, nwcConnection);
|
||||
encryptedNwcConnections.push(encrypted);
|
||||
}
|
||||
|
||||
// Re-encrypt Cashu mints
|
||||
const encryptedCashuMints = [];
|
||||
for (const cashuMint of browserSessionData.cashuMints ?? []) {
|
||||
const encrypted = await encryptCashuMint.call(this, cashuMint);
|
||||
encryptedCashuMints.push(encrypted);
|
||||
}
|
||||
|
||||
const encryptedSelectedIdentityId = browserSessionData.selectedIdentityId
|
||||
? await this.encrypt(browserSessionData.selectedIdentityId)
|
||||
: null;
|
||||
@@ -247,6 +285,8 @@ async function migrateVaultV1ToV2(
|
||||
identities: encryptedIdentities,
|
||||
permissions: encryptedPermissions,
|
||||
relays: encryptedRelays,
|
||||
nwcConnections: encryptedNwcConnections,
|
||||
cashuMints: encryptedCashuMints,
|
||||
selectedIdentityId: encryptedSelectedIdentityId,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Bookmark, BrowserSyncFlow, SignerMetaData } from './types';
|
||||
import { Bookmark, BrowserSyncData, BrowserSyncFlow, SignerMetaData, SignerMetaData_VaultSnapshot } from './types';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export abstract class SignerMetaHandler {
|
||||
get signerMetaData(): SignerMetaData | undefined {
|
||||
@@ -8,7 +9,8 @@ export abstract class SignerMetaHandler {
|
||||
|
||||
#signerMetaData?: SignerMetaData;
|
||||
|
||||
readonly metaProperties = ['syncFlow', 'vaultSnapshots', 'recklessMode', 'whitelistedHosts', 'bookmarks'];
|
||||
readonly metaProperties = ['syncFlow', 'vaultSnapshots', 'maxBackups', 'recklessMode', 'whitelistedHosts', 'bookmarks'];
|
||||
readonly DEFAULT_MAX_BACKUPS = 5;
|
||||
/**
|
||||
* Load the full data from the storage. If the storage is used for storing
|
||||
* other data (e.g. browser sync data when the user decided to NOT sync),
|
||||
@@ -111,4 +113,120 @@ export abstract class SignerMetaHandler {
|
||||
getBookmarks(): Bookmark[] {
|
||||
return this.#signerMetaData?.bookmarks ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum number of backups to keep.
|
||||
*/
|
||||
getMaxBackups(): number {
|
||||
return this.#signerMetaData?.maxBackups ?? this.DEFAULT_MAX_BACKUPS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum number of backups to keep and immediately saves it.
|
||||
*/
|
||||
async setMaxBackups(count: number): Promise<void> {
|
||||
const clampedCount = Math.max(1, Math.min(20, count)); // Clamp between 1-20
|
||||
if (!this.#signerMetaData) {
|
||||
this.#signerMetaData = {
|
||||
maxBackups: clampedCount,
|
||||
};
|
||||
} else {
|
||||
this.#signerMetaData.maxBackups = clampedCount;
|
||||
}
|
||||
|
||||
await this.saveFullData(this.#signerMetaData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all vault backups, sorted newest first.
|
||||
*/
|
||||
getBackups(): SignerMetaData_VaultSnapshot[] {
|
||||
const backups = this.#signerMetaData?.vaultSnapshots ?? [];
|
||||
return [...backups].sort((a, b) =>
|
||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a specific backup by ID.
|
||||
*/
|
||||
getBackupById(id: string): SignerMetaData_VaultSnapshot | undefined {
|
||||
return this.#signerMetaData?.vaultSnapshots?.find(b => b.id === id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new backup of the vault data.
|
||||
* Automatically removes old backups if exceeding maxBackups.
|
||||
*/
|
||||
async createBackup(
|
||||
browserSyncData: BrowserSyncData,
|
||||
reason: 'manual' | 'auto' | 'pre-restore' = 'manual'
|
||||
): Promise<SignerMetaData_VaultSnapshot> {
|
||||
const now = new Date();
|
||||
const dateTimeString = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
||||
const identityCount = browserSyncData.identities?.length ?? 0;
|
||||
|
||||
const snapshot: SignerMetaData_VaultSnapshot = {
|
||||
id: uuidv4(),
|
||||
fileName: `Vault Backup - ${dateTimeString}`,
|
||||
createdAt: now.toISOString(),
|
||||
data: JSON.parse(JSON.stringify(browserSyncData)), // Deep clone
|
||||
identityCount,
|
||||
reason,
|
||||
};
|
||||
|
||||
if (!this.#signerMetaData) {
|
||||
this.#signerMetaData = {
|
||||
vaultSnapshots: [snapshot],
|
||||
};
|
||||
} else {
|
||||
const existingBackups = this.#signerMetaData.vaultSnapshots ?? [];
|
||||
existingBackups.push(snapshot);
|
||||
|
||||
// Enforce max backups limit (only for auto backups, keep manual and pre-restore)
|
||||
const maxBackups = this.getMaxBackups();
|
||||
const autoBackups = existingBackups.filter(b => b.reason === 'auto');
|
||||
const otherBackups = existingBackups.filter(b => b.reason !== 'auto');
|
||||
|
||||
// Sort auto backups by date (newest first) and keep only maxBackups
|
||||
autoBackups.sort((a, b) =>
|
||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
);
|
||||
const trimmedAutoBackups = autoBackups.slice(0, maxBackups);
|
||||
|
||||
this.#signerMetaData.vaultSnapshots = [...otherBackups, ...trimmedAutoBackups];
|
||||
}
|
||||
|
||||
await this.saveFullData(this.#signerMetaData);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a backup by ID.
|
||||
*/
|
||||
async deleteBackup(backupId: string): Promise<boolean> {
|
||||
if (!this.#signerMetaData?.vaultSnapshots) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const initialLength = this.#signerMetaData.vaultSnapshots.length;
|
||||
this.#signerMetaData.vaultSnapshots = this.#signerMetaData.vaultSnapshots.filter(
|
||||
b => b.id !== backupId
|
||||
);
|
||||
|
||||
if (this.#signerMetaData.vaultSnapshots.length < initialLength) {
|
||||
await this.saveFullData(this.#signerMetaData);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the data from a backup for restoration.
|
||||
* Note: The caller should create a pre-restore backup before calling this.
|
||||
*/
|
||||
getBackupData(backupId: string): BrowserSyncData | undefined {
|
||||
const backup = this.getBackupById(backupId);
|
||||
return backup?.data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,17 @@ import {
|
||||
import { deletePermission } from './related/permission';
|
||||
import { createNewVault, deleteVault, unlockVault } from './related/vault';
|
||||
import { addRelay, deleteRelay, updateRelay } from './related/relay';
|
||||
import {
|
||||
addNwcConnection,
|
||||
deleteNwcConnection,
|
||||
updateNwcConnectionBalance,
|
||||
} from './related/nwc';
|
||||
import {
|
||||
addCashuMint,
|
||||
deleteCashuMint,
|
||||
updateCashuMintProofs,
|
||||
} from './related/cashu';
|
||||
import { CashuMint_DECRYPTED, CashuProof } from './types';
|
||||
|
||||
export interface StorageServiceConfig {
|
||||
browserSessionHandler: BrowserSessionHandler;
|
||||
@@ -176,6 +187,43 @@ export class StorageService {
|
||||
await updateRelay.call(this, relayClone);
|
||||
}
|
||||
|
||||
async addNwcConnection(data: {
|
||||
name: string;
|
||||
connectionUrl: string;
|
||||
}): Promise<void> {
|
||||
await addNwcConnection.call(this, data);
|
||||
}
|
||||
|
||||
async deleteNwcConnection(connectionId: string): Promise<void> {
|
||||
await deleteNwcConnection.call(this, connectionId);
|
||||
}
|
||||
|
||||
async updateNwcConnectionBalance(
|
||||
connectionId: string,
|
||||
balanceMillisats: number
|
||||
): Promise<void> {
|
||||
await updateNwcConnectionBalance.call(this, connectionId, balanceMillisats);
|
||||
}
|
||||
|
||||
async addCashuMint(data: {
|
||||
name: string;
|
||||
mintUrl: string;
|
||||
unit?: string;
|
||||
}): Promise<CashuMint_DECRYPTED> {
|
||||
return await addCashuMint.call(this, data);
|
||||
}
|
||||
|
||||
async deleteCashuMint(mintId: string): Promise<void> {
|
||||
await deleteCashuMint.call(this, mintId);
|
||||
}
|
||||
|
||||
async updateCashuMintProofs(
|
||||
mintId: string,
|
||||
proofs: CashuProof[]
|
||||
): Promise<void> {
|
||||
await updateCashuMintProofs.call(this, mintId, proofs);
|
||||
}
|
||||
|
||||
exportVault(): string {
|
||||
this.assureIsInitialized();
|
||||
const vaultJson = JSON.stringify(
|
||||
@@ -226,6 +274,17 @@ export class StorageService {
|
||||
return this.#signerMetaHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current browser sync flow setting.
|
||||
* Returns NO_SYNC if not initialized or no setting found.
|
||||
*/
|
||||
getSyncFlow(): BrowserSyncFlow {
|
||||
if (!this.isInitialized || !this.#signerMetaHandler?.signerMetaData) {
|
||||
return BrowserSyncFlow.NO_SYNC;
|
||||
}
|
||||
return this.#signerMetaHandler.signerMetaData.syncFlow ?? BrowserSyncFlow.NO_SYNC;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an exception if the service is not initialized.
|
||||
*/
|
||||
|
||||
@@ -43,6 +43,80 @@ export interface Relay_ENCRYPTED {
|
||||
write: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* NWC (Nostr Wallet Connect) connection - Decrypted
|
||||
* Stores NIP-47 wallet connection data
|
||||
*/
|
||||
export interface NwcConnection_DECRYPTED {
|
||||
id: string;
|
||||
name: string; // User-defined wallet name
|
||||
connectionUrl: string; // Full nostr+walletconnect:// URL
|
||||
walletPubkey: string; // Wallet service pubkey
|
||||
relayUrl: string; // Relay URL for NWC communication
|
||||
secret: string; // Client secret key (32-byte hex)
|
||||
lud16?: string; // Optional lightning address
|
||||
createdAt: string; // ISO timestamp
|
||||
cachedBalance?: number; // Balance in millisatoshis
|
||||
cachedBalanceAt?: string; // ISO timestamp when balance was fetched
|
||||
}
|
||||
|
||||
/**
|
||||
* NWC connection - Encrypted for storage
|
||||
*/
|
||||
export interface NwcConnection_ENCRYPTED {
|
||||
id: string;
|
||||
name: string;
|
||||
connectionUrl: string;
|
||||
walletPubkey: string;
|
||||
relayUrl: string;
|
||||
secret: string;
|
||||
lud16?: string;
|
||||
createdAt: string;
|
||||
cachedBalance?: string; // Encrypted as string
|
||||
cachedBalanceAt?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cashu Proof - represents a single ecash token
|
||||
* This is the actual money stored locally
|
||||
*/
|
||||
export interface CashuProof {
|
||||
id: string; // Keyset ID from mint
|
||||
amount: number; // Satoshi amount
|
||||
secret: string; // Blinded secret
|
||||
C: string; // Unblinded signature (commitment)
|
||||
receivedAt?: string; // ISO timestamp when token was received
|
||||
}
|
||||
|
||||
/**
|
||||
* Cashu Mint Connection - Decrypted
|
||||
* Stores NIP-60 Cashu mint connection data with local proofs
|
||||
*/
|
||||
export interface CashuMint_DECRYPTED {
|
||||
id: string;
|
||||
name: string; // User-defined mint name
|
||||
mintUrl: string; // Mint API URL
|
||||
unit: string; // Unit (default: 'sat')
|
||||
createdAt: string; // ISO timestamp
|
||||
proofs: CashuProof[]; // Unspent proofs for this mint
|
||||
cachedBalance?: number; // Sum of proof amounts (sats)
|
||||
cachedBalanceAt?: string; // When balance was calculated
|
||||
}
|
||||
|
||||
/**
|
||||
* Cashu Mint Connection - Encrypted for storage
|
||||
*/
|
||||
export interface CashuMint_ENCRYPTED {
|
||||
id: string;
|
||||
name: string;
|
||||
mintUrl: string;
|
||||
unit: string;
|
||||
createdAt: string;
|
||||
proofs: string; // JSON stringified and encrypted
|
||||
cachedBalance?: string;
|
||||
cachedBalanceAt?: string;
|
||||
}
|
||||
|
||||
export interface BrowserSyncData_PART_Unencrypted {
|
||||
version: number;
|
||||
iv: string;
|
||||
@@ -57,6 +131,8 @@ export interface BrowserSyncData_PART_Encrypted {
|
||||
permissions: Permission_ENCRYPTED[];
|
||||
identities: Identity_ENCRYPTED[];
|
||||
relays: Relay_ENCRYPTED[];
|
||||
nwcConnections?: NwcConnection_ENCRYPTED[];
|
||||
cashuMints?: CashuMint_ENCRYPTED[];
|
||||
}
|
||||
|
||||
export type BrowserSyncData = BrowserSyncData_PART_Unencrypted &
|
||||
@@ -83,11 +159,17 @@ export interface BrowserSessionData {
|
||||
identities: Identity_DECRYPTED[];
|
||||
selectedIdentityId: string | null;
|
||||
relays: Relay_DECRYPTED[];
|
||||
nwcConnections?: NwcConnection_DECRYPTED[];
|
||||
cashuMints?: CashuMint_DECRYPTED[];
|
||||
}
|
||||
|
||||
export interface SignerMetaData_VaultSnapshot {
|
||||
id: string;
|
||||
fileName: string;
|
||||
createdAt: string; // ISO timestamp
|
||||
data: BrowserSyncData;
|
||||
identityCount: number;
|
||||
reason?: 'manual' | 'auto' | 'pre-restore'; // Why was this backup created
|
||||
}
|
||||
|
||||
export const SIGNER_META_DATA_KEY = {
|
||||
@@ -109,6 +191,9 @@ export interface SignerMetaData {
|
||||
|
||||
vaultSnapshots?: SignerMetaData_VaultSnapshot[];
|
||||
|
||||
// Maximum number of automatic backups to keep (default: 5)
|
||||
maxBackups?: number;
|
||||
|
||||
// Reckless mode: auto-approve all actions without prompting
|
||||
recklessMode?: boolean;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user