cashu.ts raw

   1  import {
   2    CryptoHelper,
   3    CashuMint_DECRYPTED,
   4    CashuMint_ENCRYPTED,
   5    CashuProof,
   6    StorageService,
   7  } from '@common';
   8  import { LockedVaultContext } from './identity';
   9  
  10  /**
  11   * Validate a Cashu mint URL
  12   */
  13  export function isValidMintUrl(url: string): boolean {
  14    try {
  15      const parsed = new URL(url);
  16      return parsed.protocol === 'https:' || parsed.protocol === 'http:';
  17    } catch {
  18      return false;
  19    }
  20  }
  21  
  22  export const addCashuMint = async function (
  23    this: StorageService,
  24    data: {
  25      name: string;
  26      mintUrl: string;
  27      unit?: string;
  28    }
  29  ): Promise<CashuMint_DECRYPTED> {
  30    this.assureIsInitialized();
  31  
  32    // Validate the mint URL
  33    if (!isValidMintUrl(data.mintUrl)) {
  34      throw new Error('Invalid mint URL format');
  35    }
  36  
  37    // Normalize URL (remove trailing slash)
  38    const normalizedUrl = data.mintUrl.replace(/\/$/, '');
  39  
  40    // Check if a mint with the same URL already exists
  41    const existingMint = (
  42      this.getBrowserSessionHandler().browserSessionData?.cashuMints ?? []
  43    ).find((x) => x.mintUrl === normalizedUrl);
  44    if (existingMint) {
  45      throw new Error(
  46        `A connection to this mint already exists: ${existingMint.name}`
  47      );
  48    }
  49  
  50    const browserSessionData = this.getBrowserSessionHandler().browserSessionData;
  51    if (!browserSessionData) {
  52      throw new Error('Browser session data is undefined.');
  53    }
  54  
  55    const decryptedMint: CashuMint_DECRYPTED = {
  56      id: CryptoHelper.v4(),
  57      name: data.name,
  58      mintUrl: normalizedUrl,
  59      unit: data.unit ?? 'sat',
  60      createdAt: new Date().toISOString(),
  61      proofs: [], // Start with no proofs
  62      cachedBalance: 0,
  63      cachedBalanceAt: new Date().toISOString(),
  64    };
  65  
  66    // Initialize array if needed
  67    if (!browserSessionData.cashuMints) {
  68      browserSessionData.cashuMints = [];
  69    }
  70  
  71    // Add the new mint to the session data
  72    browserSessionData.cashuMints.push(decryptedMint);
  73    this.getBrowserSessionHandler().saveFullData(browserSessionData);
  74  
  75    // Encrypt the new mint and add it to the sync data
  76    const encryptedMint = await encryptCashuMint.call(this, decryptedMint);
  77    const encryptedMints = [
  78      ...(this.getBrowserSyncHandler().browserSyncData?.cashuMints ?? []),
  79      encryptedMint,
  80    ];
  81  
  82    await this.getBrowserSyncHandler().saveAndSetPartialData_CashuMints({
  83      cashuMints: encryptedMints,
  84    });
  85  
  86    return decryptedMint;
  87  };
  88  
  89  export const deleteCashuMint = async function (
  90    this: StorageService,
  91    mintId: string
  92  ): Promise<void> {
  93    this.assureIsInitialized();
  94  
  95    if (!mintId) {
  96      return;
  97    }
  98  
  99    const browserSessionData = this.getBrowserSessionHandler().browserSessionData;
 100    const browserSyncData = this.getBrowserSyncHandler().browserSyncData;
 101    if (!browserSessionData || !browserSyncData) {
 102      throw new Error('Browser session or sync data is undefined.');
 103    }
 104  
 105    // Remove from session data
 106    browserSessionData.cashuMints = (browserSessionData.cashuMints ?? []).filter(
 107      (x) => x.id !== mintId
 108    );
 109    await this.getBrowserSessionHandler().saveFullData(browserSessionData);
 110  
 111    // Handle Sync data
 112    const encryptedMintId = await this.encrypt(mintId);
 113    await this.getBrowserSyncHandler().saveAndSetPartialData_CashuMints({
 114      cashuMints: (browserSyncData.cashuMints ?? []).filter(
 115        (x) => x.id !== encryptedMintId
 116      ),
 117    });
 118  };
 119  
 120  /**
 121   * Update the proofs for a Cashu mint
 122   * This is called after send/receive operations
 123   */
 124  export const updateCashuMintProofs = async function (
 125    this: StorageService,
 126    mintId: string,
 127    proofs: CashuProof[]
 128  ): Promise<void> {
 129    this.assureIsInitialized();
 130  
 131    const browserSessionData = this.getBrowserSessionHandler().browserSessionData;
 132    const browserSyncData = this.getBrowserSyncHandler().browserSyncData;
 133    if (!browserSessionData || !browserSyncData) {
 134      throw new Error('Browser session or sync data is undefined.');
 135    }
 136  
 137    const sessionMint = (browserSessionData.cashuMints ?? []).find(
 138      (x) => x.id === mintId
 139    );
 140    const encryptedMintId = await this.encrypt(mintId);
 141    const syncMint = (browserSyncData.cashuMints ?? []).find(
 142      (x) => x.id === encryptedMintId
 143    );
 144  
 145    if (!sessionMint || !syncMint) {
 146      throw new Error('Cashu mint not found for proofs update.');
 147    }
 148  
 149    const now = new Date().toISOString();
 150    // Calculate balance from proofs (sum of all proof amounts in satoshis)
 151    const balance = proofs.reduce((sum, p) => sum + p.amount, 0);
 152  
 153    // Update session data
 154    sessionMint.proofs = proofs;
 155    sessionMint.cachedBalance = balance;
 156    sessionMint.cachedBalanceAt = now;
 157    await this.getBrowserSessionHandler().saveFullData(browserSessionData);
 158  
 159    // Update sync data
 160    syncMint.proofs = await this.encrypt(JSON.stringify(proofs));
 161    syncMint.cachedBalance = await this.encrypt(balance.toString());
 162    syncMint.cachedBalanceAt = await this.encrypt(now);
 163    await this.getBrowserSyncHandler().saveAndSetPartialData_CashuMints({
 164      cashuMints: browserSyncData.cashuMints ?? [],
 165    });
 166  };
 167  
 168  export const encryptCashuMint = async function (
 169    this: StorageService,
 170    mint: CashuMint_DECRYPTED
 171  ): Promise<CashuMint_ENCRYPTED> {
 172    const encrypted: CashuMint_ENCRYPTED = {
 173      id: await this.encrypt(mint.id),
 174      name: await this.encrypt(mint.name),
 175      mintUrl: await this.encrypt(mint.mintUrl),
 176      unit: await this.encrypt(mint.unit),
 177      createdAt: await this.encrypt(mint.createdAt),
 178      proofs: await this.encrypt(JSON.stringify(mint.proofs)),
 179    };
 180  
 181    if (mint.cachedBalance !== undefined) {
 182      encrypted.cachedBalance = await this.encrypt(mint.cachedBalance.toString());
 183    }
 184    if (mint.cachedBalanceAt) {
 185      encrypted.cachedBalanceAt = await this.encrypt(mint.cachedBalanceAt);
 186    }
 187  
 188    return encrypted;
 189  };
 190  
 191  export const decryptCashuMint = async function (
 192    this: StorageService,
 193    mint: CashuMint_ENCRYPTED,
 194    withLockedVault: LockedVaultContext | undefined = undefined
 195  ): Promise<CashuMint_DECRYPTED> {
 196    if (typeof withLockedVault === 'undefined') {
 197      // Normal decryption with unlocked vault
 198      const proofsJson = await this.decrypt(mint.proofs, 'string');
 199      const decrypted: CashuMint_DECRYPTED = {
 200        id: await this.decrypt(mint.id, 'string'),
 201        name: await this.decrypt(mint.name, 'string'),
 202        mintUrl: await this.decrypt(mint.mintUrl, 'string'),
 203        unit: await this.decrypt(mint.unit, 'string'),
 204        createdAt: await this.decrypt(mint.createdAt, 'string'),
 205        proofs: JSON.parse(proofsJson) as CashuProof[],
 206      };
 207  
 208      if (mint.cachedBalance) {
 209        decrypted.cachedBalance = await this.decrypt(mint.cachedBalance, 'number');
 210      }
 211      if (mint.cachedBalanceAt) {
 212        decrypted.cachedBalanceAt = await this.decrypt(
 213          mint.cachedBalanceAt,
 214          'string'
 215        );
 216      }
 217  
 218      return decrypted;
 219    }
 220  
 221    // v2: Use pre-derived key
 222    if (withLockedVault.keyBase64) {
 223      const proofsJson = await this.decryptWithLockedVaultV2(
 224        mint.proofs,
 225        'string',
 226        withLockedVault.iv,
 227        withLockedVault.keyBase64
 228      );
 229      const decrypted: CashuMint_DECRYPTED = {
 230        id: await this.decryptWithLockedVaultV2(
 231          mint.id,
 232          'string',
 233          withLockedVault.iv,
 234          withLockedVault.keyBase64
 235        ),
 236        name: await this.decryptWithLockedVaultV2(
 237          mint.name,
 238          'string',
 239          withLockedVault.iv,
 240          withLockedVault.keyBase64
 241        ),
 242        mintUrl: await this.decryptWithLockedVaultV2(
 243          mint.mintUrl,
 244          'string',
 245          withLockedVault.iv,
 246          withLockedVault.keyBase64
 247        ),
 248        unit: await this.decryptWithLockedVaultV2(
 249          mint.unit,
 250          'string',
 251          withLockedVault.iv,
 252          withLockedVault.keyBase64
 253        ),
 254        createdAt: await this.decryptWithLockedVaultV2(
 255          mint.createdAt,
 256          'string',
 257          withLockedVault.iv,
 258          withLockedVault.keyBase64
 259        ),
 260        proofs: JSON.parse(proofsJson) as CashuProof[],
 261      };
 262  
 263      if (mint.cachedBalance) {
 264        decrypted.cachedBalance = await this.decryptWithLockedVaultV2(
 265          mint.cachedBalance,
 266          'number',
 267          withLockedVault.iv,
 268          withLockedVault.keyBase64
 269        );
 270      }
 271      if (mint.cachedBalanceAt) {
 272        decrypted.cachedBalanceAt = await this.decryptWithLockedVaultV2(
 273          mint.cachedBalanceAt,
 274          'string',
 275          withLockedVault.iv,
 276          withLockedVault.keyBase64
 277        );
 278      }
 279  
 280      return decrypted;
 281    }
 282  
 283    // v1: Use password (PBKDF2)
 284    const proofsJson = await this.decryptWithLockedVault(
 285      mint.proofs,
 286      'string',
 287      withLockedVault.iv,
 288      withLockedVault.password!
 289    );
 290    const decrypted: CashuMint_DECRYPTED = {
 291      id: await this.decryptWithLockedVault(
 292        mint.id,
 293        'string',
 294        withLockedVault.iv,
 295        withLockedVault.password!
 296      ),
 297      name: await this.decryptWithLockedVault(
 298        mint.name,
 299        'string',
 300        withLockedVault.iv,
 301        withLockedVault.password!
 302      ),
 303      mintUrl: await this.decryptWithLockedVault(
 304        mint.mintUrl,
 305        'string',
 306        withLockedVault.iv,
 307        withLockedVault.password!
 308      ),
 309      unit: await this.decryptWithLockedVault(
 310        mint.unit,
 311        'string',
 312        withLockedVault.iv,
 313        withLockedVault.password!
 314      ),
 315      createdAt: await this.decryptWithLockedVault(
 316        mint.createdAt,
 317        'string',
 318        withLockedVault.iv,
 319        withLockedVault.password!
 320      ),
 321      proofs: JSON.parse(proofsJson) as CashuProof[],
 322    };
 323  
 324    if (mint.cachedBalance) {
 325      decrypted.cachedBalance = await this.decryptWithLockedVault(
 326        mint.cachedBalance,
 327        'number',
 328        withLockedVault.iv,
 329        withLockedVault.password!
 330      );
 331    }
 332    if (mint.cachedBalanceAt) {
 333      decrypted.cachedBalanceAt = await this.decryptWithLockedVault(
 334        mint.cachedBalanceAt,
 335        'string',
 336        withLockedVault.iv,
 337        withLockedVault.password!
 338      );
 339    }
 340  
 341    return decrypted;
 342  };
 343  
 344  export const decryptCashuMints = async function (
 345    this: StorageService,
 346    mints: CashuMint_ENCRYPTED[],
 347    withLockedVault: LockedVaultContext | undefined = undefined
 348  ): Promise<CashuMint_DECRYPTED[]> {
 349    const decryptedMints: CashuMint_DECRYPTED[] = [];
 350  
 351    for (const mint of mints) {
 352      const decryptedMint = await decryptCashuMint.call(
 353        this,
 354        mint,
 355        withLockedVault
 356      );
 357      decryptedMints.push(decryptedMint);
 358    }
 359  
 360    return decryptedMints;
 361  };
 362