wallet-core: cache fresh coin count in DB
This commit is contained in:
parent
9d044058e2
commit
c021876b41
@ -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",
|
||||
|
@ -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.
|
||||
*/
|
||||
@ -605,11 +617,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.
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
});
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
@ -347,65 +345,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,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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;
|
||||
});
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user