aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2022-09-14 20:34:37 +0200
committerFlorian Dold <florian@dold.me>2022-09-14 20:40:38 +0200
commitc021876b41bff11ad28c3a43808795fa0d02ce99 (patch)
treec92f4e83def462ddb0d446c9c476fd32f648d744
parent9d044058e267e9e94f3ee63809a1e22426151ee5 (diff)
wallet-core: cache fresh coin count in DB
-rw-r--r--packages/taler-util/src/walletTypes.ts1
-rw-r--r--packages/taler-wallet-core/src/db.ts17
-rw-r--r--packages/taler-wallet-core/src/operations/backup/import.ts1
-rw-r--r--packages/taler-wallet-core/src/operations/deposits.ts20
-rw-r--r--packages/taler-wallet-core/src/operations/pay.ts77
-rw-r--r--packages/taler-wallet-core/src/operations/peer-to-peer.ts46
-rw-r--r--packages/taler-wallet-core/src/operations/refresh.ts23
-rw-r--r--packages/taler-wallet-core/src/operations/tip.ts6
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw.ts7
-rw-r--r--packages/taler-wallet-core/src/util/query.ts5
-rw-r--r--packages/taler-wallet-core/src/wallet.ts123
11 files changed, 197 insertions, 129 deletions
diff --git a/packages/taler-util/src/walletTypes.ts b/packages/taler-util/src/walletTypes.ts
index 437ac2964..701049c26 100644
--- a/packages/taler-util/src/walletTypes.ts
+++ b/packages/taler-util/src/walletTypes.ts
@@ -529,6 +529,7 @@ export interface PlanchetCreationRequest {
export enum RefreshReason {
Manual = "manual",
PayMerchant = "pay-merchant",
+ PayDeposit = "pay-deposit",
PayPeerPush = "pay-peer-push",
PayPeerPull = "pay-peer-pull",
Refund = "refund",
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index da566ff24..6dfa63c06 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -314,6 +314,11 @@ export interface DenominationRecord {
* that includes this denomination.
*/
listIssueDate: TalerProtocolTimestamp;
+
+ /**
+ * Number of fresh coins of this denomination that are available.
+ */
+ freshCoinCount?: number;
}
/**
@@ -520,6 +525,13 @@ export enum CoinStatus {
* Withdrawn and never shown to anybody.
*/
Fresh = "fresh",
+
+ /**
+ * Fresh, but currently marked as "suspended", thus won't be used
+ * for spending. Used for testing.
+ */
+ FreshSuspended = "fresh-suspended",
+
/**
* A coin that has been spent and refreshed.
*/
@@ -606,11 +618,6 @@ export interface CoinRecord {
exchangeBaseUrl: string;
/**
- * The coin is currently suspended, and will not be used for payments.
- */
- suspended: boolean;
-
- /**
* Blinding key used when withdrawing the coin.
* Potentionally used again during payback.
*/
diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts
index 8f5d019d4..53e45918e 100644
--- a/packages/taler-wallet-core/src/operations/backup/import.ts
+++ b/packages/taler-wallet-core/src/operations/backup/import.ts
@@ -413,7 +413,6 @@ export async function importBackup(
currentAmount: Amounts.parseOrThrow(backupCoin.current_amount),
denomSig: backupCoin.denom_sig,
coinPub: compCoin.coinPub,
- suspended: false,
exchangeBaseUrl: backupExchangeDetails.base_url,
denomPubHash,
status: backupCoin.fresh
diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts
index 5838be765..6d63def59 100644
--- a/packages/taler-wallet-core/src/operations/deposits.ts
+++ b/packages/taler-wallet-core/src/operations/deposits.ts
@@ -33,12 +33,11 @@ import {
getRandomBytes,
hashWire,
Logger,
- NotificationType,
parsePaytoUri,
PayCoinSelection,
PrepareDepositRequest,
PrepareDepositResponse,
- TalerErrorDetail,
+ RefreshReason,
TalerProtocolTimestamp,
TrackDepositGroupRequest,
TrackDepositGroupResponse,
@@ -46,18 +45,15 @@ import {
} from "@gnu-taler/taler-util";
import {
DepositGroupRecord,
- OperationAttemptErrorResult,
OperationAttemptResult,
OperationStatus,
} from "../db.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { selectPayCoins } from "../util/coinSelection.js";
import { readSuccessResponseJsonOrThrow } from "../util/http.js";
-import { RetryInfo } from "../util/retries.js";
-import { guardOperationException } from "./common.js";
+import { spendCoins } from "../wallet.js";
import { getExchangeDetails } from "./exchanges.js";
import {
- applyCoinSpend,
CoinSelectionRequest,
extractContractData,
generateDepositPermissions,
@@ -525,12 +521,12 @@ export async function createDepositGroup(
x.refreshGroups,
])
.runReadWrite(async (tx) => {
- await applyCoinSpend(
- ws,
- tx,
- payCoinSel,
- `deposit-group:${depositGroup.depositGroupId}`,
- );
+ await spendCoins(ws, tx, {
+ allocationId: `deposit-group:${depositGroup.depositGroupId}`,
+ coinPubs: payCoinSel.coinPubs,
+ contributions: payCoinSel.coinContributions,
+ refreshReason: RefreshReason.PayDeposit,
+ });
await tx.depositGroups.put(depositGroup);
});
diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts
index 322e90487..bd7b1f7f0 100644
--- a/packages/taler-wallet-core/src/operations/pay.ts
+++ b/packages/taler-wallet-core/src/operations/pay.ts
@@ -100,6 +100,7 @@ import {
} from "../util/http.js";
import { GetReadWriteAccess } from "../util/query.js";
import { RetryInfo, RetryTags, scheduleRetry } from "../util/retries.js";
+import { spendCoins } from "../wallet.js";
import { getExchangeDetails } from "./exchanges.js";
import { createRefreshGroup, getTotalRefreshCost } from "./refresh.js";
@@ -156,9 +157,6 @@ export async function getTotalPaymentCost(
}
function isSpendableCoin(coin: CoinRecord, denom: DenominationRecord): boolean {
- if (coin.suspended) {
- return false;
- }
if (denom.isRevoked) {
return false;
}
@@ -348,65 +346,6 @@ export async function getCandidatePayCoins(
}
/**
- * Apply a coin selection to the database. Marks coins as spent
- * and creates a refresh session for the remaining amount.
- *
- * FIXME: This does not deal well with conflicting spends!
- * When two payments are made in parallel, the same coin can be selected
- * for two payments.
- * However, this is a situation that can also happen via sync.
- */
-export async function applyCoinSpend(
- ws: InternalWalletState,
- tx: GetReadWriteAccess<{
- coins: typeof WalletStoresV1.coins;
- refreshGroups: typeof WalletStoresV1.refreshGroups;
- denominations: typeof WalletStoresV1.denominations;
- }>,
- coinSelection: PayCoinSelection,
- allocationId: string,
-): Promise<void> {
- logger.info(`applying coin spend ${j2s(coinSelection)}`);
- for (let i = 0; i < coinSelection.coinPubs.length; i++) {
- const coin = await tx.coins.get(coinSelection.coinPubs[i]);
- if (!coin) {
- throw Error("coin allocated for payment doesn't exist anymore");
- }
- const contrib = coinSelection.coinContributions[i];
- if (coin.status !== CoinStatus.Fresh) {
- const alloc = coin.allocation;
- if (!alloc) {
- continue;
- }
- if (alloc.id !== allocationId) {
- // FIXME: assign error code
- throw Error("conflicting coin allocation (id)");
- }
- if (0 !== Amounts.cmp(alloc.amount, contrib)) {
- // FIXME: assign error code
- throw Error("conflicting coin allocation (contrib)");
- }
- continue;
- }
- coin.status = CoinStatus.Dormant;
- coin.allocation = {
- id: allocationId,
- amount: Amounts.stringify(contrib),
- };
- const remaining = Amounts.sub(coin.currentAmount, contrib);
- if (remaining.saturated) {
- throw Error("not enough remaining balance on coin for payment");
- }
- coin.currentAmount = remaining.amount;
- await tx.coins.put(coin);
- }
- const refreshCoinPubs = coinSelection.coinPubs.map((x) => ({
- coinPub: x,
- }));
- await createRefreshGroup(ws, tx, refreshCoinPubs, RefreshReason.PayMerchant);
-}
-
-/**
* Record all information that is necessary to
* pay for a proposal in the wallet's database.
*/
@@ -468,7 +407,12 @@ async function recordConfirmPay(
await tx.proposals.put(p);
}
await tx.purchases.put(t);
- await applyCoinSpend(ws, tx, coinSelection, `proposal:${t.proposalId}`);
+ await spendCoins(ws, tx, {
+ allocationId: `proposal:${t.proposalId}`,
+ coinPubs: coinSelection.coinPubs,
+ contributions: coinSelection.coinContributions,
+ refreshReason: RefreshReason.PayMerchant,
+ });
});
ws.notify({
@@ -1038,7 +982,12 @@ async function handleInsufficientFunds(
p.payCoinSelectionUid = encodeCrock(getRandomBytes(32));
p.coinDepositPermissions = undefined;
await tx.purchases.put(p);
- await applyCoinSpend(ws, tx, res, `proposal:${p.proposalId}`);
+ await spendCoins(ws, tx, {
+ allocationId: `proposal:${p.proposalId}`,
+ coinPubs: p.payCoinSelection.coinPubs,
+ contributions: p.payCoinSelection.coinContributions,
+ refreshReason: RefreshReason.PayMerchant,
+ });
});
}
diff --git a/packages/taler-wallet-core/src/operations/peer-to-peer.ts b/packages/taler-wallet-core/src/operations/peer-to-peer.ts
index 59dad3d55..449a91c68 100644
--- a/packages/taler-wallet-core/src/operations/peer-to-peer.ts
+++ b/packages/taler-wallet-core/src/operations/peer-to-peer.ts
@@ -75,6 +75,8 @@ import { internalCreateWithdrawalGroup } from "./withdraw.js";
import { GetReadOnlyAccess } from "../util/query.js";
import { createRefreshGroup } from "./refresh.js";
import { updateExchangeFromUrl } from "./exchanges.js";
+import { spendCoins } from "../wallet.js";
+import { RetryTags } from "../util/retries.js";
const logger = new Logger("operations/peer-to-peer.ts");
@@ -256,18 +258,14 @@ export async function initiatePeerToPeerPush(
return undefined;
}
- const pubs: CoinPublicKey[] = [];
- for (const c of sel.coins) {
- const coin = await tx.coins.get(c.coinPub);
- checkDbInvariant(!!coin);
- coin.currentAmount = Amounts.sub(
- coin.currentAmount,
- Amounts.parseOrThrow(c.contribution),
- ).amount;
- coin.status = CoinStatus.Dormant;
- pubs.push({ coinPub: coin.coinPub });
- await tx.coins.put(coin);
- }
+ await spendCoins(ws, tx, {
+ allocationId: `peer-push:${pursePair.pub}`,
+ coinPubs: sel.coins.map((x) => x.coinPub),
+ contributions: sel.coins.map((x) =>
+ Amounts.parseOrThrow(x.contribution),
+ ),
+ refreshReason: RefreshReason.PayPeerPush,
+ });
await tx.peerPushPaymentInitiations.add({
amount: Amounts.stringify(instructedAmount),
@@ -284,8 +282,6 @@ export async function initiatePeerToPeerPush(
timestampCreated: TalerProtocolTimestamp.now(),
});
- await createRefreshGroup(ws, tx, pubs, RefreshReason.PayPeerPush);
-
return sel;
});
logger.info(`selected p2p coins (push): ${j2s(coinSelRes)}`);
@@ -588,20 +584,14 @@ export async function acceptPeerPullPayment(
return undefined;
}
- const pubs: CoinPublicKey[] = [];
- for (const c of sel.coins) {
- const coin = await tx.coins.get(c.coinPub);
- checkDbInvariant(!!coin);
- coin.currentAmount = Amounts.sub(
- coin.currentAmount,
- Amounts.parseOrThrow(c.contribution),
- ).amount;
- coin.status = CoinStatus.Dormant;
- pubs.push({ coinPub: coin.coinPub });
- await tx.coins.put(coin);
- }
-
- await createRefreshGroup(ws, tx, pubs, RefreshReason.PayPeerPull);
+ await spendCoins(ws, tx, {
+ allocationId: `peer-pull:${req.peerPullPaymentIncomingId}`,
+ coinPubs: sel.coins.map((x) => x.coinPub),
+ contributions: sel.coins.map((x) =>
+ Amounts.parseOrThrow(x.contribution),
+ ),
+ refreshReason: RefreshReason.PayPeerPull,
+ });
const pi = await tx.peerPullPaymentIncoming.get(
req.peerPullPaymentIncomingId,
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts
index 719093bd8..d1c366cd0 100644
--- a/packages/taler-wallet-core/src/operations/refresh.ts
+++ b/packages/taler-wallet-core/src/operations/refresh.ts
@@ -77,6 +77,7 @@ import {
import { checkDbInvariant } from "../util/invariants.js";
import { GetReadWriteAccess } from "../util/query.js";
import { RetryInfo, runOperationHandlerForResult } from "../util/retries.js";
+import { makeCoinAvailable } from "../wallet.js";
import { guardOperationException } from "./common.js";
import { updateExchangeFromUrl } from "./exchanges.js";
import {
@@ -670,7 +671,6 @@ async function refreshReveal(
type: CoinSourceType.Refresh,
oldCoinPub: refreshGroup.oldCoinPubs[coinIndex],
},
- suspended: false,
coinEvHash: pc.coinEvHash,
ageCommitmentProof: pc.ageCommitmentProof,
};
@@ -680,7 +680,7 @@ async function refreshReveal(
}
await ws.db
- .mktx((x) => [x.coins, x.refreshGroups])
+ .mktx((x) => [x.coins, x.denominations, x.refreshGroups])
.runReadWrite(async (tx) => {
const rg = await tx.refreshGroups.get(refreshGroupId);
if (!rg) {
@@ -694,7 +694,7 @@ async function refreshReveal(
rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Finished;
updateGroupStatus(rg);
for (const coin of coins) {
- await tx.coins.put(coin);
+ await makeCoinAvailable(ws, tx, coin);
}
await tx.refreshGroups.put(rg);
});
@@ -865,10 +865,22 @@ export async function createRefreshGroup(
!!denom,
"denomination for existing coin must be in database",
);
+ if (coin.status !== CoinStatus.Dormant) {
+ coin.status = CoinStatus.Dormant;
+ const denom = await tx.denominations.get([
+ coin.exchangeBaseUrl,
+ coin.denomPubHash,
+ ]);
+ checkDbInvariant(!!denom);
+ checkDbInvariant(
+ denom.freshCoinCount != null && denom.freshCoinCount > 0,
+ );
+ denom.freshCoinCount--;
+ await tx.denominations.put(denom);
+ }
const refreshAmount = coin.currentAmount;
inputPerCoin.push(refreshAmount);
coin.currentAmount = Amounts.getZero(refreshAmount.currency);
- coin.status = CoinStatus.Dormant;
await tx.coins.put(coin);
const denoms = await getDenoms(coin.exchangeBaseUrl);
const cost = getTotalRefreshCost(denoms, denom, refreshAmount);
@@ -965,9 +977,6 @@ export async function autoRefresh(
if (coin.status !== CoinStatus.Fresh) {
continue;
}
- if (coin.suspended) {
- continue;
- }
const denom = await tx.denominations.get([
exchangeBaseUrl,
coin.denomPubHash,
diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts
index 04da2b988..f70e2d02b 100644
--- a/packages/taler-wallet-core/src/operations/tip.ts
+++ b/packages/taler-wallet-core/src/operations/tip.ts
@@ -51,6 +51,7 @@ import {
readSuccessResponseJsonOrThrow,
} from "../util/http.js";
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
+import { makeCoinAvailable } from "../wallet.js";
import { updateExchangeFromUrl } from "./exchanges.js";
import {
getCandidateWithdrawalDenoms,
@@ -310,13 +311,12 @@ export async function processTip(
denomSig: { cipher: DenomKeyType.Rsa, rsa_signature: denomSigRsa.sig },
exchangeBaseUrl: tipRecord.exchangeBaseUrl,
status: CoinStatus.Fresh,
- suspended: false,
coinEvHash: planchet.coinEvHash,
});
}
await ws.db
- .mktx((x) => [x.coins, x.tips, x.withdrawalGroups])
+ .mktx((x) => [x.coins, x.denominations, x.tips])
.runReadWrite(async (tx) => {
const tr = await tx.tips.get(walletTipId);
if (!tr) {
@@ -328,7 +328,7 @@ export async function processTip(
tr.pickedUpTimestamp = TalerProtocolTimestamp.now();
await tx.tips.put(tr);
for (const cr of newCoinRecords) {
- await tx.coins.put(cr);
+ await makeCoinAvailable(ws, tx, cr);
}
});
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts
index 1b8383776..bee83265c 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -93,11 +93,11 @@ import {
} from "../util/http.js";
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
import { DbAccess, GetReadOnlyAccess } from "../util/query.js";
-import { RetryInfo } from "../util/retries.js";
import {
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
WALLET_EXCHANGE_PROTOCOL_VERSION,
} from "../versions.js";
+import { makeCoinAvailable } from "../wallet.js";
import {
getExchangeDetails,
getExchangePaytoUri,
@@ -805,7 +805,6 @@ async function processPlanchetVerifyAndStoreCoin(
reservePub: planchet.reservePub,
withdrawalGroupId: withdrawalGroup.withdrawalGroupId,
},
- suspended: false,
ageCommitmentProof: planchet.ageCommitmentProof,
};
@@ -815,7 +814,7 @@ async function processPlanchetVerifyAndStoreCoin(
// withdrawal succeeded. If so, mark the withdrawal
// group as finished.
const firstSuccess = await ws.db
- .mktx((x) => [x.coins, x.withdrawalGroups, x.planchets])
+ .mktx((x) => [x.coins, x.denominations, x.withdrawalGroups, x.planchets])
.runReadWrite(async (tx) => {
const p = await tx.planchets.get(planchetCoinPub);
if (!p || p.withdrawalDone) {
@@ -823,7 +822,7 @@ async function processPlanchetVerifyAndStoreCoin(
}
p.withdrawalDone = true;
await tx.planchets.put(p);
- await tx.coins.add(coin);
+ await makeCoinAvailable(ws, tx, coin);
return true;
});
diff --git a/packages/taler-wallet-core/src/util/query.ts b/packages/taler-wallet-core/src/util/query.ts
index 025959253..17b713659 100644
--- a/packages/taler-wallet-core/src/util/query.ts
+++ b/packages/taler-wallet-core/src/util/query.ts
@@ -445,14 +445,15 @@ function runTx<Arg, Res>(
if (!gotFunResult) {
const msg =
"BUG: transaction closed before transaction function returned";
- console.error(msg);
+ logger.error(msg);
+ logger.error(`${stack.stack}`);
reject(Error(msg));
}
resolve(funResult);
};
tx.onerror = () => {
logger.error("error in transaction");
- logger.error(`${stack}`);
+ logger.error(`${stack.stack}`);
};
tx.onabort = () => {
let msg: string;
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index 0e7772259..afbee4e64 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -99,7 +99,9 @@ import {
} from "./crypto/workers/cryptoDispatcher.js";
import {
AuditorTrustRecord,
+ CoinRecord,
CoinSourceType,
+ CoinStatus,
exportDb,
importDb,
OperationAttemptResult,
@@ -216,6 +218,7 @@ import {
HttpRequestLibrary,
readSuccessResponseJsonOrThrow,
} from "./util/http.js";
+import { checkDbInvariant } from "./util/invariants.js";
import {
AsyncCondition,
OpenedPromise,
@@ -787,21 +790,135 @@ async function getExchangeDetailedInfo(
};
}
+export async function makeCoinAvailable(
+ ws: InternalWalletState,
+ tx: GetReadWriteAccess<{
+ coins: typeof WalletStoresV1.coins;
+ denominations: typeof WalletStoresV1.denominations;
+ }>,
+ coinRecord: CoinRecord,
+): Promise<void> {
+ const denom = await tx.denominations.get([
+ coinRecord.exchangeBaseUrl,
+ coinRecord.denomPubHash,
+ ]);
+ checkDbInvariant(!!denom);
+ if (!denom.freshCoinCount) {
+ denom.freshCoinCount = 0;
+ }
+ denom.freshCoinCount++;
+ await tx.coins.put(coinRecord);
+ await tx.denominations.put(denom);
+}
+
+export interface CoinsSpendInfo {
+ coinPubs: string[];
+ contributions: AmountJson[];
+ refreshReason: RefreshReason;
+ /**
+ * Identifier for what the coin has been spent for.
+ */
+ allocationId: string;
+}
+
+export async function spendCoins(
+ ws: InternalWalletState,
+ tx: GetReadWriteAccess<{
+ coins: typeof WalletStoresV1.coins;
+ refreshGroups: typeof WalletStoresV1.refreshGroups;
+ denominations: typeof WalletStoresV1.denominations;
+ }>,
+ csi: CoinsSpendInfo,
+): Promise<void> {
+ for (let i = 0; i < csi.coinPubs.length; i++) {
+ const coin = await tx.coins.get(csi.coinPubs[i]);
+ if (!coin) {
+ throw Error("coin allocated for payment doesn't exist anymore");
+ }
+ const denom = await tx.denominations.get([
+ coin.exchangeBaseUrl,
+ coin.denomPubHash,
+ ]);
+ checkDbInvariant(!!denom);
+ const contrib = csi.contributions[i];
+ if (coin.status !== CoinStatus.Fresh) {
+ const alloc = coin.allocation;
+ if (!alloc) {
+ continue;
+ }
+ if (alloc.id !== csi.allocationId) {
+ // FIXME: assign error code
+ throw Error("conflicting coin allocation (id)");
+ }
+ if (0 !== Amounts.cmp(alloc.amount, contrib)) {
+ // FIXME: assign error code
+ throw Error("conflicting coin allocation (contrib)");
+ }
+ continue;
+ }
+ coin.status = CoinStatus.Dormant;
+ coin.allocation = {
+ id: csi.allocationId,
+ amount: Amounts.stringify(contrib),
+ };
+ const remaining = Amounts.sub(coin.currentAmount, contrib);
+ if (remaining.saturated) {
+ throw Error("not enough remaining balance on coin for payment");
+ }
+ coin.currentAmount = remaining.amount;
+ checkDbInvariant(!!denom);
+ if (denom.freshCoinCount == null || denom.freshCoinCount === 0) {
+ throw Error(`invalid coin count ${denom.freshCoinCount} in DB`);
+ }
+ denom.freshCoinCount--;
+ await tx.coins.put(coin);
+ await tx.denominations.put(denom);
+ }
+ const refreshCoinPubs = csi.coinPubs.map((x) => ({
+ coinPub: x,
+ }));
+ await createRefreshGroup(ws, tx, refreshCoinPubs, RefreshReason.PayMerchant);
+}
+
async function setCoinSuspended(
ws: InternalWalletState,
coinPub: string,
suspended: boolean,
): Promise<void> {
await ws.db
- .mktx((x) => [x.coins])
+ .mktx((x) => [x.coins, x.denominations])
.runReadWrite(async (tx) => {
const c = await tx.coins.get(coinPub);
if (!c) {
logger.warn(`coin ${coinPub} not found, won't suspend`);
return;
}
- c.suspended = suspended;
+ const denom = await tx.denominations.get([
+ c.exchangeBaseUrl,
+ c.denomPubHash,
+ ]);
+ checkDbInvariant(!!denom);
+ if (suspended) {
+ if (c.status !== CoinStatus.Fresh) {
+ return;
+ }
+ if (denom.freshCoinCount == null || denom.freshCoinCount === 0) {
+ throw Error(`invalid coin count ${denom.freshCoinCount} in DB`);
+ }
+ denom.freshCoinCount--;
+ c.status = CoinStatus.FreshSuspended;
+ } else {
+ if (c.status == CoinStatus.Dormant) {
+ return;
+ }
+ if (denom.freshCoinCount == null) {
+ denom.freshCoinCount = 0;
+ }
+ denom.freshCoinCount++;
+ c.status = CoinStatus.Fresh;
+ }
await tx.coins.put(c);
+ await tx.denominations.put(denom);
});
}
@@ -857,7 +974,7 @@ async function dumpCoins(ws: InternalWalletState): Promise<CoinDumpJson> {
refresh_parent_coin_pub: refreshParentCoinPub,
remaining_value: Amounts.stringify(c.currentAmount),
withdrawal_reserve_pub: withdrawalReservePub,
- coin_suspended: c.suspended,
+ coin_suspended: c.status === CoinStatus.FreshSuspended,
ageCommitmentProof: c.ageCommitmentProof,
});
}