commit d9544b558919d780f095c0219f4c3e4579be9bdf
parent 36a84e83a47e8721f3e719bead5bbc9761be0d77
Author: Florian Dold <dold@taler.net>
Date: Fri, 24 Jul 2026 17:16:04 +0200
wallet-core: don't run the refresh reveal after a failed melt
Also, the retry branch of the melt conflict handler never stored the
refresh group, losing the corrected input amount that the newly created
session had just been computed from.
Diffstat:
1 file changed, 37 insertions(+), 14 deletions(-)
diff --git a/packages/taler-wallet-core/src/refresh.ts b/packages/taler-wallet-core/src/refresh.ts
@@ -580,13 +580,16 @@ function getRefreshRequestTimeout(rg: WalletRefreshGroup): Duration {
* If the melt step succeeds or fails permanently,
* the status in the refresh group is updated.
*
+ * Returns true if the coin has been melted and the noreveal index is
+ * stored in the refresh session, i.e. the reveal step can proceed.
+ *
* When a transient error occurs, an exception is thrown.
*/
async function refreshMelt(
wex: WalletExecutionContext,
refreshGroupId: string,
coinIndex: number,
-): Promise<void> {
+): Promise<boolean> {
const ctx = new RefreshTransactionContext(wex, refreshGroupId);
const d = await wex.runWalletDbTx(async (tx) => {
const refreshGroup = await tx.getRefreshGroup(refreshGroupId);
@@ -601,7 +604,7 @@ async function refreshMelt(
return;
}
if (refreshSession.norevealIndex !== undefined) {
- return;
+ return "already-melted" as const;
}
const oldCoin = await tx.getCoin(refreshGroup.oldCoinPubs[coinIndex]);
@@ -639,7 +642,12 @@ async function refreshMelt(
});
if (!d) {
- return;
+ // The refresh group or session is gone, nothing left to do.
+ return false;
+ }
+
+ if (d === "already-melted") {
+ return true;
}
// Make sure that we have a seed.
@@ -736,10 +744,10 @@ async function refreshMelt(
resp.response,
resp.detail!,
);
- return;
+ return false;
case HttpStatusCode.Gone:
await handleRefreshMeltGone(ctx, coinIndex, resp.detail!);
- return;
+ return false;
case HttpStatusCode.Conflict:
await handleRefreshMeltConflict(
ctx,
@@ -749,7 +757,7 @@ async function refreshMelt(
derived.meltValueWithFee,
oldCoin,
);
- return;
+ return false;
case "ok":
break;
default:
@@ -764,23 +772,24 @@ async function refreshMelt(
refreshSession.norevealIndex = norevealIndex;
- await wex.runWalletDbTx(async (tx) => {
+ return await wex.runWalletDbTx(async (tx) => {
const rg = await tx.getRefreshGroup(refreshGroupId);
if (!rg) {
- return;
+ return false;
}
if (rg.timestampFinished) {
- return;
+ return false;
}
const rs = await tx.getRefreshSession(refreshGroupId, coinIndex);
if (!rs) {
- return;
+ return false;
}
if (rs.norevealIndex !== undefined) {
- return;
+ return true;
}
rs.norevealIndex = norevealIndex;
await tx.upsertRefreshSession(rs);
+ return true;
});
}
@@ -926,6 +935,9 @@ async function handleRefreshMeltConflict(
await destroyRefreshSession(ctx.wex, tx, rg, refreshSession);
await tx.deleteRefreshSession(ctx.refreshGroupId, coinIndex);
await initRefreshSession(ctx.wex, tx, rg, coinIndex);
+ // The new session was computed from the corrected input amount,
+ // so that amount must be stored as well.
+ await h.update(rg, "melt-conflict-retry");
}
});
}
@@ -994,6 +1006,7 @@ async function refreshReveal(
}
const norevealIndex = refreshSession.norevealIndex;
if (norevealIndex === undefined) {
+ // The caller only reveals after a successful melt.
throw Error("can't reveal without melting first");
}
@@ -1381,7 +1394,7 @@ async function processRefreshSession(
);
let { refreshGroup, refreshSession } = await wex.runWalletDbTx(async (tx) => {
const rg = await tx.getRefreshGroup(refreshGroupId);
- const rs = await tx.getRefreshSession(refreshGroupId, coinIndex);
+ let rs = await tx.getRefreshSession(refreshGroupId, coinIndex);
if (
rg != null &&
@@ -1391,9 +1404,14 @@ async function processRefreshSession(
await destroyRefreshSession(wex, tx, rg, rs);
}
await tx.deleteRefreshSession(refreshGroupId, coinIndex);
- await initRefreshSession(wex, tx, rg, coinIndex);
+ // Set the status before initializing the session, as the
+ // initialization marks the coin as finished when the remaining
+ // amount is too small to be refreshed at all.
rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Pending;
+ await initRefreshSession(wex, tx, rg, coinIndex);
await tx.upsertRefreshGroup(rg);
+ // The old session has been replaced, don't act on it anymore.
+ rs = await tx.getRefreshSession(refreshGroupId, coinIndex);
}
return {
@@ -1412,7 +1430,12 @@ async function processRefreshSession(
return;
}
if (refreshSession.norevealIndex === undefined) {
- await refreshMelt(wex, refreshGroupId, coinIndex);
+ const melted = await refreshMelt(wex, refreshGroupId, coinIndex);
+ if (!melted) {
+ // The melt didn't happen. The reason for it has already been
+ // recorded in the database, so there's nothing to report here.
+ return;
+ }
}
await refreshReveal(wex, refreshGroupId, coinIndex);
}