import { computed, ref, watch } from 'vue'; import { Notify } from 'quasar'; import { writeClipboard } from '@/capabilities/clipboard'; import { MintedNoteSpentError, prepareMint, claimMintedNote } from '@/lnurlcash/ops'; import type { ClaimedNote, PreparedMint } from '@/lnurlcash/ops'; import type { Bearer } from '@/lnurlcash/types'; import { msatToSats, satsToMsat, floorMsatToSat, MSAT_PER_SAT } from '@/lnurlcash/units'; import { mintAddressCacheInfo } from '@/lnurlcash/trustedMints'; import { TrustedMintPostCommitError, useWalletStore } from '@/stores/wallet'; import { useMintsStore } from '@/stores/mints'; import { useActivityStore } from '@/stores/activity'; import type { WalletOwnerFence } from '@/stores/walletOwnerFence'; import { useMintTrustPrompt } from './useMintTrustPrompt'; type ReceiveLightningProps = Readonly<{ modelValue: boolean }>; type ReceiveLightningEmit = (event: 'received') => void; type MintOption = Readonly<{ label: string; value: string }>; export const useReceiveLightningDialog = ( props: ReceiveLightningProps, emit: ReceiveLightningEmit, ) => { const wallet = useWalletStore(); const mints = useMintsStore(); const activity = useActivityStore(); const CUSTOM_MINT = '__custom__'; const displaySats = (msat: number): number => floorMsatToSat(msat) / MSAT_PER_SAT; const errorMessage = (error: unknown): string => error instanceof Error ? error.message : 'Something went wrong.'; const step = ref<'form' | 'invoice' | 'success'>('form'); const amountSats = ref(null); const mintChoice = ref(''); const customMint = ref(''); const preparing = ref(false); const formError = ref(''); const prepared = ref(null); const stagedOutput = ref(null); const waiting = ref(false); const claimError = ref(''); const claimRetryable = ref(true); const receivedSats = ref(0); const receivedServer = ref(''); const rotationWarning = ref(''); const trustPrompt = useMintTrustPrompt(); let claimRun: Promise | null = null; const mintOptions = computed(() => { const options: MintOption[] = []; const seen = new Set(); for (const mint of mints.mints) { const address = mint.username ? `${mint.username}@${mint.server}` : `@${mint.server}`; if (seen.has(address)) continue; seen.add(address); options.push({ label: mint.nodeAlias ? `${address} (${mint.nodeAlias})` : address, value: address, }); } for (const publicMint of mints.PUBLIC_MINTS) { if (seen.has(publicMint)) continue; seen.add(publicMint); options.push({ label: publicMint, value: publicMint }); } options.push({ label: 'Another mint…', value: CUSTOM_MINT }); return options; }); const defaultChoice = (): string => { const options = mintOptions.value; if (mints.defaultMint) { const match = options.find((option) => option.value.endsWith(`@${mints.defaultMint}`)); if (match) return match.value; } const first = options[0]; return first && first.value !== CUSTOM_MINT ? first.value : CUSTOM_MINT; }; const formValid = computed(() => { if (!Number.isInteger(amountSats.value) || (amountSats.value ?? 0) < 1) return false; return mintChoice.value === CUSTOM_MINT ? customMint.value.trim() !== '' : mintChoice.value !== ''; }); const grossSats = computed(() => (prepared.value ? msatToSats(prepared.value.grossMsat) : 0)); const netSats = computed(() => prepared.value ? msatToSats(prepared.value.expectedNoteValueMsat) : 0, ); const feeSats = computed(() => grossSats.value - netSats.value); const onClaimed = async ( claimed: ClaimedNote, from: PreparedMint, staged: Bearer, ownerFence: WalletOwnerFence, ): Promise => { const server = from.server; const wasTrusted = mints.isTrusted(server); let trustWarning = ''; try { await wallet.finalizeStagedMintOutput(staged.id, claimed.note, ownerFence); } catch (error) { if (!(error instanceof TrustedMintPostCommitError)) throw error; trustWarning = error.message; } receivedSats.value = displaySats(claimed.note.amount); receivedServer.value = server; rotationWarning.value = ''; await activity.log( 'mint', `Received ${receivedSats.value.toLocaleString()} sats from ${server} over Lightning.`, (error) => { trustWarning = error.message; }, ); const nodeInfo = mintAddressCacheInfo(from.nodeInfo, from.username); if (nodeInfo) { try { await mints.cacheNodeInfo(server, nodeInfo); } catch (error) { if (!(error instanceof Error)) throw error; trustWarning = `Funds were saved, but mint details could not be updated: ${errorMessage(error)}`; } } Notify.create({ type: 'positive', message: `Received ${receivedSats.value.toLocaleString()} sats.`, }); if (trustWarning) Notify.create({ type: 'warning', message: trustWarning }); emit('received'); if (props.modelValue) step.value = 'success'; if (!wasTrusted && claimed.note.mintPubkey) { trustPrompt.openTrust(server, claimed.note.mintPubkey, from.nodeInfo?.nodeAlias ?? ''); } }; const beginClaim = (): void => { if (!prepared.value || !stagedOutput.value || claimRun) return; waiting.value = true; claimError.value = ''; claimRetryable.value = true; const current = prepared.value; const staged = stagedOutput.value; claimRun = (async () => { const fundOperation = wallet.beginFundOperation(); try { const ownerFence = fundOperation.ownerFence; await onClaimed( await claimMintedNote(current, {}, { assertOwner: ownerFence }), current, staged, ownerFence, ); } catch (error) { if (!(error instanceof Error)) throw error; if (error instanceof MintedNoteSpentError) { await wallet.finalizeSpentStagedMintOutput(staged.id, fundOperation.ownerFence); claimRetryable.value = false; claimError.value = errorMessage(error); Notify.create({ type: 'negative', message: claimError.value }); return; } claimError.value = `${errorMessage(error)} The invoice stays valid — you can try again.`; Notify.create({ type: 'negative', message: errorMessage(error) }); } finally { fundOperation.complete(); waiting.value = false; } })(); }; const createInvoice = async (): Promise => { const sats = amountSats.value; if (!sats || preparing.value) return; preparing.value = true; formError.value = ''; let fundOperation: ReturnType | undefined; try { const input = mintChoice.value === CUSTOM_MINT ? customMint.value.trim() : mintChoice.value; fundOperation = wallet.beginFundOperation(); const ownerFence = fundOperation.ownerFence; let persisted: Bearer | undefined; const next = await prepareMint(input, satsToMsat(sats), { assertOwner: ownerFence, allocateOutputSecrets: (server, count) => wallet.allocateOutputSecrets(server, count, ownerFence), persistOutput: async (note) => { [persisted] = await wallet.addBearers([note], ownerFence); }, }); if (!persisted) throw new Error('The pending mint output was not persisted.'); stagedOutput.value = persisted; prepared.value = next; claimRun = null; claimError.value = ''; claimRetryable.value = true; step.value = 'invoice'; beginClaim(); } catch (error) { if (!(error instanceof Error)) throw error; formError.value = errorMessage(error); Notify.create({ type: 'negative', message: formError.value }); } finally { fundOperation?.complete(); preparing.value = false; } }; const copyInvoice = async (): Promise => { if (!prepared.value) return; try { await writeClipboard(prepared.value.invoice); Notify.create({ type: 'positive', message: 'Invoice copied.' }); } catch (error) { if (!(error instanceof Error)) throw error; Notify.create({ type: 'negative', message: errorMessage(error) }); } }; const retryClaim = (): void => { claimRun = null; beginClaim(); }; const stopWaiting = (): void => { waiting.value = false; }; const resumeWaiting = (): void => { if (claimRun) waiting.value = true; }; watch( () => props.modelValue, (open) => { if (!open) return; step.value = 'form'; amountSats.value = null; customMint.value = ''; mintChoice.value = defaultChoice(); preparing.value = false; formError.value = ''; prepared.value = null; stagedOutput.value = null; waiting.value = false; rotationWarning.value = ''; }, ); return { CUSTOM_MINT, amountSats, claimError, claimRetryable, copyInvoice, createInvoice, customMint, feeSats, formError, formValid, grossSats, mintChoice, mintOptions, netSats, prepared, preparing, receivedSats, receivedServer, resumeWaiting, retryClaim, rotationWarning, showTrust: trustPrompt.showTrust, skipTrust: trustPrompt.skipTrust, step, stopWaiting, trustMint: trustPrompt.trustMint, trustNodeAlias: trustPrompt.trustNodeAlias, trustServer: trustPrompt.trustServer, waiting, }; };