mirror of
https://github.com/tompro/sattle.git
synced 2026-10-26 01:49:58 +00:00
fix: commit send, pay and move carves at the checkpoint (#6)
The three composables commit the carve inside onCarve via the existing commitCarve helper instead of after the flow, falling back to a URL lookup for no-mutation carves (which fire no hook). On failed-funds-returned with a rotation, the re-secured note is added before the burned carved note is marked spent.
This commit is contained in:
@@ -7,7 +7,7 @@ import type { MintFee } from 'lnurlcash-kit';
|
|||||||
import { transferBetweenMints } from '@/lnurlcash/ops';
|
import { transferBetweenMints } from '@/lnurlcash/ops';
|
||||||
import type { TransferOutcome } from '@/lnurlcash/ops';
|
import type { TransferOutcome } from '@/lnurlcash/ops';
|
||||||
import { maxNetForBalance, quoteMintFee } from '@/lnurlcash/fees';
|
import { maxNetForBalance, quoteMintFee } from '@/lnurlcash/fees';
|
||||||
import type { NewBearer } from '@/lnurlcash/types';
|
import type { Bearer, NewBearer } from '@/lnurlcash/types';
|
||||||
import { floorMsatToSat, msatToSats, satsToMsat, MSAT_PER_SAT } from '@/lnurlcash/units';
|
import { floorMsatToSat, msatToSats, satsToMsat, MSAT_PER_SAT } from '@/lnurlcash/units';
|
||||||
import { useWalletStore } from '@/stores/wallet';
|
import { useWalletStore } from '@/stores/wallet';
|
||||||
import { useMintsStore } from '@/stores/mints';
|
import { useMintsStore } from '@/stores/mints';
|
||||||
@@ -135,14 +135,28 @@ export const useMoveFundsPage = () => {
|
|||||||
try {
|
try {
|
||||||
const ownerFence = wallet.captureOwnerFence();
|
const ownerFence = wallet.captureOwnerFence();
|
||||||
const commitContext = { ownerFence, warn: warnCommitted };
|
const commitContext = { ownerFence, warn: warnCommitted };
|
||||||
|
// 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(
|
const transfer = await transferBetweenMints(
|
||||||
wallet.bearers,
|
wallet.bearers,
|
||||||
satsToMsat(sats),
|
satsToMsat(sats),
|
||||||
targetInput.value,
|
targetInput.value,
|
||||||
{ assertOwner: ownerFence },
|
{
|
||||||
|
assertOwner: ownerFence,
|
||||||
|
onCarve: async (carve) => {
|
||||||
|
carveState.committed = await commitCarve(wallet, carve, commitContext);
|
||||||
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
stage.value = 'Confirming the result…';
|
stage.value = 'Confirming the result…';
|
||||||
const carved = await commitCarve(wallet, transfer.carve, commitContext);
|
// a carve that mutated nothing fires no hook - the note is already
|
||||||
|
// tracked; look it up
|
||||||
|
const carved =
|
||||||
|
carveState.committed ??
|
||||||
|
wallet.bearers.find((bearer) => bearer.url === transfer.carve.note.url);
|
||||||
|
if (!carved) throw new Error('The carved note was not tracked.');
|
||||||
if (transfer.rescuedNote) {
|
if (transfer.rescuedNote) {
|
||||||
await addCommittedBearers(wallet, [transfer.rescuedNote], commitContext);
|
await addCommittedBearers(wallet, [transfer.rescuedNote], commitContext);
|
||||||
}
|
}
|
||||||
@@ -163,6 +177,13 @@ export const useMoveFundsPage = () => {
|
|||||||
);
|
);
|
||||||
toast('positive', `Moved ${sats.toLocaleString()} sats.`);
|
toast('positive', `Moved ${sats.toLocaleString()} sats.`);
|
||||||
} else if (transfer.outcome === 'failed-funds-returned') {
|
} 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 activity.log(
|
await activity.log(
|
||||||
'transfer',
|
'transfer',
|
||||||
`A ${sats.toLocaleString()} sat move to ${transfer.targetServer} failed - funds are back in your wallet.`,
|
`A ${sats.toLocaleString()} sat move to ${transfer.targetServer} failed - funds are back in your wallet.`,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { decodeBolt11AmountMsat, isBolt11Invoice, resolveLnurlInput } from 'lnur
|
|||||||
import { readClipboard } from '@/capabilities/clipboard';
|
import { readClipboard } from '@/capabilities/clipboard';
|
||||||
import { payWithBearers, UncertainOutcomeError } from '@/lnurlcash/ops';
|
import { payWithBearers, UncertainOutcomeError } from '@/lnurlcash/ops';
|
||||||
import type { PayOutcome } from '@/lnurlcash/ops';
|
import type { PayOutcome } from '@/lnurlcash/ops';
|
||||||
|
import type { Bearer } from '@/lnurlcash/types';
|
||||||
import { msatToSats, satsToMsat } from '@/lnurlcash/units';
|
import { msatToSats, satsToMsat } from '@/lnurlcash/units';
|
||||||
import { useWalletStore } from '@/stores/wallet';
|
import { useWalletStore } from '@/stores/wallet';
|
||||||
import { useActivityStore } from '@/stores/activity';
|
import { useActivityStore } from '@/stores/activity';
|
||||||
@@ -145,15 +146,26 @@ export const usePayInvoiceDialog = (props: PayInvoiceProps, emit: PayInvoiceEmit
|
|||||||
try {
|
try {
|
||||||
ownerFence = wallet.captureOwnerFence();
|
ownerFence = wallet.captureOwnerFence();
|
||||||
const commitContext = { ownerFence, warn: warnCommitted };
|
const commitContext = { ownerFence, 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 } = {};
|
||||||
|
const onCarve = async (carve: Parameters<typeof commitCarve>[1]): Promise<void> => {
|
||||||
|
carveState.committed = await commitCarve(wallet, carve, commitContext);
|
||||||
|
};
|
||||||
const paid = await payWithBearers(
|
const paid = await payWithBearers(
|
||||||
wallet.bearers,
|
wallet.bearers,
|
||||||
payment.input,
|
payment.input,
|
||||||
payment.kind === 'address'
|
payment.kind === 'address'
|
||||||
? { amountMsat: payment.amountMsat, assertOwner: ownerFence }
|
? { amountMsat: payment.amountMsat, assertOwner: ownerFence, onCarve }
|
||||||
: { assertOwner: ownerFence },
|
: { assertOwner: ownerFence, onCarve },
|
||||||
);
|
);
|
||||||
stage.value = 'Confirming the result…';
|
stage.value = 'Confirming the result…';
|
||||||
const committed = await commitCarve(wallet, paid.carve, commitContext);
|
// 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) {
|
if (paid.rescuedNote) {
|
||||||
await addCommittedBearers(wallet, [paid.rescuedNote], commitContext);
|
await addCommittedBearers(wallet, [paid.rescuedNote], commitContext);
|
||||||
}
|
}
|
||||||
@@ -166,6 +178,13 @@ export const usePayInvoiceDialog = (props: PayInvoiceProps, emit: PayInvoiceEmit
|
|||||||
toast('positive', `Paid ${sats} sats.`);
|
toast('positive', `Paid ${sats} sats.`);
|
||||||
emit('sent');
|
emit('sent');
|
||||||
} else if (paid.outcome === 'failed-funds-returned') {
|
} 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(
|
await activity.log(
|
||||||
'transfer',
|
'transfer',
|
||||||
`A ${sats} sat payment failed - funds are back in your wallet.`,
|
`A ${sats} sat payment failed - funds are back in your wallet.`,
|
||||||
|
|||||||
@@ -82,10 +82,26 @@ export const useSendTokenDialog = (props: SendTokenProps, emit: SendTokenEmit) =
|
|||||||
let ownerFence: WalletOwnerFence | undefined;
|
let ownerFence: WalletOwnerFence | undefined;
|
||||||
try {
|
try {
|
||||||
ownerFence = wallet.captureOwnerFence();
|
ownerFence = wallet.captureOwnerFence();
|
||||||
|
const fence = ownerFence;
|
||||||
|
// 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), {
|
const carve = await ensureExactAmount(wallet.bearers, satsToMsat(sats), {
|
||||||
assertOwner: ownerFence,
|
assertOwner: fence,
|
||||||
|
onCarve: async (landed) => {
|
||||||
|
carveState.committed = await commitCarve(wallet, landed, {
|
||||||
|
ownerFence: fence,
|
||||||
|
warn: warnCommitted,
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const note = await commitCarve(wallet, carve, { ownerFence, warn: warnCommitted });
|
// 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 === carve.note.url);
|
||||||
|
if (!committed) throw new Error('The carved note was not tracked.');
|
||||||
|
const note = committed;
|
||||||
if (carve.change) {
|
if (carve.change) {
|
||||||
await activity.log(
|
await activity.log(
|
||||||
'split',
|
'split',
|
||||||
|
|||||||
Reference in New Issue
Block a user