wallet.component.ts raw

   1  import { Component, inject, OnInit, OnDestroy } from '@angular/core';
   2  import { Router } from '@angular/router';
   3  import { FormsModule } from '@angular/forms';
   4  import { CommonModule } from '@angular/common';
   5  import {
   6    LoggerService,
   7    NavComponent,
   8    NwcService,
   9    NwcConnection_DECRYPTED,
  10    CashuService,
  11    CashuMint_DECRYPTED,
  12    CashuProof,
  13    NwcLookupInvoiceResult,
  14    BrowserSyncFlow,
  15  } from '@common';
  16  import * as QRCode from 'qrcode';
  17  
  18  type WalletSection =
  19    | 'main'
  20    | 'cashu'
  21    | 'cashu-detail'
  22    | 'cashu-add'
  23    | 'cashu-receive'
  24    | 'cashu-send'
  25    | 'cashu-mint'
  26    | 'lightning'
  27    | 'lightning-detail'
  28    | 'lightning-add'
  29    | 'lightning-receive'
  30    | 'lightning-pay';
  31  
  32  @Component({
  33    selector: 'app-wallet',
  34    templateUrl: './wallet.component.html',
  35    styleUrl: './wallet.component.scss',
  36    imports: [CommonModule, FormsModule],
  37  })
  38  export class WalletComponent extends NavComponent implements OnInit, OnDestroy {
  39    readonly #logger = inject(LoggerService);
  40    readonly #router = inject(Router);
  41    readonly nwcService = inject(NwcService);
  42    readonly cashuService = inject(CashuService);
  43  
  44    activeSection: WalletSection = 'main';
  45    selectedConnectionId: string | null = null;
  46    selectedMintId: string | null = null;
  47  
  48    // Form fields for adding new NWC connection
  49    newWalletName = '';
  50    newWalletUrl = '';
  51    addingConnection = false;
  52    testingConnection = false;
  53    connectionError = '';
  54    connectionTestResult = '';
  55  
  56    // Form fields for adding new Cashu mint
  57    newMintName = '';
  58    newMintUrl = '';
  59    addingMint = false;
  60    testingMint = false;
  61    mintError = '';
  62    mintTestResult = '';
  63  
  64    // Cashu receive/send fields
  65    receiveToken = '';
  66    receivingToken = false;
  67    receiveError = '';
  68    receiveResult = '';
  69    sendAmount = 0;
  70    sendingToken = false;
  71    sendError = '';
  72    sendResult = '';
  73  
  74    // Cashu mint (deposit) fields
  75    depositAmount = 0;
  76    creatingDepositQuote = false;
  77    depositQuoteId = '';
  78    depositInvoice = '';
  79    depositInvoiceQr = '';
  80    depositError = '';
  81    depositSuccess = '';
  82    checkingDepositPayment = false;
  83    depositQuoteState: 'UNPAID' | 'PAID' | 'ISSUED' = 'UNPAID';
  84    private depositPollingInterval: ReturnType<typeof setInterval> | null = null;
  85  
  86    // Loading states
  87    loadingBalances = false;
  88    balanceError = '';
  89  
  90    // Lightning transaction history
  91    transactions: NwcLookupInvoiceResult[] = [];
  92    loadingTransactions = false;
  93    transactionsError = '';
  94    transactionsNotSupported = false;
  95  
  96    // Lightning receive
  97    lnReceiveAmount = 0;
  98    lnReceiveDescription = '';
  99    generatingInvoice = false;
 100    generatedInvoice = '';
 101    generatedInvoiceQr = '';
 102    lnReceiveError = '';
 103    invoiceCopied = false;
 104  
 105    // Lightning pay
 106    showPayModal = false;
 107    payInput = '';
 108    payAmount = 0;
 109    paying = false;
 110    paymentSuccess = false;
 111    paymentError = '';
 112  
 113    // Clipboard feedback
 114    addressCopied = false;
 115  
 116    // Cashu onboarding info
 117    showCashuInfo = true;
 118    currentSyncFlow: BrowserSyncFlow = BrowserSyncFlow.NO_SYNC;
 119    readonly BrowserSyncFlow = BrowserSyncFlow; // Expose enum to template
 120    readonly browserDownloadSettingsUrl = 'about:preferences#general';
 121  
 122    // Cashu mint refresh
 123    refreshingMint = false;
 124    refreshError = '';
 125  
 126    get title(): string {
 127      switch (this.activeSection) {
 128        case 'cashu':
 129          return 'Cashu';
 130        case 'cashu-detail':
 131          return this.selectedMint?.name ?? 'Mint';
 132        case 'cashu-add':
 133          return 'Add Mint';
 134        case 'cashu-receive':
 135          return 'Receive';
 136        case 'cashu-send':
 137          return 'Send';
 138        case 'cashu-mint':
 139          return 'Deposit';
 140        case 'lightning':
 141          return 'Lightning';
 142        case 'lightning-detail':
 143          return this.selectedConnection?.name ?? 'Wallet';
 144        case 'lightning-add':
 145          return 'Add Wallet';
 146        case 'lightning-receive':
 147          return 'Receive';
 148        case 'lightning-pay':
 149          return 'Pay';
 150        default:
 151          return 'Wallet';
 152      }
 153    }
 154  
 155    get showBackButton(): boolean {
 156      return this.activeSection !== 'main';
 157    }
 158  
 159    get connections(): NwcConnection_DECRYPTED[] {
 160      return this.nwcService.getConnections();
 161    }
 162  
 163    get selectedConnection(): NwcConnection_DECRYPTED | undefined {
 164      if (!this.selectedConnectionId) return undefined;
 165      return this.nwcService.getConnection(this.selectedConnectionId);
 166    }
 167  
 168    get totalLightningBalance(): number {
 169      return this.nwcService.getCachedTotalBalance();
 170    }
 171  
 172    get mints(): CashuMint_DECRYPTED[] {
 173      return this.cashuService.getMints();
 174    }
 175  
 176    get selectedMint(): CashuMint_DECRYPTED | undefined {
 177      if (!this.selectedMintId) return undefined;
 178      return this.cashuService.getMint(this.selectedMintId);
 179    }
 180  
 181    get totalCashuBalance(): number {
 182      return this.cashuService.getCachedTotalBalance();
 183    }
 184  
 185    get selectedMintBalance(): number {
 186      if (!this.selectedMintId) return 0;
 187      return this.cashuService.getBalance(this.selectedMintId);
 188    }
 189  
 190    get selectedMintProofs(): CashuProof[] {
 191      if (!this.selectedMintId) return [];
 192      return this.cashuService.getProofs(this.selectedMintId);
 193    }
 194  
 195    ngOnInit(): void {
 196      // Load current sync flow setting
 197      this.currentSyncFlow = this.storage.getSyncFlow();
 198  
 199      // Refresh balances on init if we have connections
 200      if (this.connections.length > 0) {
 201        this.refreshAllBalances();
 202      }
 203    }
 204  
 205    ngOnDestroy(): void {
 206      this.nwcService.disconnectAll();
 207      this.stopDepositPolling();
 208    }
 209  
 210    setSection(section: WalletSection) {
 211      this.activeSection = section;
 212      this.connectionError = '';
 213      this.connectionTestResult = '';
 214    }
 215  
 216    goBack() {
 217      switch (this.activeSection) {
 218        case 'lightning-detail':
 219        case 'lightning-add':
 220          this.activeSection = 'lightning';
 221          this.selectedConnectionId = null;
 222          this.resetAddForm();
 223          this.resetLightningForms();
 224          break;
 225        case 'lightning-receive':
 226        case 'lightning-pay':
 227          this.activeSection = 'lightning-detail';
 228          this.resetLightningForms();
 229          break;
 230        case 'cashu-detail':
 231        case 'cashu-add':
 232          this.activeSection = 'cashu';
 233          this.selectedMintId = null;
 234          this.resetAddMintForm();
 235          break;
 236        case 'cashu-receive':
 237        case 'cashu-send':
 238        case 'cashu-mint':
 239          this.activeSection = 'cashu-detail';
 240          this.resetReceiveSendForm();
 241          this.resetDepositForm();
 242          break;
 243        case 'lightning':
 244        case 'cashu':
 245          this.activeSection = 'main';
 246          break;
 247      }
 248    }
 249  
 250    selectConnection(connectionId: string) {
 251      this.selectedConnectionId = connectionId;
 252      this.activeSection = 'lightning-detail';
 253      this.loadTransactions(connectionId);
 254    }
 255  
 256    private resetLightningForms() {
 257      this.lnReceiveAmount = 0;
 258      this.lnReceiveDescription = '';
 259      this.generatingInvoice = false;
 260      this.generatedInvoice = '';
 261      this.generatedInvoiceQr = '';
 262      this.lnReceiveError = '';
 263      this.invoiceCopied = false;
 264      this.payInput = '';
 265      this.payAmount = 0;
 266      this.paying = false;
 267      this.paymentSuccess = false;
 268      this.paymentError = '';
 269      this.showPayModal = false;
 270    }
 271  
 272    showAddConnection() {
 273      this.resetAddForm();
 274      this.activeSection = 'lightning-add';
 275    }
 276  
 277    private resetAddForm() {
 278      this.newWalletName = '';
 279      this.newWalletUrl = '';
 280      this.connectionError = '';
 281      this.connectionTestResult = '';
 282      this.addingConnection = false;
 283      this.testingConnection = false;
 284    }
 285  
 286    async testConnection() {
 287      if (!this.newWalletUrl.trim()) {
 288        this.connectionError = 'Please enter an NWC URL';
 289        return;
 290      }
 291  
 292      this.testingConnection = true;
 293      this.connectionError = '';
 294      this.connectionTestResult = '';
 295      this.nwcService.clearLogs();
 296  
 297      try {
 298        const info = await this.nwcService.testConnection(this.newWalletUrl);
 299        this.connectionTestResult = `Connected! ${info.alias ? 'Wallet: ' + info.alias : ''}`;
 300        // Hide logs on success
 301        this.nwcService.clearLogs();
 302      } catch (error) {
 303        this.connectionError =
 304          error instanceof Error ? error.message : 'Connection test failed';
 305        // Keep logs visible on failure for debugging
 306      } finally {
 307        this.testingConnection = false;
 308      }
 309    }
 310  
 311    async addConnection() {
 312      if (!this.newWalletName.trim()) {
 313        this.connectionError = 'Please enter a wallet name';
 314        return;
 315      }
 316      if (!this.newWalletUrl.trim()) {
 317        this.connectionError = 'Please enter an NWC URL';
 318        return;
 319      }
 320  
 321      this.addingConnection = true;
 322      this.connectionError = '';
 323  
 324      try {
 325        await this.nwcService.addConnection(
 326          this.newWalletName.trim(),
 327          this.newWalletUrl.trim()
 328        );
 329  
 330        // Refresh the balance for the new connection
 331        const connections = this.nwcService.getConnections();
 332        const newConnection = connections[connections.length - 1];
 333        if (newConnection) {
 334          try {
 335            await this.nwcService.getBalance(newConnection.id);
 336          } catch {
 337            // Ignore balance fetch error
 338          }
 339        }
 340  
 341        this.goBack();
 342      } catch (error) {
 343        this.connectionError =
 344          error instanceof Error ? error.message : 'Failed to add connection';
 345      } finally {
 346        this.addingConnection = false;
 347      }
 348    }
 349  
 350    async deleteConnection() {
 351      if (!this.selectedConnectionId) return;
 352  
 353      const connection = this.selectedConnection;
 354      if (
 355        !confirm(`Delete wallet "${connection?.name}"? This cannot be undone.`)
 356      ) {
 357        return;
 358      }
 359  
 360      try {
 361        await this.nwcService.deleteConnection(this.selectedConnectionId);
 362        this.goBack();
 363      } catch (error) {
 364        console.error('Failed to delete connection:', error);
 365      }
 366    }
 367  
 368    // Cashu methods
 369  
 370    selectMint(mintId: string) {
 371      this.selectedMintId = mintId;
 372      this.activeSection = 'cashu-detail';
 373      // Auto-refresh to check for spent proofs
 374      this.refreshMint();
 375    }
 376  
 377    async refreshMint() {
 378      if (!this.selectedMintId || this.refreshingMint) return;
 379  
 380      this.refreshingMint = true;
 381      this.refreshError = '';
 382  
 383      try {
 384        const removedAmount = await this.cashuService.checkProofsSpent(this.selectedMintId);
 385        if (removedAmount > 0) {
 386          // Balance was updated, proofs were spent
 387          console.log(`Removed ${removedAmount} sats of spent proofs`);
 388        }
 389      } catch (error) {
 390        this.refreshError = error instanceof Error ? error.message : 'Failed to refresh';
 391        console.error('Failed to refresh mint:', error);
 392      } finally {
 393        this.refreshingMint = false;
 394      }
 395    }
 396  
 397    showAddMint() {
 398      this.resetAddMintForm();
 399      this.activeSection = 'cashu-add';
 400    }
 401  
 402    showReceive() {
 403      this.resetReceiveSendForm();
 404      this.activeSection = 'cashu-receive';
 405    }
 406  
 407    showSend() {
 408      this.resetReceiveSendForm();
 409      this.activeSection = 'cashu-send';
 410    }
 411  
 412    private resetAddMintForm() {
 413      this.newMintName = '';
 414      this.newMintUrl = '';
 415      this.mintError = '';
 416      this.mintTestResult = '';
 417      this.addingMint = false;
 418      this.testingMint = false;
 419    }
 420  
 421    private resetReceiveSendForm() {
 422      this.receiveToken = '';
 423      this.receivingToken = false;
 424      this.receiveError = '';
 425      this.receiveResult = '';
 426      this.sendAmount = 0;
 427      this.sendingToken = false;
 428      this.sendError = '';
 429      this.sendResult = '';
 430    }
 431  
 432    private resetDepositForm() {
 433      this.depositAmount = 0;
 434      this.creatingDepositQuote = false;
 435      this.depositQuoteId = '';
 436      this.depositInvoice = '';
 437      this.depositInvoiceQr = '';
 438      this.depositError = '';
 439      this.depositSuccess = '';
 440      this.checkingDepositPayment = false;
 441      this.depositQuoteState = 'UNPAID';
 442      this.stopDepositPolling();
 443    }
 444  
 445    private stopDepositPolling() {
 446      if (this.depositPollingInterval) {
 447        clearInterval(this.depositPollingInterval);
 448        this.depositPollingInterval = null;
 449      }
 450    }
 451  
 452    async testMint() {
 453      if (!this.newMintUrl.trim()) {
 454        this.mintError = 'Please enter a mint URL';
 455        return;
 456      }
 457  
 458      this.testingMint = true;
 459      this.mintError = '';
 460      this.mintTestResult = '';
 461  
 462      try {
 463        const info = await this.cashuService.testMintConnection(
 464          this.newMintUrl.trim()
 465        );
 466        this.mintTestResult = `Connected! ${info.name ? 'Mint: ' + info.name : ''}`;
 467      } catch (error) {
 468        this.mintError =
 469          error instanceof Error ? error.message : 'Connection test failed';
 470      } finally {
 471        this.testingMint = false;
 472      }
 473    }
 474  
 475    async addMint() {
 476      if (!this.newMintName.trim()) {
 477        this.mintError = 'Please enter a mint name';
 478        return;
 479      }
 480      if (!this.newMintUrl.trim()) {
 481        this.mintError = 'Please enter a mint URL';
 482        return;
 483      }
 484  
 485      this.addingMint = true;
 486      this.mintError = '';
 487  
 488      try {
 489        await this.cashuService.addMint(
 490          this.newMintName.trim(),
 491          this.newMintUrl.trim()
 492        );
 493        this.goBack();
 494      } catch (error) {
 495        this.mintError =
 496          error instanceof Error ? error.message : 'Failed to add mint';
 497      } finally {
 498        this.addingMint = false;
 499      }
 500    }
 501  
 502    async deleteMint() {
 503      if (!this.selectedMintId) return;
 504  
 505      const mint = this.selectedMint;
 506      if (!confirm(`Delete mint "${mint?.name}"? Any tokens stored will be lost. This cannot be undone.`)) {
 507        return;
 508      }
 509  
 510      try {
 511        await this.cashuService.deleteMint(this.selectedMintId);
 512        this.goBack();
 513      } catch (error) {
 514        console.error('Failed to delete mint:', error);
 515      }
 516    }
 517  
 518    async receiveTokens() {
 519      if (!this.receiveToken.trim()) {
 520        this.receiveError = 'Please paste a Cashu token';
 521        return;
 522      }
 523  
 524      this.receivingToken = true;
 525      this.receiveError = '';
 526      this.receiveResult = '';
 527  
 528      try {
 529        const result = await this.cashuService.receive(this.receiveToken.trim());
 530        this.receiveResult = `Received ${result.amount} sats!`;
 531        this.receiveToken = '';
 532      } catch (error) {
 533        this.receiveError =
 534          error instanceof Error ? error.message : 'Failed to receive token';
 535      } finally {
 536        this.receivingToken = false;
 537      }
 538    }
 539  
 540    async sendTokens() {
 541      if (!this.selectedMintId) return;
 542  
 543      if (this.sendAmount <= 0) {
 544        this.sendError = 'Please enter a valid amount';
 545        return;
 546      }
 547  
 548      const balance = this.selectedMintBalance;
 549      if (this.sendAmount > balance) {
 550        this.sendError = `Insufficient balance. You have ${balance} sats`;
 551        return;
 552      }
 553  
 554      this.sendingToken = true;
 555      this.sendError = '';
 556      this.sendResult = '';
 557  
 558      try {
 559        const result = await this.cashuService.send(
 560          this.selectedMintId,
 561          this.sendAmount
 562        );
 563        this.sendResult = result.token;
 564        this.sendAmount = 0;
 565      } catch (error) {
 566        this.sendError =
 567          error instanceof Error ? error.message : 'Failed to create token';
 568      } finally {
 569        this.sendingToken = false;
 570      }
 571    }
 572  
 573    copyToken() {
 574      if (this.sendResult) {
 575        navigator.clipboard.writeText(this.sendResult);
 576      }
 577    }
 578  
 579    async checkProofs() {
 580      if (!this.selectedMintId) return;
 581  
 582      try {
 583        const removedAmount = await this.cashuService.checkProofsSpent(
 584          this.selectedMintId
 585        );
 586        if (removedAmount > 0) {
 587          alert(`Removed ${removedAmount} sats of spent proofs.`);
 588        } else {
 589          alert('All proofs are valid.');
 590        }
 591      } catch (error) {
 592        console.error('Failed to check proofs:', error);
 593      }
 594    }
 595  
 596    // Cashu deposit (mint) methods
 597  
 598    showDeposit() {
 599      this.resetDepositForm();
 600      this.activeSection = 'cashu-mint';
 601    }
 602  
 603    async createDepositInvoice() {
 604      if (!this.selectedMintId) return;
 605  
 606      if (this.depositAmount <= 0) {
 607        this.depositError = 'Please enter an amount';
 608        return;
 609      }
 610  
 611      this.creatingDepositQuote = true;
 612      this.depositError = '';
 613      this.depositInvoice = '';
 614      this.depositInvoiceQr = '';
 615  
 616      try {
 617        const quote = await this.cashuService.createMintQuote(
 618          this.selectedMintId,
 619          this.depositAmount
 620        );
 621  
 622        this.depositQuoteId = quote.quoteId;
 623        this.depositInvoice = quote.invoice;
 624        this.depositQuoteState = quote.state;
 625  
 626        // Generate QR code
 627        this.depositInvoiceQr = await QRCode.toDataURL(quote.invoice, {
 628          width: 200,
 629          margin: 2,
 630          color: {
 631            dark: '#000000',
 632            light: '#ffffff',
 633          },
 634        });
 635  
 636        // Start polling for payment
 637        this.startDepositPolling();
 638      } catch (error) {
 639        this.depositError =
 640          error instanceof Error ? error.message : 'Failed to create invoice';
 641      } finally {
 642        this.creatingDepositQuote = false;
 643      }
 644    }
 645  
 646    private startDepositPolling() {
 647      // Poll every 3 seconds for payment confirmation
 648      this.depositPollingInterval = setInterval(async () => {
 649        await this.checkDepositPayment();
 650      }, 3000);
 651    }
 652  
 653    async checkDepositPayment() {
 654      if (!this.selectedMintId || !this.depositQuoteId) return;
 655  
 656      this.checkingDepositPayment = true;
 657  
 658      try {
 659        const quote = await this.cashuService.checkMintQuote(
 660          this.selectedMintId,
 661          this.depositQuoteId
 662        );
 663  
 664        this.depositQuoteState = quote.state;
 665  
 666        if (quote.state === 'PAID') {
 667          // Invoice is paid, claim the tokens
 668          this.stopDepositPolling();
 669          await this.claimDepositTokens();
 670        } else if (quote.state === 'ISSUED') {
 671          // Already claimed
 672          this.stopDepositPolling();
 673          this.depositSuccess = 'Tokens already claimed!';
 674        }
 675      } catch (error) {
 676        // Don't show error for polling failures, just log
 677        console.error('Failed to check payment:', error);
 678      } finally {
 679        this.checkingDepositPayment = false;
 680      }
 681    }
 682  
 683    async claimDepositTokens() {
 684      if (!this.selectedMintId || !this.depositQuoteId) return;
 685  
 686      try {
 687        const result = await this.cashuService.mintTokens(
 688          this.selectedMintId,
 689          this.depositAmount,
 690          this.depositQuoteId
 691        );
 692  
 693        this.depositSuccess = `Received ${result.amount} sats!`;
 694        this.depositQuoteState = 'ISSUED';
 695      } catch (error) {
 696        this.depositError =
 697          error instanceof Error ? error.message : 'Failed to claim tokens';
 698      }
 699    }
 700  
 701    async copyDepositInvoice() {
 702      if (this.depositInvoice) {
 703        await navigator.clipboard.writeText(this.depositInvoice);
 704      }
 705    }
 706  
 707    formatCashuBalance(sats: number | undefined): string {
 708      return this.cashuService.formatBalance(sats);
 709    }
 710  
 711    async refreshBalance(connectionId: string) {
 712      try {
 713        await this.nwcService.getBalance(connectionId);
 714      } catch (error) {
 715        console.error('Failed to refresh balance:', error);
 716      }
 717    }
 718  
 719    async refreshAllBalances() {
 720      this.loadingBalances = true;
 721      this.balanceError = '';
 722  
 723      try {
 724        await this.nwcService.getAllBalances();
 725      } catch {
 726        this.balanceError = 'Failed to refresh some balances';
 727      } finally {
 728        this.loadingBalances = false;
 729      }
 730    }
 731  
 732    formatBalance(millisats: number | undefined): string {
 733      if (millisats === undefined) return '—';
 734      // Convert millisats to sats with 3 decimal places
 735      const sats = millisats / 1000;
 736      return sats.toLocaleString('en-US', {
 737        minimumFractionDigits: 0,
 738        maximumFractionDigits: 3,
 739      });
 740    }
 741  
 742    // Lightning transaction methods
 743  
 744    async loadTransactions(connectionId: string) {
 745      this.loadingTransactions = true;
 746      this.transactionsError = '';
 747      this.transactionsNotSupported = false;
 748  
 749      try {
 750        this.transactions = await this.nwcService.listTransactions(connectionId, {
 751          limit: 20,
 752        });
 753      } catch (error) {
 754        const errorMsg = error instanceof Error ? error.message : 'Unknown error';
 755        if (errorMsg.includes('NOT_IMPLEMENTED') || errorMsg.includes('not supported')) {
 756          this.transactionsNotSupported = true;
 757        } else {
 758          this.transactionsError = errorMsg;
 759        }
 760        this.transactions = [];
 761      } finally {
 762        this.loadingTransactions = false;
 763      }
 764    }
 765  
 766    async refreshWallet() {
 767      if (!this.selectedConnectionId) return;
 768  
 769      // Refresh balance and transactions in parallel
 770      await Promise.all([
 771        this.refreshBalance(this.selectedConnectionId),
 772        this.loadTransactions(this.selectedConnectionId),
 773      ]);
 774    }
 775  
 776    showLnReceive() {
 777      this.resetLightningForms();
 778      this.activeSection = 'lightning-receive';
 779    }
 780  
 781    showLnPay() {
 782      this.resetLightningForms();
 783      this.showPayModal = true;
 784    }
 785  
 786    closePayModal() {
 787      this.showPayModal = false;
 788      this.resetLightningForms();
 789    }
 790  
 791    async createReceiveInvoice() {
 792      if (!this.selectedConnectionId) return;
 793  
 794      if (this.lnReceiveAmount <= 0) {
 795        this.lnReceiveError = 'Please enter an amount';
 796        return;
 797      }
 798  
 799      this.generatingInvoice = true;
 800      this.lnReceiveError = '';
 801      this.generatedInvoice = '';
 802      this.generatedInvoiceQr = '';
 803  
 804      try {
 805        const result = await this.nwcService.makeInvoice(
 806          this.selectedConnectionId,
 807          this.lnReceiveAmount * 1000, // Convert sats to millisats
 808          this.lnReceiveDescription || undefined
 809        );
 810        this.generatedInvoice = result.invoice;
 811  
 812        // Generate QR code
 813        this.generatedInvoiceQr = await QRCode.toDataURL(result.invoice, {
 814          width: 200,
 815          margin: 2,
 816          color: {
 817            dark: '#000000',
 818            light: '#ffffff',
 819          },
 820        });
 821      } catch (error) {
 822        this.lnReceiveError =
 823          error instanceof Error ? error.message : 'Failed to create invoice';
 824      } finally {
 825        this.generatingInvoice = false;
 826      }
 827    }
 828  
 829    async copyInvoice() {
 830      if (this.generatedInvoice) {
 831        await navigator.clipboard.writeText(this.generatedInvoice);
 832        this.invoiceCopied = true;
 833        setTimeout(() => (this.invoiceCopied = false), 2000);
 834      }
 835    }
 836  
 837    async copyLightningAddress() {
 838      const lud16 = this.selectedConnection?.lud16;
 839      if (lud16) {
 840        await navigator.clipboard.writeText(lud16);
 841        this.addressCopied = true;
 842        setTimeout(() => (this.addressCopied = false), 2000);
 843      }
 844    }
 845  
 846    async payInvoiceOrAddress() {
 847      if (!this.selectedConnectionId || !this.payInput.trim()) {
 848        this.paymentError = 'Please enter a lightning address or invoice';
 849        return;
 850      }
 851  
 852      this.paying = true;
 853      this.paymentError = '';
 854      this.paymentSuccess = false;
 855  
 856      try {
 857        let invoice = this.payInput.trim();
 858  
 859        // Check if it's a lightning address
 860        if (this.nwcService.isLightningAddress(invoice)) {
 861          if (this.payAmount <= 0) {
 862            this.paymentError = 'Please enter an amount for lightning address payments';
 863            this.paying = false;
 864            return;
 865          }
 866          // Resolve lightning address to invoice
 867          invoice = await this.nwcService.resolveLightningAddress(
 868            invoice,
 869            this.payAmount * 1000 // Convert sats to millisats
 870          );
 871        }
 872  
 873        // Pay the invoice
 874        await this.nwcService.payInvoice(
 875          this.selectedConnectionId,
 876          invoice,
 877          this.payAmount > 0 ? this.payAmount * 1000 : undefined
 878        );
 879  
 880        this.paymentSuccess = true;
 881  
 882        // Refresh balance and transactions after payment
 883        await this.refreshWallet();
 884  
 885        // Close modal after a delay
 886        setTimeout(() => {
 887          this.closePayModal();
 888        }, 2000);
 889      } catch (error) {
 890        this.paymentError =
 891          error instanceof Error ? error.message : 'Payment failed';
 892      } finally {
 893        this.paying = false;
 894      }
 895    }
 896  
 897    formatTransactionTime(timestamp: number): string {
 898      const date = new Date(timestamp * 1000);
 899      const now = new Date();
 900      const isToday = date.toDateString() === now.toDateString();
 901  
 902      if (isToday) {
 903        return date.toLocaleTimeString('en-US', {
 904          hour: '2-digit',
 905          minute: '2-digit',
 906        });
 907      }
 908  
 909      return date.toLocaleDateString('en-US', {
 910        month: 'short',
 911        day: 'numeric',
 912      });
 913    }
 914  
 915    formatProofTime(isoTimestamp: string | undefined): string {
 916      if (!isoTimestamp) return '—';
 917  
 918      const date = new Date(isoTimestamp);
 919      const now = new Date();
 920      const isToday = date.toDateString() === now.toDateString();
 921  
 922      if (isToday) {
 923        return date.toLocaleTimeString('en-US', {
 924          hour: '2-digit',
 925          minute: '2-digit',
 926        });
 927      }
 928  
 929      return date.toLocaleDateString('en-US', {
 930        month: 'short',
 931        day: 'numeric',
 932        hour: '2-digit',
 933        minute: '2-digit',
 934      });
 935    }
 936  
 937    async onClickLock() {
 938      this.#logger.logVaultLock();
 939      await this.storage.lockVault();
 940      this.#router.navigateByUrl('/vault-login');
 941    }
 942  
 943    // Cashu onboarding methods
 944    dismissCashuInfo() {
 945      this.showCashuInfo = false;
 946    }
 947  
 948    navigateToSettings() {
 949      this.#router.navigateByUrl('/home/settings');
 950    }
 951  }
 952