mirror of
https://github.com/tompro/sattle.git
synced 2026-10-26 01:49:58 +00:00
feat(composables): route fund dialogs through the durable seam
This commit is contained in:
@@ -113,6 +113,7 @@
|
||||
{{ claimError }}
|
||||
</q-banner>
|
||||
<q-btn
|
||||
v-if="claimRetryable"
|
||||
unelevated
|
||||
color="primary"
|
||||
text-color="dark"
|
||||
@@ -215,6 +216,7 @@ const {
|
||||
CUSTOM_MINT,
|
||||
amountSats,
|
||||
claimError,
|
||||
claimRetryable,
|
||||
copyInvoice,
|
||||
createInvoice,
|
||||
customMint,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// allow: SIZE_OK — cohesive move-funds UI state machine spanning quote through durable outcome.
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useQuasar } from 'quasar';
|
||||
@@ -7,9 +8,10 @@ import type { MintFee } from 'lnurlcash-kit';
|
||||
import { transferBetweenMints } from '@/lnurlcash/ops';
|
||||
import type { TransferOutcome } from '@/lnurlcash/ops';
|
||||
import { maxNetForBalance, quoteMintFee } from '@/lnurlcash/fees';
|
||||
import type { Bearer, NewBearer } from '@/lnurlcash/types';
|
||||
import { getTrustedMintVerificationKeys } from '@/lnurlcash/trustedMints';
|
||||
import type { Bearer } from '@/lnurlcash/types';
|
||||
import { floorMsatToSat, msatToSats, satsToMsat, MSAT_PER_SAT } from '@/lnurlcash/units';
|
||||
import { useWalletStore } from '@/stores/wallet';
|
||||
import { TrustedMintPostCommitError, useWalletStore } from '@/stores/wallet';
|
||||
import { useMintsStore } from '@/stores/mints';
|
||||
import { useActivityStore } from '@/stores/activity';
|
||||
import { addCommittedBearers, commitCarve } from './walletCarveCommit';
|
||||
@@ -132,28 +134,54 @@ export const useMoveFundsPage = () => {
|
||||
if (!sats) return;
|
||||
step.value = 'working';
|
||||
stage.value = 'Asking the target mint for an invoice…';
|
||||
let fundOperation: ReturnType<typeof wallet.beginFundOperation> | undefined;
|
||||
try {
|
||||
const ownerFence = wallet.captureOwnerFence();
|
||||
fundOperation = wallet.beginFundOperation();
|
||||
const ownerFence = fundOperation.ownerFence;
|
||||
const ownerId = wallet.pubkey ?? undefined;
|
||||
const commitContext = { ownerFence, warn: warnCommitted };
|
||||
let stagedOutput: Bearer | undefined;
|
||||
let carvedOutput: Bearer | undefined;
|
||||
// the carve commits the moment it lands server-side (onCarve), not
|
||||
// after the melt + settlement wait - an abort mid-wait can then
|
||||
// never leave burned inputs looking spendable or strand the outputs
|
||||
const carveState: { committed?: Bearer } = {};
|
||||
const transfer = await transferBetweenMints(
|
||||
wallet.bearers,
|
||||
wallet.bearers.filter((bearer) => serverOf(bearer.url) === sourceServer.value),
|
||||
satsToMsat(sats),
|
||||
targetInput.value,
|
||||
{
|
||||
assertOwner: ownerFence,
|
||||
allocateOutputSecrets: (server, count) =>
|
||||
wallet.allocateOutputSecrets(server, count, ownerFence),
|
||||
mintSignatureKeys: (server) => getTrustedMintVerificationKeys(server, ownerId),
|
||||
persistOutput: async (note) => {
|
||||
[stagedOutput] = await addCommittedBearers(wallet, [note], commitContext);
|
||||
},
|
||||
onCarve: async (carve) => {
|
||||
carveState.committed = await commitCarve(wallet, carve, commitContext);
|
||||
},
|
||||
onMeltReady: async (carve, sourceRecoverySecret) => {
|
||||
if (!stagedOutput) throw new Error('The pending target note was not persisted.');
|
||||
carvedOutput =
|
||||
carveState.committed ??
|
||||
wallet.bearers.find((bearer) => bearer.url === carve.note.url);
|
||||
if (!carvedOutput) throw new Error('The carved note was not tracked.');
|
||||
await wallet.reserveStagedMintSource(
|
||||
stagedOutput.id,
|
||||
carvedOutput.id,
|
||||
sourceRecoverySecret,
|
||||
ownerFence,
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
if (!stagedOutput) throw new Error('The pending target note was not persisted.');
|
||||
stage.value = 'Confirming the result…';
|
||||
// a carve that mutated nothing fires no hook - the note is already
|
||||
// tracked; look it up
|
||||
const carved =
|
||||
carvedOutput ??
|
||||
carveState.committed ??
|
||||
wallet.bearers.find((bearer) => bearer.url === transfer.carve.note.url);
|
||||
if (!carved) throw new Error('The carved note was not tracked.');
|
||||
@@ -162,13 +190,14 @@ export const useMoveFundsPage = () => {
|
||||
}
|
||||
const feeSats = msatToSats(transfer.quote.targetMintFeeMsat);
|
||||
if (transfer.outcome === 'settled') {
|
||||
await wallet.markSpent(carved.id, ownerFence);
|
||||
const claimed = transfer.mintedAtTarget;
|
||||
if (claimed) {
|
||||
const notes: NewBearer[] = claimed.possibleCopy
|
||||
? [claimed.note, claimed.possibleCopy]
|
||||
: [claimed.note];
|
||||
await addCommittedBearers(wallet, notes, commitContext);
|
||||
try {
|
||||
await wallet.finalizeStagedMintOutput(stagedOutput.id, claimed.note, ownerFence);
|
||||
} catch (error) {
|
||||
if (!(error instanceof TrustedMintPostCommitError)) throw error;
|
||||
warnCommitted(error.message);
|
||||
}
|
||||
}
|
||||
await activity.log(
|
||||
'transfer',
|
||||
@@ -178,11 +207,11 @@ export const useMoveFundsPage = () => {
|
||||
toast('positive', `Moved ${sats.toLocaleString()} sats.`);
|
||||
} else if (transfer.outcome === 'failed-funds-returned') {
|
||||
if (transfer.rotatedNote) {
|
||||
// the source-probe rotate re-secured the returned funds at a
|
||||
// fresh secret, burning the carved note the checkpoint already
|
||||
// committed - add the new note BEFORE marking the old one spent
|
||||
await addCommittedBearers(wallet, [transfer.rotatedNote], commitContext);
|
||||
await wallet.markSpent(carved.id, ownerFence);
|
||||
await wallet.restoreStagedMintSource(
|
||||
stagedOutput.id,
|
||||
transfer.rotatedNote,
|
||||
ownerFence,
|
||||
);
|
||||
}
|
||||
await activity.log(
|
||||
'transfer',
|
||||
@@ -191,22 +220,13 @@ export const useMoveFundsPage = () => {
|
||||
);
|
||||
} else if (transfer.outcome === 'unknown-still-pending') {
|
||||
await wallet.markSpent(carved.id, ownerFence);
|
||||
// a named target's claim material carries the note at the wallet's
|
||||
// own secret - claimable the moment the melt settles, so it is
|
||||
// tracked unverified rather than lost with the result object
|
||||
if (transfer.claimMaterial?.note) {
|
||||
await addCommittedBearers(wallet, [transfer.claimMaterial.note], commitContext);
|
||||
}
|
||||
await activity.log(
|
||||
'transfer',
|
||||
`A move of ${sats.toLocaleString()} sats to ${transfer.targetServer} is still in flight - the note is locked.`,
|
||||
(error) => warnCommitted(error.message),
|
||||
);
|
||||
} else if (transfer.outcome === 'settled-claim-failed') {
|
||||
await wallet.markSpent(carved.id, ownerFence);
|
||||
if (transfer.claimMaterial?.note) {
|
||||
await addCommittedBearers(wallet, [transfer.claimMaterial.note], commitContext);
|
||||
}
|
||||
await wallet.finalizeSpentStagedMintOutput(stagedOutput.id, ownerFence);
|
||||
await activity.log(
|
||||
'transfer',
|
||||
`${sats.toLocaleString()} sats arrived at ${transfer.targetServer} but claiming the note failed - it is saved unverified.`,
|
||||
@@ -220,7 +240,7 @@ export const useMoveFundsPage = () => {
|
||||
(error) => warnCommitted(error.message),
|
||||
);
|
||||
}
|
||||
const claimNote = transfer.claimMaterial?.note ?? null;
|
||||
const claimNote = transfer.claimMaterial?.note ?? stagedOutput;
|
||||
result.value = {
|
||||
outcome: transfer.outcome,
|
||||
requestedSats: sats,
|
||||
@@ -237,6 +257,8 @@ export const useMoveFundsPage = () => {
|
||||
: message;
|
||||
toast('negative', inlineError.value);
|
||||
step.value = 'form';
|
||||
} finally {
|
||||
fundOperation?.complete();
|
||||
}
|
||||
};
|
||||
return {
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { decodeBolt11AmountMsat, isBolt11Invoice, resolveLnurlInput } from 'lnurlcash-kit';
|
||||
import {
|
||||
decodeBolt11AmountMsat,
|
||||
isBolt11Invoice,
|
||||
resolveLnurlInput,
|
||||
withNewK1,
|
||||
} from 'lnurlcash-kit';
|
||||
|
||||
import { readClipboard } from '@/capabilities/clipboard';
|
||||
import { payWithBearers, UncertainOutcomeError } from '@/lnurlcash/ops';
|
||||
import type { PayOutcome } from '@/lnurlcash/ops';
|
||||
import { getTrustedMintVerificationKeys } from '@/lnurlcash/trustedMints';
|
||||
import type { Bearer } from '@/lnurlcash/types';
|
||||
import { msatToSats, satsToMsat } from '@/lnurlcash/units';
|
||||
import { useWalletStore } from '@/stores/wallet';
|
||||
@@ -143,31 +149,72 @@ export const usePayInvoiceDialog = (props: PayInvoiceProps, emit: PayInvoiceEmit
|
||||
step.value = 'working';
|
||||
stage.value = 'Preparing the exact amount and sending the payment…';
|
||||
let ownerFence: WalletOwnerFence | undefined;
|
||||
let fundOperation: ReturnType<typeof wallet.beginFundOperation> | undefined;
|
||||
try {
|
||||
ownerFence = wallet.captureOwnerFence();
|
||||
const commitContext = { ownerFence, warn: warnCommitted };
|
||||
fundOperation = wallet.beginFundOperation();
|
||||
ownerFence = fundOperation.ownerFence;
|
||||
const fence = ownerFence;
|
||||
const ownerId = wallet.pubkey ?? undefined;
|
||||
const commitContext = { ownerFence: fence, warn: warnCommitted };
|
||||
// the carve commits the moment it lands server-side (onCarve), not
|
||||
// after the settlement wait - an abort mid-wait can then never leave
|
||||
// burned inputs looking spendable or strand the fresh outputs
|
||||
const carveState: { committed?: Bearer } = {};
|
||||
let returnStage: Bearer | undefined;
|
||||
const onCarve = async (carve: Parameters<typeof commitCarve>[1]): Promise<void> => {
|
||||
carveState.committed = await commitCarve(wallet, carve, commitContext);
|
||||
};
|
||||
const paid = await payWithBearers(
|
||||
wallet.bearers,
|
||||
payment.input,
|
||||
payment.kind === 'address'
|
||||
? { amountMsat: payment.amountMsat, assertOwner: ownerFence, onCarve }
|
||||
: { assertOwner: ownerFence, onCarve },
|
||||
);
|
||||
const allocateOutputSecrets = (server: string, count: number) =>
|
||||
wallet.allocateOutputSecrets(server, count, fence);
|
||||
const mintSignatureKeys = (server: string) =>
|
||||
getTrustedMintVerificationKeys(server, ownerId);
|
||||
// the pay-return recovery output: staged durably (allocated secret,
|
||||
// zero value, pendingMint) and linked to the carved source BEFORE the
|
||||
// classification rotate can burn it
|
||||
const onReturnReady = async (
|
||||
carve: Parameters<typeof commitCarve>[1],
|
||||
recoverySecret: string,
|
||||
): Promise<void> => {
|
||||
const source =
|
||||
carveState.committed ?? wallet.bearers.find((bearer) => bearer.url === carve.note.url);
|
||||
if (!source) throw new Error('The carved note was not tracked.');
|
||||
[returnStage] = await addCommittedBearers(
|
||||
wallet,
|
||||
[
|
||||
{
|
||||
url: withNewK1(carve.note.url, recoverySecret, carve.note.amount),
|
||||
callback: '',
|
||||
amount: 0,
|
||||
verified: false,
|
||||
pendingMint: { retireAfter: Math.floor(Date.now() / 1000) + 14 * 24 * 60 * 60 },
|
||||
},
|
||||
],
|
||||
commitContext,
|
||||
);
|
||||
if (!returnStage) throw new Error('The payment recovery output was not persisted.');
|
||||
await wallet.reserveStagedMintSource(returnStage.id, source.id, recoverySecret, fence);
|
||||
};
|
||||
const paid = await payWithBearers(wallet.bearers, payment.input, {
|
||||
...(payment.kind === 'address' ? { amountMsat: payment.amountMsat } : {}),
|
||||
assertOwner: fence,
|
||||
onCarve,
|
||||
allocateOutputSecrets,
|
||||
mintSignatureKeys,
|
||||
onReturnReady,
|
||||
});
|
||||
stage.value = 'Confirming the result…';
|
||||
// a carve that mutated nothing (one note already held the exact
|
||||
// amount) fires no hook - the note is already tracked; look it up
|
||||
const committed =
|
||||
carveState.committed ?? wallet.bearers.find((bearer) => bearer.url === paid.carve.note.url);
|
||||
if (!committed) throw new Error('The carved note was not tracked.');
|
||||
if (paid.rescuedNote) {
|
||||
await addCommittedBearers(wallet, [paid.rescuedNote], commitContext);
|
||||
if (returnStage) {
|
||||
const returned = paid.rotatedNote ?? paid.rescuedNote;
|
||||
if (returned) {
|
||||
await wallet.finalizeStagedMintOutput(returnStage.id, returned, fence);
|
||||
} else if (paid.outcome !== 'unknown-still-pending') {
|
||||
await wallet.finalizeSpentStagedMintOutput(returnStage.id, fence);
|
||||
}
|
||||
}
|
||||
const sats = formatSats(msatToSats(paid.amountMsat));
|
||||
if (paid.outcome === 'settled') {
|
||||
@@ -178,13 +225,6 @@ export const usePayInvoiceDialog = (props: PayInvoiceProps, emit: PayInvoiceEmit
|
||||
toast('positive', `Paid ${sats} sats.`);
|
||||
emit('sent');
|
||||
} else if (paid.outcome === 'failed-funds-returned') {
|
||||
if (paid.rotatedNote) {
|
||||
// the classification rotate re-secured the returned funds at a
|
||||
// fresh secret, burning the carved note the checkpoint already
|
||||
// committed - add the new note BEFORE marking the old one spent
|
||||
await addCommittedBearers(wallet, [paid.rotatedNote], commitContext);
|
||||
await wallet.markSpent(committed.id, ownerFence);
|
||||
}
|
||||
await activity.log(
|
||||
'transfer',
|
||||
`A ${sats} sat payment failed - funds are back in your wallet.`,
|
||||
@@ -227,6 +267,8 @@ export const usePayInvoiceDialog = (props: PayInvoiceProps, emit: PayInvoiceEmit
|
||||
toast('negative', inlineError.value);
|
||||
}
|
||||
step.value = 'input';
|
||||
} finally {
|
||||
fundOperation?.complete();
|
||||
}
|
||||
};
|
||||
const closeResult = (): void => {
|
||||
|
||||
@@ -2,9 +2,9 @@ import { computed, ref, watch } from 'vue';
|
||||
import { Notify } from 'quasar';
|
||||
|
||||
import { writeClipboard } from '@/capabilities/clipboard';
|
||||
import { prepareMint, claimMintedNote } from '@/lnurlcash/ops';
|
||||
import { MintedNoteSpentError, prepareMint, claimMintedNote } from '@/lnurlcash/ops';
|
||||
import type { ClaimedNote, PreparedMint } from '@/lnurlcash/ops';
|
||||
import type { NewBearer } from '@/lnurlcash/types';
|
||||
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';
|
||||
@@ -35,8 +35,10 @@ export const useReceiveLightningDialog = (
|
||||
const preparing = ref(false);
|
||||
const formError = ref('');
|
||||
const prepared = ref<PreparedMint | null>(null);
|
||||
const stagedOutput = ref<Bearer | null>(null);
|
||||
const waiting = ref(false);
|
||||
const claimError = ref('');
|
||||
const claimRetryable = ref(true);
|
||||
const receivedSats = ref(0);
|
||||
const receivedServer = ref('');
|
||||
const rotationWarning = ref('');
|
||||
@@ -85,23 +87,21 @@ export const useReceiveLightningDialog = (
|
||||
const onClaimed = async (
|
||||
claimed: ClaimedNote,
|
||||
from: PreparedMint,
|
||||
staged: Bearer,
|
||||
ownerFence: WalletOwnerFence,
|
||||
): Promise<void> => {
|
||||
const server = from.server;
|
||||
const wasTrusted = mints.isTrusted(server);
|
||||
const notes: NewBearer[] = claimed.possibleCopy
|
||||
? [claimed.note, claimed.possibleCopy]
|
||||
: [claimed.note];
|
||||
let trustWarning = '';
|
||||
try {
|
||||
await wallet.addBearers(notes, ownerFence);
|
||||
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 = claimed.rotationError ?? '';
|
||||
rotationWarning.value = '';
|
||||
await activity.log(
|
||||
'mint',
|
||||
`Received ${receivedSats.value.toLocaleString()} sats from ${server} over Lightning.`,
|
||||
@@ -130,23 +130,35 @@ export const useReceiveLightningDialog = (
|
||||
}
|
||||
};
|
||||
const beginClaim = (): void => {
|
||||
if (!prepared.value || claimRun) return;
|
||||
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 = wallet.captureOwnerFence();
|
||||
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;
|
||||
}
|
||||
})();
|
||||
@@ -156,20 +168,26 @@ export const useReceiveLightningDialog = (
|
||||
if (!sats || preparing.value) return;
|
||||
preparing.value = true;
|
||||
formError.value = '';
|
||||
let fundOperation: ReturnType<typeof wallet.beginFundOperation> | undefined;
|
||||
try {
|
||||
const input = mintChoice.value === CUSTOM_MINT ? customMint.value.trim() : mintChoice.value;
|
||||
const next = await prepareMint(input, satsToMsat(sats));
|
||||
// named mints claim through the wallet's own secret and may
|
||||
// legitimately serve no verify URL; unnamed mints need one (the
|
||||
// preimage it reveals IS the claim)
|
||||
if (next.mode === 'unnamed' && !next.verifyUrl) {
|
||||
formError.value =
|
||||
'This mint does not support automatic claiming, so sattle cannot receive from it. Choose a different mint.';
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
@@ -177,6 +195,7 @@ export const useReceiveLightningDialog = (
|
||||
formError.value = errorMessage(error);
|
||||
Notify.create({ type: 'negative', message: formError.value });
|
||||
} finally {
|
||||
fundOperation?.complete();
|
||||
preparing.value = false;
|
||||
}
|
||||
};
|
||||
@@ -211,6 +230,7 @@ export const useReceiveLightningDialog = (
|
||||
preparing.value = false;
|
||||
formError.value = '';
|
||||
prepared.value = null;
|
||||
stagedOutput.value = null;
|
||||
waiting.value = false;
|
||||
rotationWarning.value = '';
|
||||
},
|
||||
@@ -219,6 +239,7 @@ export const useReceiveLightningDialog = (
|
||||
CUSTOM_MINT,
|
||||
amountSats,
|
||||
claimError,
|
||||
claimRetryable,
|
||||
copyInvoice,
|
||||
createInvoice,
|
||||
customMint,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { toBech32Lnurl } from 'lnurlcash-kit';
|
||||
import { writeClipboard } from '@/capabilities/clipboard';
|
||||
import { canShareText, shareText } from '@/capabilities/share';
|
||||
import { ensureExactAmount, UncertainOutcomeError } from '@/lnurlcash/ops';
|
||||
import { getTrustedMintVerificationKeys } from '@/lnurlcash/trustedMints';
|
||||
import type { Bearer } from '@/lnurlcash/types';
|
||||
import { msatToSats, satsToMsat } from '@/lnurlcash/units';
|
||||
import { useWalletStore } from '@/stores/wallet';
|
||||
@@ -80,15 +81,21 @@ export const useSendTokenDialog = (props: SendTokenProps, emit: SendTokenEmit) =
|
||||
preparing.value = true;
|
||||
errorMessage.value = null;
|
||||
let ownerFence: WalletOwnerFence | undefined;
|
||||
let fundOperation: ReturnType<typeof wallet.beginFundOperation> | undefined;
|
||||
try {
|
||||
ownerFence = wallet.captureOwnerFence();
|
||||
fundOperation = wallet.beginFundOperation();
|
||||
ownerFence = fundOperation.ownerFence;
|
||||
const fence = ownerFence;
|
||||
const ownerId = wallet.pubkey ?? undefined;
|
||||
// the carve commits the moment it lands server-side (onCarve), so
|
||||
// even a crash right after can never strand the fresh outputs or
|
||||
// leave burned inputs looking spendable
|
||||
const carveState: { committed?: Bearer } = {};
|
||||
const carve = await ensureExactAmount(wallet.bearers, satsToMsat(sats), {
|
||||
assertOwner: fence,
|
||||
allocateOutputSecrets: (server, count) =>
|
||||
wallet.allocateOutputSecrets(server, count, fence),
|
||||
mintSignatureKeys: (server) => getTrustedMintVerificationKeys(server, ownerId),
|
||||
onCarve: async (landed) => {
|
||||
carveState.committed = await commitCarve(wallet, landed, {
|
||||
ownerFence: fence,
|
||||
@@ -141,6 +148,7 @@ export const useSendTokenDialog = (props: SendTokenProps, emit: SendTokenEmit) =
|
||||
: message;
|
||||
toast('negative', errorMessage.value);
|
||||
} finally {
|
||||
fundOperation?.complete();
|
||||
preparing.value = false;
|
||||
}
|
||||
};
|
||||
@@ -165,8 +173,10 @@ export const useSendTokenDialog = (props: SendTokenProps, emit: SendTokenEmit) =
|
||||
const note = prepared.value;
|
||||
if (!note) return;
|
||||
removing.value = true;
|
||||
let fundOperation: ReturnType<typeof wallet.beginFundOperation> | undefined;
|
||||
try {
|
||||
await wallet.markSpent(note.id, wallet.captureOwnerFence());
|
||||
fundOperation = wallet.beginFundOperation();
|
||||
await wallet.markSpent(note.id, fundOperation.ownerFence);
|
||||
await activity.log(
|
||||
'spent',
|
||||
`Handed over a ${formatSats(msatToSats(note.amount))} sat note.`,
|
||||
@@ -178,6 +188,7 @@ export const useSendTokenDialog = (props: SendTokenProps, emit: SendTokenEmit) =
|
||||
} catch (error) {
|
||||
toast('negative', error instanceof Error ? error.message : 'Something went wrong.');
|
||||
} finally {
|
||||
fundOperation?.complete();
|
||||
removing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user