wallet-core: support age restrictions in new coin selection
This commit is contained in:
parent
2747bc260b
commit
b91caf977f
@ -20,7 +20,7 @@ import {
|
||||
ObjectStoreRecord,
|
||||
MemoryBackendDump,
|
||||
} from "./MemoryBackend";
|
||||
import { Event } from "./idbtypes";
|
||||
import { Event, IDBKeyRange } from "./idbtypes";
|
||||
import {
|
||||
BridgeIDBCursor,
|
||||
BridgeIDBDatabase,
|
||||
@ -89,6 +89,17 @@ export type { AccessStats } from "./MemoryBackend";
|
||||
delete Object.prototype.__magic__;
|
||||
})();
|
||||
|
||||
/**
|
||||
* Global indexeddb objects, either from the native or bridge-idb
|
||||
* implementation, depending on what is availabe in
|
||||
* the global environment.
|
||||
*/
|
||||
export const GlobalIDB: {
|
||||
KeyRange: typeof BridgeIDBKeyRange;
|
||||
} = {
|
||||
KeyRange: (globalThis as any).IDBKeyRange ?? BridgeIDBKeyRange,
|
||||
};
|
||||
|
||||
/**
|
||||
* Populate the global name space such that the given IndexedDB factory is made
|
||||
* available globally.
|
||||
|
@ -988,6 +988,11 @@ function invariant(cond: boolean): asserts cond {
|
||||
}
|
||||
|
||||
export namespace AgeRestriction {
|
||||
/**
|
||||
* Smallest age value that the protocol considers "unrestricted".
|
||||
*/
|
||||
export const AGE_UNRESTRICTED = 32;
|
||||
|
||||
export function hashCommitment(ac: AgeCommitment): HashCodeString {
|
||||
const hc = new nacl.HashState();
|
||||
for (const pub of ac.publicKeys) {
|
||||
|
@ -1226,6 +1226,7 @@ export interface RefreshPlanchetInfo {
|
||||
*/
|
||||
blindingKey: string;
|
||||
|
||||
maxAge: number;
|
||||
ageCommitmentProof?: AgeCommitmentProof;
|
||||
}
|
||||
|
||||
|
@ -1213,6 +1213,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
|
||||
coinPriv: encodeCrock(coinPriv),
|
||||
coinPub: encodeCrock(coinPub),
|
||||
coinEvHash: encodeCrock(coinEvHash),
|
||||
maxAge: req.meltCoinMaxAge,
|
||||
ageCommitmentProof: newAc,
|
||||
};
|
||||
planchets.push(planchet);
|
||||
|
@ -61,6 +61,7 @@ export interface DeriveRefreshSessionRequest {
|
||||
meltCoinPub: string;
|
||||
meltCoinPriv: string;
|
||||
meltCoinDenomPubHash: string;
|
||||
meltCoinMaxAge: number;
|
||||
meltCoinAgeCommitmentProof?: AgeCommitmentProof;
|
||||
newCoinDenoms: RefreshNewDenomInfo[];
|
||||
feeRefresh: AmountJson;
|
||||
|
@ -319,11 +319,6 @@ export interface DenominationRecord {
|
||||
* that includes this denomination.
|
||||
*/
|
||||
listIssueDate: TalerProtocolTimestamp;
|
||||
|
||||
/**
|
||||
* Number of fresh coins of this denomination that are available.
|
||||
*/
|
||||
freshCoinCount?: number;
|
||||
}
|
||||
|
||||
export namespace DenominationRecord {
|
||||
@ -546,6 +541,8 @@ export interface PlanchetRecord {
|
||||
|
||||
coinEvHash: string;
|
||||
|
||||
maxAge: number;
|
||||
|
||||
ageCommitmentProof?: AgeCommitmentProof;
|
||||
}
|
||||
|
||||
@ -674,6 +671,8 @@ export interface CoinRecord {
|
||||
*/
|
||||
allocation?: CoinAllocation;
|
||||
|
||||
maxAge: number;
|
||||
|
||||
ageCommitmentProof?: AgeCommitmentProof;
|
||||
}
|
||||
|
||||
@ -1770,7 +1769,45 @@ export interface OperationAttemptLongpollResult {
|
||||
type: OperationAttemptResultType.Longpoll;
|
||||
}
|
||||
|
||||
/**
|
||||
* Availability of coins of a given denomination (and age restriction!).
|
||||
*
|
||||
* We can't store this information with the denomination record, as one denomination
|
||||
* can be withdrawn with multiple age restrictions.
|
||||
*/
|
||||
export interface CoinAvailabilityRecord {
|
||||
currency: string;
|
||||
amountVal: number;
|
||||
amountFrac: number;
|
||||
denomPubHash: string;
|
||||
exchangeBaseUrl: string;
|
||||
|
||||
/**
|
||||
* Age restriction on the coin, or 0 for no age restriction (or
|
||||
* denomination without age restriction support).
|
||||
*/
|
||||
maxAge: number;
|
||||
|
||||
/**
|
||||
* Number of fresh coins of this denomination that are available.
|
||||
*/
|
||||
freshCoinCount: number;
|
||||
}
|
||||
|
||||
export const WalletStoresV1 = {
|
||||
coinAvailability: describeStore(
|
||||
"coinAvailability",
|
||||
describeContents<CoinAvailabilityRecord>({
|
||||
keyPath: ["exchangeBaseUrl", "denomPubHash", "maxAge"],
|
||||
}),
|
||||
{
|
||||
byExchangeAgeAvailability: describeIndex("byExchangeAgeAvailability", [
|
||||
"exchangeBaseUrl",
|
||||
"maxAge",
|
||||
"freshCoinCount",
|
||||
]),
|
||||
},
|
||||
),
|
||||
coins: describeStore(
|
||||
"coins",
|
||||
describeContents<CoinRecord>({
|
||||
@ -1779,10 +1816,10 @@ export const WalletStoresV1 = {
|
||||
{
|
||||
byBaseUrl: describeIndex("byBaseUrl", "exchangeBaseUrl"),
|
||||
byDenomPubHash: describeIndex("byDenomPubHash", "denomPubHash"),
|
||||
byDenomPubHashAndStatus: describeIndex("byDenomPubHashAndStatus", [
|
||||
"denomPubHash",
|
||||
"status",
|
||||
]),
|
||||
byExchangeDenomPubHashAndAgeAndStatus: describeIndex(
|
||||
"byExchangeDenomPubHashAndAgeAndStatus",
|
||||
["exchangeBaseUrl", "denomPubHash", "maxAge", "status"],
|
||||
),
|
||||
byCoinEvHash: describeIndex("byCoinEvHash", "coinEvHash"),
|
||||
},
|
||||
),
|
||||
|
@ -49,6 +49,7 @@ import {
|
||||
BankWithdrawDetails,
|
||||
parseWithdrawUri,
|
||||
AmountJson,
|
||||
AgeRestriction,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
|
||||
import { DenominationRecord } from "./db.js";
|
||||
@ -86,6 +87,7 @@ export interface CoinInfo {
|
||||
denomPubHash: string;
|
||||
feeDeposit: string;
|
||||
feeRefresh: string;
|
||||
maxAge: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -200,6 +202,7 @@ export async function withdrawCoin(args: {
|
||||
feeDeposit: Amounts.stringify(denom.fees.feeDeposit),
|
||||
feeRefresh: Amounts.stringify(denom.fees.feeRefresh),
|
||||
exchangeBaseUrl: args.exchangeBaseUrl,
|
||||
maxAge: AgeRestriction.AGE_UNRESTRICTED,
|
||||
};
|
||||
}
|
||||
|
||||
@ -298,6 +301,7 @@ export async function refreshCoin(req: {
|
||||
value: x.amountVal,
|
||||
},
|
||||
})),
|
||||
meltCoinMaxAge: oldCoin.maxAge,
|
||||
});
|
||||
|
||||
const meltReqBody: ExchangeMeltRequest = {
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
AgeRestriction,
|
||||
AmountJson,
|
||||
Amounts,
|
||||
BackupCoinSourceType,
|
||||
@ -436,6 +437,8 @@ export async function importBackup(
|
||||
? CoinStatus.Fresh
|
||||
: CoinStatus.Dormant,
|
||||
coinSource,
|
||||
// FIXME!
|
||||
maxAge: AgeRestriction.AGE_UNRESTRICTED,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -51,16 +51,14 @@ import {
|
||||
OperationStatus,
|
||||
} from "../db.js";
|
||||
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||
import { selectPayCoinsLegacy } from "../util/coinSelection.js";
|
||||
import { readSuccessResponseJsonOrThrow } from "../util/http.js";
|
||||
import { spendCoins } from "../wallet.js";
|
||||
import { getExchangeDetails } from "./exchanges.js";
|
||||
import {
|
||||
CoinSelectionRequest,
|
||||
extractContractData,
|
||||
generateDepositPermissions,
|
||||
getCandidatePayCoins,
|
||||
getTotalPaymentCost,
|
||||
selectPayCoinsNew,
|
||||
} from "./pay.js";
|
||||
import { getTotalRefreshCost } from "./refresh.js";
|
||||
import { makeEventId } from "./transactions.js";
|
||||
@ -255,28 +253,17 @@ export async function getFeeForDeposit(
|
||||
}
|
||||
});
|
||||
|
||||
const csr: CoinSelectionRequest = {
|
||||
allowedAuditors: [],
|
||||
allowedExchanges: Object.values(exchangeInfos).map((v) => ({
|
||||
const payCoinSel = await selectPayCoinsNew(ws, {
|
||||
auditors: [],
|
||||
exchanges: Object.values(exchangeInfos).map((v) => ({
|
||||
exchangeBaseUrl: v.url,
|
||||
exchangePub: v.master_pub,
|
||||
})),
|
||||
amount: Amounts.parseOrThrow(req.amount),
|
||||
maxDepositFee: Amounts.parseOrThrow(req.amount),
|
||||
maxWireFee: Amounts.parseOrThrow(req.amount),
|
||||
timestamp: TalerProtocolTimestamp.now(),
|
||||
wireFeeAmortization: 1,
|
||||
wireMethod: p.targetType,
|
||||
};
|
||||
|
||||
const candidates = await getCandidatePayCoins(ws, csr);
|
||||
|
||||
const payCoinSel = selectPayCoinsLegacy({
|
||||
candidates,
|
||||
contractTermsAmount: csr.amount,
|
||||
depositFeeLimit: csr.maxDepositFee,
|
||||
wireFeeAmortization: csr.wireFeeAmortization,
|
||||
wireFeeLimit: csr.maxWireFee,
|
||||
contractTermsAmount: Amounts.parseOrThrow(req.amount),
|
||||
depositFeeLimit: Amounts.parseOrThrow(req.amount),
|
||||
wireFeeAmortization: 1,
|
||||
wireFeeLimit: Amounts.parseOrThrow(req.amount),
|
||||
prevPayCoins: [],
|
||||
});
|
||||
|
||||
@ -356,19 +343,10 @@ export async function prepareDepositGroup(
|
||||
"",
|
||||
);
|
||||
|
||||
const candidates = await getCandidatePayCoins(ws, {
|
||||
allowedAuditors: contractData.allowedAuditors,
|
||||
allowedExchanges: contractData.allowedExchanges,
|
||||
amount: contractData.amount,
|
||||
maxDepositFee: contractData.maxDepositFee,
|
||||
maxWireFee: contractData.maxWireFee,
|
||||
timestamp: contractData.timestamp,
|
||||
wireFeeAmortization: contractData.wireFeeAmortization,
|
||||
const payCoinSel = await selectPayCoinsNew(ws, {
|
||||
auditors: contractData.allowedAuditors,
|
||||
exchanges: contractData.allowedExchanges,
|
||||
wireMethod: contractData.wireMethod,
|
||||
});
|
||||
|
||||
const payCoinSel = selectPayCoinsLegacy({
|
||||
candidates,
|
||||
contractTermsAmount: contractData.amount,
|
||||
depositFeeLimit: contractData.maxDepositFee,
|
||||
wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
|
||||
@ -459,19 +437,10 @@ export async function createDepositGroup(
|
||||
"",
|
||||
);
|
||||
|
||||
const candidates = await getCandidatePayCoins(ws, {
|
||||
allowedAuditors: contractData.allowedAuditors,
|
||||
allowedExchanges: contractData.allowedExchanges,
|
||||
amount: contractData.amount,
|
||||
maxDepositFee: contractData.maxDepositFee,
|
||||
maxWireFee: contractData.maxWireFee,
|
||||
timestamp: contractData.timestamp,
|
||||
wireFeeAmortization: contractData.wireFeeAmortization,
|
||||
const payCoinSel = await selectPayCoinsNew(ws, {
|
||||
auditors: contractData.allowedAuditors,
|
||||
exchanges: contractData.allowedExchanges,
|
||||
wireMethod: contractData.wireMethod,
|
||||
});
|
||||
|
||||
const payCoinSel = selectPayCoinsLegacy({
|
||||
candidates,
|
||||
contractTermsAmount: contractData.amount,
|
||||
depositFeeLimit: contractData.maxDepositFee,
|
||||
wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
|
||||
@ -522,6 +491,7 @@ export async function createDepositGroup(
|
||||
x.recoupGroups,
|
||||
x.denominations,
|
||||
x.refreshGroups,
|
||||
x.coinAvailability,
|
||||
])
|
||||
.runReadWrite(async (tx) => {
|
||||
await spendCoins(ws, tx, {
|
||||
|
@ -24,6 +24,7 @@
|
||||
/**
|
||||
* Imports.
|
||||
*/
|
||||
import { BridgeIDBKeyRange, GlobalIDB } from "@gnu-taler/idb-bridge";
|
||||
import {
|
||||
AbsoluteTime,
|
||||
AgeRestriction,
|
||||
@ -102,7 +103,7 @@ import {
|
||||
readUnexpectedResponseDetails,
|
||||
throwUnexpectedRequestError,
|
||||
} from "../util/http.js";
|
||||
import { checkLogicInvariant } from "../util/invariants.js";
|
||||
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
|
||||
import { GetReadWriteAccess } from "../util/query.js";
|
||||
import { RetryInfo, RetryTags, scheduleRetry } from "../util/retries.js";
|
||||
import { spendCoins } from "../wallet.js";
|
||||
@ -215,149 +216,6 @@ export interface CoinSelectionRequest {
|
||||
minimumAge?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get candidate coins. From these candidate coins,
|
||||
* the actual contributions will be computed later.
|
||||
*
|
||||
* The resulting candidate coin list is sorted deterministically.
|
||||
*
|
||||
* TODO: Exclude more coins:
|
||||
* - when we already have a coin with more remaining amount than
|
||||
* the payment amount, coins with even higher amounts can be skipped.
|
||||
*/
|
||||
export async function getCandidatePayCoins(
|
||||
ws: InternalWalletState,
|
||||
req: CoinSelectionRequest,
|
||||
): Promise<CoinCandidateSelection> {
|
||||
const candidateCoins: AvailableCoinInfo[] = [];
|
||||
const wireFeesPerExchange: Record<string, AmountJson> = {};
|
||||
|
||||
await ws.db
|
||||
.mktx((x) => [x.exchanges, x.exchangeDetails, x.denominations, x.coins])
|
||||
.runReadOnly(async (tx) => {
|
||||
const exchanges = await tx.exchanges.iter().toArray();
|
||||
for (const exchange of exchanges) {
|
||||
let isOkay = false;
|
||||
const exchangeDetails = await getExchangeDetails(tx, exchange.baseUrl);
|
||||
if (!exchangeDetails) {
|
||||
continue;
|
||||
}
|
||||
const exchangeFees = exchangeDetails.wireInfo;
|
||||
if (!exchangeFees) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const wireTypes = new Set<string>();
|
||||
for (const acc of exchangeDetails.wireInfo.accounts) {
|
||||
const p = parsePaytoUri(acc.payto_uri);
|
||||
if (p) {
|
||||
wireTypes.add(p.targetType);
|
||||
}
|
||||
}
|
||||
|
||||
if (!wireTypes.has(req.wireMethod)) {
|
||||
// Exchange can't be used, because it doesn't support
|
||||
// the wire type that the merchant requested.
|
||||
continue;
|
||||
}
|
||||
|
||||
// is the exchange explicitly allowed?
|
||||
for (const allowedExchange of req.allowedExchanges) {
|
||||
if (allowedExchange.exchangePub === exchangeDetails.masterPublicKey) {
|
||||
isOkay = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// is the exchange allowed because of one of its auditors?
|
||||
if (!isOkay) {
|
||||
for (const allowedAuditor of req.allowedAuditors) {
|
||||
for (const auditor of exchangeDetails.auditors) {
|
||||
if (auditor.auditor_pub === allowedAuditor.auditorPub) {
|
||||
isOkay = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isOkay) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isOkay) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const coins = await tx.coins.indexes.byBaseUrl
|
||||
.iter(exchange.baseUrl)
|
||||
.toArray();
|
||||
|
||||
if (!coins || coins.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Denomination of the first coin, we assume that all other
|
||||
// coins have the same currency
|
||||
const firstDenom = await ws.getDenomInfo(
|
||||
ws,
|
||||
tx,
|
||||
exchange.baseUrl,
|
||||
coins[0].denomPubHash,
|
||||
);
|
||||
if (!firstDenom) {
|
||||
throw Error("db inconsistent");
|
||||
}
|
||||
const currency = firstDenom.value.currency;
|
||||
for (const coin of coins) {
|
||||
const denom = await tx.denominations.get([
|
||||
exchange.baseUrl,
|
||||
coin.denomPubHash,
|
||||
]);
|
||||
if (!denom) {
|
||||
throw Error("db inconsistent");
|
||||
}
|
||||
if (denom.currency !== currency) {
|
||||
logger.warn(
|
||||
`same pubkey for different currencies at exchange ${exchange.baseUrl}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!isSpendableCoin(coin, denom)) {
|
||||
continue;
|
||||
}
|
||||
candidateCoins.push({
|
||||
availableAmount: coin.currentAmount,
|
||||
value: DenominationRecord.getValue(denom),
|
||||
coinPub: coin.coinPub,
|
||||
denomPub: denom.denomPub,
|
||||
feeDeposit: denom.fees.feeDeposit,
|
||||
exchangeBaseUrl: denom.exchangeBaseUrl,
|
||||
ageCommitmentProof: coin.ageCommitmentProof,
|
||||
});
|
||||
}
|
||||
|
||||
let wireFee: AmountJson | undefined;
|
||||
for (const fee of exchangeFees.feesForType[req.wireMethod] || []) {
|
||||
if (
|
||||
fee.startStamp <= req.timestamp &&
|
||||
fee.endStamp >= req.timestamp
|
||||
) {
|
||||
wireFee = fee.wireFee;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (wireFee) {
|
||||
wireFeesPerExchange[exchange.baseUrl] = wireFee;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
candidateCoins,
|
||||
wireFeesPerExchange,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Record all information that is necessary to
|
||||
* pay for a proposal in the wallet's database.
|
||||
@ -412,6 +270,7 @@ async function recordConfirmPay(
|
||||
x.coins,
|
||||
x.refreshGroups,
|
||||
x.denominations,
|
||||
x.coinAvailability,
|
||||
])
|
||||
.runReadWrite(async (tx) => {
|
||||
const p = await tx.proposals.get(proposal.proposalId);
|
||||
@ -976,7 +835,13 @@ async function handleInsufficientFunds(
|
||||
logger.trace("re-selected coins");
|
||||
|
||||
await ws.db
|
||||
.mktx((x) => [x.purchases, x.coins, x.denominations, x.refreshGroups])
|
||||
.mktx((x) => [
|
||||
x.purchases,
|
||||
x.coins,
|
||||
x.coinAvailability,
|
||||
x.denominations,
|
||||
x.refreshGroups,
|
||||
])
|
||||
.runReadWrite(async (tx) => {
|
||||
const p = await tx.purchases.get(proposalId);
|
||||
if (!p) {
|
||||
@ -1029,6 +894,7 @@ export interface SelectPayCoinRequestNg {
|
||||
}
|
||||
|
||||
export type AvailableDenom = DenominationInfo & {
|
||||
maxAge: number;
|
||||
numAvailable: number;
|
||||
};
|
||||
|
||||
@ -1037,7 +903,12 @@ async function selectCandidates(
|
||||
req: SelectPayCoinRequestNg,
|
||||
): Promise<[AvailableDenom[], Record<string, AmountJson>]> {
|
||||
return await ws.db
|
||||
.mktx((x) => [x.exchanges, x.exchangeDetails, x.denominations])
|
||||
.mktx((x) => [
|
||||
x.exchanges,
|
||||
x.exchangeDetails,
|
||||
x.denominations,
|
||||
x.coinAvailability,
|
||||
])
|
||||
.runReadOnly(async (tx) => {
|
||||
const denoms: AvailableDenom[] = [];
|
||||
const exchanges = await tx.exchanges.iter().toArray();
|
||||
@ -1065,17 +936,35 @@ async function selectCandidates(
|
||||
if (!accepted) {
|
||||
continue;
|
||||
}
|
||||
// FIXME: Do this query more efficiently via indexing
|
||||
const exchangeDenoms = await tx.denominations.indexes.byExchangeBaseUrl
|
||||
.iter(exchangeDetails.exchangeBaseUrl)
|
||||
.filter((x) => x.freshCoinCount != null && x.freshCoinCount > 0);
|
||||
let ageLower = 0;
|
||||
let ageUpper = Number.MAX_SAFE_INTEGER;
|
||||
if (req.requiredMinimumAge) {
|
||||
ageLower = req.requiredMinimumAge;
|
||||
}
|
||||
const myExchangeDenoms =
|
||||
await tx.coinAvailability.indexes.byExchangeAgeAvailability.getAll(
|
||||
GlobalIDB.KeyRange.bound(
|
||||
[exchangeDetails.exchangeBaseUrl, ageLower, 1],
|
||||
[
|
||||
exchangeDetails.exchangeBaseUrl,
|
||||
ageUpper,
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
],
|
||||
),
|
||||
);
|
||||
// FIXME: Check that the individual denomination is audited!
|
||||
// FIXME: Should we exclude denominations that are
|
||||
// not spendable anymore?
|
||||
for (const denom of exchangeDenoms) {
|
||||
for (const denomAvail of myExchangeDenoms) {
|
||||
const denom = await tx.denominations.get([
|
||||
denomAvail.exchangeBaseUrl,
|
||||
denomAvail.denomPubHash,
|
||||
]);
|
||||
checkDbInvariant(!!denom);
|
||||
denoms.push({
|
||||
...DenominationRecord.toDenomInfo(denom),
|
||||
numAvailable: denom.freshCoinCount ?? 0,
|
||||
numAvailable: denomAvail.freshCoinCount ?? 0,
|
||||
maxAge: denomAvail.maxAge,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1092,15 +981,28 @@ async function selectCandidates(
|
||||
});
|
||||
}
|
||||
|
||||
function makeAvailabilityKey(
|
||||
exchangeBaseUrl: string,
|
||||
denomPubHash: string,
|
||||
maxAge: number,
|
||||
): string {
|
||||
return `${denomPubHash};${maxAge};${exchangeBaseUrl}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selection result.
|
||||
*/
|
||||
interface SelResult {
|
||||
/**
|
||||
* Map from denomination public key hashes
|
||||
* Map from an availability key
|
||||
* to an array of contributions.
|
||||
*/
|
||||
[dph: string]: AmountJson[];
|
||||
[avKey: string]: {
|
||||
exchangeBaseUrl: string;
|
||||
denomPubHash: string;
|
||||
maxAge: number;
|
||||
contributions: AmountJson[];
|
||||
};
|
||||
}
|
||||
|
||||
export function selectGreedy(
|
||||
@ -1146,7 +1048,22 @@ export function selectGreedy(
|
||||
}
|
||||
|
||||
if (contributions.length) {
|
||||
selectedDenom[aci.denomPubHash] = contributions;
|
||||
const avKey = makeAvailabilityKey(
|
||||
aci.exchangeBaseUrl,
|
||||
aci.denomPubHash,
|
||||
aci.maxAge,
|
||||
);
|
||||
let sd = selectedDenom[avKey];
|
||||
if (!sd) {
|
||||
sd = {
|
||||
contributions: [],
|
||||
denomPubHash: aci.denomPubHash,
|
||||
exchangeBaseUrl: aci.exchangeBaseUrl,
|
||||
maxAge: aci.maxAge,
|
||||
};
|
||||
}
|
||||
sd.contributions.push(...contributions);
|
||||
selectedDenom[avKey] = sd;
|
||||
}
|
||||
|
||||
if (Amounts.isZero(tally.amountPayRemaining)) {
|
||||
@ -1173,9 +1090,22 @@ export function selectForced(
|
||||
}
|
||||
if (Amounts.cmp(aci.value, forcedCoin.value) === 0) {
|
||||
aci.numAvailable--;
|
||||
const contributions = selectedDenom[aci.denomPubHash] ?? [];
|
||||
contributions.push(Amounts.parseOrThrow(forcedCoin.value));
|
||||
selectedDenom[aci.denomPubHash] = contributions;
|
||||
const avKey = makeAvailabilityKey(
|
||||
aci.exchangeBaseUrl,
|
||||
aci.denomPubHash,
|
||||
aci.maxAge,
|
||||
);
|
||||
let sd = selectedDenom[avKey];
|
||||
if (!sd) {
|
||||
sd = {
|
||||
contributions: [],
|
||||
denomPubHash: aci.denomPubHash,
|
||||
exchangeBaseUrl: aci.exchangeBaseUrl,
|
||||
maxAge: aci.maxAge,
|
||||
};
|
||||
}
|
||||
sd.contributions.push(Amounts.parseOrThrow(forcedCoin.value));
|
||||
selectedDenom[avKey] = sd;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
@ -1273,18 +1203,27 @@ export async function selectPayCoinsNew(
|
||||
.mktx((x) => [x.coins, x.denominations])
|
||||
.runReadOnly(async (tx) => {
|
||||
for (const dph of Object.keys(finalSel)) {
|
||||
const contributions = finalSel[dph];
|
||||
const coins = await tx.coins.indexes.byDenomPubHashAndStatus.getAll(
|
||||
[dph, CoinStatus.Fresh],
|
||||
contributions.length,
|
||||
);
|
||||
if (coins.length != contributions.length) {
|
||||
const selInfo = finalSel[dph];
|
||||
const numRequested = selInfo.contributions.length;
|
||||
const query = [
|
||||
selInfo.exchangeBaseUrl,
|
||||
selInfo.denomPubHash,
|
||||
selInfo.maxAge,
|
||||
CoinStatus.Fresh,
|
||||
];
|
||||
logger.info(`query: ${j2s(query)}`);
|
||||
const coins =
|
||||
await tx.coins.indexes.byExchangeDenomPubHashAndAgeAndStatus.getAll(
|
||||
query,
|
||||
numRequested,
|
||||
);
|
||||
if (coins.length != numRequested) {
|
||||
throw Error(
|
||||
`coin selection failed (not available anymore, got only ${coins.length}/${contributions.length})`,
|
||||
`coin selection failed (not available anymore, got only ${coins.length}/${numRequested})`,
|
||||
);
|
||||
}
|
||||
coinPubs.push(...coins.map((x) => x.coinPub));
|
||||
coinContributions.push(...contributions);
|
||||
coinContributions.push(...selInfo.contributions);
|
||||
}
|
||||
});
|
||||
|
||||
@ -1535,7 +1474,7 @@ export async function generateDepositPermissions(
|
||||
let wireInfoHash: string;
|
||||
wireInfoHash = contractData.wireInfoHash;
|
||||
logger.trace(
|
||||
`signing deposit permission for coin with acp=${j2s(
|
||||
`signing deposit permission for coin with ageRestriction=${j2s(
|
||||
coin.ageCommitmentProof,
|
||||
)}`,
|
||||
);
|
||||
|
@ -118,7 +118,8 @@ interface CoinInfo {
|
||||
|
||||
denomSig: UnblindedSignature;
|
||||
|
||||
ageCommitmentProof: AgeCommitmentProof | undefined;
|
||||
maxAge: number;
|
||||
ageCommitmentProof?: AgeCommitmentProof;
|
||||
}
|
||||
|
||||
export async function selectPeerCoins(
|
||||
@ -156,6 +157,7 @@ export async function selectPeerCoins(
|
||||
denomPubHash: denom.denomPubHash,
|
||||
coinPriv: coin.coinPriv,
|
||||
denomSig: coin.denomSig,
|
||||
maxAge: coin.maxAge,
|
||||
ageCommitmentProof: coin.ageCommitmentProof,
|
||||
});
|
||||
}
|
||||
@ -245,6 +247,7 @@ export async function initiatePeerToPeerPush(
|
||||
.mktx((x) => [
|
||||
x.exchanges,
|
||||
x.coins,
|
||||
x.coinAvailability,
|
||||
x.denominations,
|
||||
x.refreshGroups,
|
||||
x.peerPullPaymentInitiations,
|
||||
@ -583,6 +586,7 @@ export async function acceptPeerPullPayment(
|
||||
x.denominations,
|
||||
x.refreshGroups,
|
||||
x.peerPullPaymentIncoming,
|
||||
x.coinAvailability,
|
||||
])
|
||||
.runReadWrite(async (tx) => {
|
||||
const sel = await selectPeerCoins(ws, tx, instructedAmount);
|
||||
|
@ -392,7 +392,13 @@ export async function processRecoupGroupHandler(
|
||||
}
|
||||
|
||||
await ws.db
|
||||
.mktx((x) => [x.recoupGroups, x.denominations, x.refreshGroups, x.coins])
|
||||
.mktx((x) => [
|
||||
x.recoupGroups,
|
||||
x.coinAvailability,
|
||||
x.denominations,
|
||||
x.refreshGroups,
|
||||
x.coins,
|
||||
])
|
||||
.runReadWrite(async (tx) => {
|
||||
const rg2 = await tx.recoupGroups.get(recoupGroupId);
|
||||
if (!rg2) {
|
||||
|
@ -77,7 +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 { makeCoinAvailable, Wallet } from "../wallet.js";
|
||||
import { guardOperationException } from "./common.js";
|
||||
import { updateExchangeFromUrl } from "./exchanges.js";
|
||||
import {
|
||||
@ -368,6 +368,7 @@ async function refreshMelt(
|
||||
meltCoinPriv: oldCoin.coinPriv,
|
||||
meltCoinPub: oldCoin.coinPub,
|
||||
feeRefresh: oldDenom.feeRefresh,
|
||||
meltCoinMaxAge: oldCoin.maxAge,
|
||||
meltCoinAgeCommitmentProof: oldCoin.ageCommitmentProof,
|
||||
newCoinDenoms,
|
||||
sessionSecretSeed: refreshSession.sessionSecretSeed,
|
||||
@ -614,6 +615,7 @@ async function refreshReveal(
|
||||
meltCoinPub: oldCoin.coinPub,
|
||||
feeRefresh: oldDenom.feeRefresh,
|
||||
newCoinDenoms,
|
||||
meltCoinMaxAge: oldCoin.maxAge,
|
||||
meltCoinAgeCommitmentProof: oldCoin.ageCommitmentProof,
|
||||
sessionSecretSeed: refreshSession.sessionSecretSeed,
|
||||
});
|
||||
@ -676,6 +678,7 @@ async function refreshReveal(
|
||||
oldCoinPub: refreshGroup.oldCoinPubs[coinIndex],
|
||||
},
|
||||
coinEvHash: pc.coinEvHash,
|
||||
maxAge: pc.maxAge,
|
||||
ageCommitmentProof: pc.ageCommitmentProof,
|
||||
};
|
||||
|
||||
@ -684,7 +687,12 @@ async function refreshReveal(
|
||||
}
|
||||
|
||||
await ws.db
|
||||
.mktx((x) => [x.coins, x.denominations, x.refreshGroups])
|
||||
.mktx((x) => [
|
||||
x.coins,
|
||||
x.denominations,
|
||||
x.coinAvailability,
|
||||
x.refreshGroups,
|
||||
])
|
||||
.runReadWrite(async (tx) => {
|
||||
const rg = await tx.refreshGroups.get(refreshGroupId);
|
||||
if (!rg) {
|
||||
@ -830,6 +838,7 @@ export async function createRefreshGroup(
|
||||
denominations: typeof WalletStoresV1.denominations;
|
||||
coins: typeof WalletStoresV1.coins;
|
||||
refreshGroups: typeof WalletStoresV1.refreshGroups;
|
||||
coinAvailability: typeof WalletStoresV1.coinAvailability;
|
||||
}>,
|
||||
oldCoinPubs: CoinPublicKey[],
|
||||
reason: RefreshReason,
|
||||
@ -871,16 +880,15 @@ export async function createRefreshGroup(
|
||||
);
|
||||
if (coin.status !== CoinStatus.Dormant) {
|
||||
coin.status = CoinStatus.Dormant;
|
||||
const denom = await tx.denominations.get([
|
||||
const coinAv = await tx.coinAvailability.get([
|
||||
coin.exchangeBaseUrl,
|
||||
coin.denomPubHash,
|
||||
coin.maxAge,
|
||||
]);
|
||||
checkDbInvariant(!!denom);
|
||||
checkDbInvariant(
|
||||
denom.freshCoinCount != null && denom.freshCoinCount > 0,
|
||||
);
|
||||
denom.freshCoinCount--;
|
||||
await tx.denominations.put(denom);
|
||||
checkDbInvariant(!!coinAv);
|
||||
checkDbInvariant(coinAv.freshCoinCount > 0);
|
||||
coinAv.freshCoinCount--;
|
||||
await tx.coinAvailability.put(coinAv);
|
||||
}
|
||||
const refreshAmount = coin.currentAmount;
|
||||
inputPerCoin.push(refreshAmount);
|
||||
@ -967,7 +975,13 @@ export async function autoRefresh(
|
||||
durationFromSpec({ days: 1 }),
|
||||
);
|
||||
await ws.db
|
||||
.mktx((x) => [x.coins, x.denominations, x.refreshGroups, x.exchanges])
|
||||
.mktx((x) => [
|
||||
x.coins,
|
||||
x.denominations,
|
||||
x.coinAvailability,
|
||||
x.refreshGroups,
|
||||
x.exchanges,
|
||||
])
|
||||
.runReadWrite(async (tx) => {
|
||||
const exchange = await tx.exchanges.get(exchangeBaseUrl);
|
||||
if (!exchange) {
|
||||
|
@ -336,7 +336,13 @@ async function acceptRefunds(
|
||||
const now = TalerProtocolTimestamp.now();
|
||||
|
||||
await ws.db
|
||||
.mktx((x) => [x.purchases, x.coins, x.denominations, x.refreshGroups])
|
||||
.mktx((x) => [
|
||||
x.purchases,
|
||||
x.coins,
|
||||
x.coinAvailability,
|
||||
x.denominations,
|
||||
x.refreshGroups,
|
||||
])
|
||||
.runReadWrite(async (tx) => {
|
||||
const p = await tx.purchases.get(proposalId);
|
||||
if (!p) {
|
||||
|
@ -18,6 +18,7 @@
|
||||
* Imports.
|
||||
*/
|
||||
import {
|
||||
AgeRestriction,
|
||||
AcceptTipResponse,
|
||||
Amounts,
|
||||
BlindedDenominationSignature,
|
||||
@ -315,11 +316,12 @@ export async function processTip(
|
||||
exchangeBaseUrl: tipRecord.exchangeBaseUrl,
|
||||
status: CoinStatus.Fresh,
|
||||
coinEvHash: planchet.coinEvHash,
|
||||
maxAge: AgeRestriction.AGE_UNRESTRICTED,
|
||||
});
|
||||
}
|
||||
|
||||
await ws.db
|
||||
.mktx((x) => [x.coins, x.denominations, x.tips])
|
||||
.mktx((x) => [x.coins, x.coinAvailability, x.denominations, x.tips])
|
||||
.runReadWrite(async (tx) => {
|
||||
const tr = await tx.tips.get(walletTipId);
|
||||
if (!tr) {
|
||||
|
@ -22,6 +22,7 @@ import {
|
||||
AcceptManualWithdrawalResult,
|
||||
AcceptWithdrawalResponse,
|
||||
addPaytoQueryParams,
|
||||
AgeRestriction,
|
||||
AmountJson,
|
||||
AmountLike,
|
||||
Amounts,
|
||||
@ -510,6 +511,7 @@ async function processPlanchetGenerate(
|
||||
withdrawalDone: false,
|
||||
withdrawSig: r.withdrawSig,
|
||||
withdrawalGroupId: withdrawalGroup.withdrawalGroupId,
|
||||
maxAge: withdrawalGroup.restrictAge ?? AgeRestriction.AGE_UNRESTRICTED,
|
||||
ageCommitmentProof: r.ageCommitmentProof,
|
||||
lastError: undefined,
|
||||
};
|
||||
@ -823,6 +825,7 @@ async function processPlanchetVerifyAndStoreCoin(
|
||||
reservePub: planchet.reservePub,
|
||||
withdrawalGroupId: withdrawalGroup.withdrawalGroupId,
|
||||
},
|
||||
maxAge: planchet.maxAge,
|
||||
ageCommitmentProof: planchet.ageCommitmentProof,
|
||||
};
|
||||
|
||||
@ -832,7 +835,13 @@ async function processPlanchetVerifyAndStoreCoin(
|
||||
// withdrawal succeeded. If so, mark the withdrawal
|
||||
// group as finished.
|
||||
const firstSuccess = await ws.db
|
||||
.mktx((x) => [x.coins, x.denominations, x.withdrawalGroups, x.planchets])
|
||||
.mktx((x) => [
|
||||
x.coins,
|
||||
x.denominations,
|
||||
x.coinAvailability,
|
||||
x.withdrawalGroups,
|
||||
x.planchets,
|
||||
])
|
||||
.runReadWrite(async (tx) => {
|
||||
const p = await tx.planchets.get(planchetCoinPub);
|
||||
if (!p || p.withdrawalDone) {
|
||||
|
@ -18,7 +18,12 @@
|
||||
* Imports.
|
||||
*/
|
||||
import test from "ava";
|
||||
import { AmountJson, Amounts, DenomKeyType } from "@gnu-taler/taler-util";
|
||||
import {
|
||||
AgeRestriction,
|
||||
AmountJson,
|
||||
Amounts,
|
||||
DenomKeyType,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { AvailableCoinInfo, selectPayCoinsLegacy } from "./coinSelection.js";
|
||||
|
||||
function a(x: string): AmountJson {
|
||||
@ -41,10 +46,14 @@ function fakeAci(current: string, feeDeposit: string): AvailableCoinInfo {
|
||||
},
|
||||
feeDeposit: a(feeDeposit),
|
||||
exchangeBaseUrl: "https://example.com/",
|
||||
maxAge: AgeRestriction.AGE_UNRESTRICTED,
|
||||
};
|
||||
}
|
||||
|
||||
function fakeAciWithAgeRestriction(current: string, feeDeposit: string): AvailableCoinInfo {
|
||||
function fakeAciWithAgeRestriction(
|
||||
current: string,
|
||||
feeDeposit: string,
|
||||
): AvailableCoinInfo {
|
||||
return {
|
||||
value: a(current),
|
||||
availableAmount: a(current),
|
||||
@ -56,6 +65,7 @@ function fakeAciWithAgeRestriction(current: string, feeDeposit: string): Availab
|
||||
},
|
||||
feeDeposit: a(feeDeposit),
|
||||
exchangeBaseUrl: "https://example.com/",
|
||||
maxAge: AgeRestriction.AGE_UNRESTRICTED,
|
||||
};
|
||||
}
|
||||
|
||||
@ -284,7 +294,6 @@ test("coin selection 9", (t) => {
|
||||
t.pass();
|
||||
});
|
||||
|
||||
|
||||
test("it should be able to use unrestricted coins for age restricted contract", (t) => {
|
||||
const acis: AvailableCoinInfo[] = [
|
||||
fakeAciWithAgeRestriction("EUR:1.0", "EUR:0.2"),
|
||||
@ -299,7 +308,7 @@ test("it should be able to use unrestricted coins for age restricted contract",
|
||||
depositFeeLimit: a("EUR:0.4"),
|
||||
wireFeeLimit: a("EUR:0"),
|
||||
wireFeeAmortization: 1,
|
||||
requiredMinimumAge: 13
|
||||
requiredMinimumAge: 13,
|
||||
});
|
||||
if (!res) {
|
||||
t.fail();
|
||||
|
@ -72,6 +72,7 @@ export interface AvailableCoinInfo {
|
||||
|
||||
exchangeBaseUrl: string;
|
||||
|
||||
maxAge: number;
|
||||
ageCommitmentProof?: AgeCommitmentProof;
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,7 @@ import {
|
||||
IDBVersionChangeEvent,
|
||||
IDBCursor,
|
||||
IDBKeyPath,
|
||||
IDBKeyRange,
|
||||
} from "@gnu-taler/idb-bridge";
|
||||
import { Logger } from "@gnu-taler/taler-util";
|
||||
import { performanceNow } from "./timer.js";
|
||||
@ -309,9 +310,12 @@ export function describeIndex(
|
||||
}
|
||||
|
||||
interface IndexReadOnlyAccessor<RecordType> {
|
||||
iter(query?: IDBValidKey): ResultStream<RecordType>;
|
||||
iter(query?: IDBKeyRange | IDBValidKey): ResultStream<RecordType>;
|
||||
get(query: IDBValidKey): Promise<RecordType | undefined>;
|
||||
getAll(query: IDBValidKey, count?: number): Promise<RecordType[]>;
|
||||
getAll(
|
||||
query: IDBKeyRange | IDBValidKey,
|
||||
count?: number,
|
||||
): Promise<RecordType[]>;
|
||||
}
|
||||
|
||||
type GetIndexReadOnlyAccess<RecordType, IndexMap> = {
|
||||
@ -319,9 +323,12 @@ type GetIndexReadOnlyAccess<RecordType, IndexMap> = {
|
||||
};
|
||||
|
||||
interface IndexReadWriteAccessor<RecordType> {
|
||||
iter(query: IDBValidKey): ResultStream<RecordType>;
|
||||
iter(query: IDBKeyRange | IDBValidKey): ResultStream<RecordType>;
|
||||
get(query: IDBValidKey): Promise<RecordType | undefined>;
|
||||
getAll(query: IDBValidKey, count?: number): Promise<RecordType[]>;
|
||||
getAll(
|
||||
query: IDBKeyRange | IDBValidKey,
|
||||
count?: number,
|
||||
): Promise<RecordType[]>;
|
||||
}
|
||||
|
||||
type GetIndexReadWriteAccess<RecordType, IndexMap> = {
|
||||
|
@ -802,6 +802,7 @@ export async function makeCoinAvailable(
|
||||
ws: InternalWalletState,
|
||||
tx: GetReadWriteAccess<{
|
||||
coins: typeof WalletStoresV1.coins;
|
||||
coinAvailability: typeof WalletStoresV1.coinAvailability;
|
||||
denominations: typeof WalletStoresV1.denominations;
|
||||
}>,
|
||||
coinRecord: CoinRecord,
|
||||
@ -811,12 +812,26 @@ export async function makeCoinAvailable(
|
||||
coinRecord.denomPubHash,
|
||||
]);
|
||||
checkDbInvariant(!!denom);
|
||||
if (!denom.freshCoinCount) {
|
||||
denom.freshCoinCount = 0;
|
||||
const ageRestriction = coinRecord.maxAge;
|
||||
let car = await tx.coinAvailability.get([
|
||||
coinRecord.exchangeBaseUrl,
|
||||
coinRecord.denomPubHash,
|
||||
ageRestriction,
|
||||
]);
|
||||
if (!car) {
|
||||
car = {
|
||||
maxAge: ageRestriction,
|
||||
amountFrac: denom.amountFrac,
|
||||
amountVal: denom.amountVal,
|
||||
currency: denom.currency,
|
||||
denomPubHash: denom.denomPubHash,
|
||||
exchangeBaseUrl: denom.exchangeBaseUrl,
|
||||
freshCoinCount: 0,
|
||||
};
|
||||
}
|
||||
denom.freshCoinCount++;
|
||||
car.freshCoinCount++;
|
||||
await tx.coins.put(coinRecord);
|
||||
await tx.denominations.put(denom);
|
||||
await tx.coinAvailability.put(car);
|
||||
}
|
||||
|
||||
export interface CoinsSpendInfo {
|
||||
@ -833,6 +848,7 @@ export async function spendCoins(
|
||||
ws: InternalWalletState,
|
||||
tx: GetReadWriteAccess<{
|
||||
coins: typeof WalletStoresV1.coins;
|
||||
coinAvailability: typeof WalletStoresV1.coinAvailability;
|
||||
refreshGroups: typeof WalletStoresV1.refreshGroups;
|
||||
denominations: typeof WalletStoresV1.denominations;
|
||||
}>,
|
||||
@ -843,11 +859,12 @@ export async function spendCoins(
|
||||
if (!coin) {
|
||||
throw Error("coin allocated for payment doesn't exist anymore");
|
||||
}
|
||||
const denom = await tx.denominations.get([
|
||||
const coinAvailability = await tx.coinAvailability.get([
|
||||
coin.exchangeBaseUrl,
|
||||
coin.denomPubHash,
|
||||
coin.maxAge,
|
||||
]);
|
||||
checkDbInvariant(!!denom);
|
||||
checkDbInvariant(!!coinAvailability);
|
||||
const contrib = csi.contributions[i];
|
||||
if (coin.status !== CoinStatus.Fresh) {
|
||||
const alloc = coin.allocation;
|
||||
@ -874,13 +891,15 @@ export async function spendCoins(
|
||||
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`);
|
||||
checkDbInvariant(!!coinAvailability);
|
||||
if (coinAvailability.freshCoinCount === 0) {
|
||||
throw Error(
|
||||
`invalid coin count ${coinAvailability.freshCoinCount} in DB`,
|
||||
);
|
||||
}
|
||||
denom.freshCoinCount--;
|
||||
coinAvailability.freshCoinCount--;
|
||||
await tx.coins.put(coin);
|
||||
await tx.denominations.put(denom);
|
||||
await tx.coinAvailability.put(coinAvailability);
|
||||
}
|
||||
const refreshCoinPubs = csi.coinPubs.map((x) => ({
|
||||
coinPub: x,
|
||||
@ -894,39 +913,45 @@ async function setCoinSuspended(
|
||||
suspended: boolean,
|
||||
): Promise<void> {
|
||||
await ws.db
|
||||
.mktx((x) => [x.coins, x.denominations])
|
||||
.mktx((x) => [x.coins, x.coinAvailability])
|
||||
.runReadWrite(async (tx) => {
|
||||
const c = await tx.coins.get(coinPub);
|
||||
if (!c) {
|
||||
logger.warn(`coin ${coinPub} not found, won't suspend`);
|
||||
return;
|
||||
}
|
||||
const denom = await tx.denominations.get([
|
||||
const coinAvailability = await tx.coinAvailability.get([
|
||||
c.exchangeBaseUrl,
|
||||
c.denomPubHash,
|
||||
c.maxAge,
|
||||
]);
|
||||
checkDbInvariant(!!denom);
|
||||
checkDbInvariant(!!coinAvailability);
|
||||
if (suspended) {
|
||||
if (c.status !== CoinStatus.Fresh) {
|
||||
return;
|
||||
}
|
||||
if (denom.freshCoinCount == null || denom.freshCoinCount === 0) {
|
||||
throw Error(`invalid coin count ${denom.freshCoinCount} in DB`);
|
||||
if (
|
||||
coinAvailability.freshCoinCount == null ||
|
||||
coinAvailability.freshCoinCount === 0
|
||||
) {
|
||||
throw Error(
|
||||
`invalid coin count ${coinAvailability.freshCoinCount} in DB`,
|
||||
);
|
||||
}
|
||||
denom.freshCoinCount--;
|
||||
coinAvailability.freshCoinCount--;
|
||||
c.status = CoinStatus.FreshSuspended;
|
||||
} else {
|
||||
if (c.status == CoinStatus.Dormant) {
|
||||
return;
|
||||
}
|
||||
if (denom.freshCoinCount == null) {
|
||||
denom.freshCoinCount = 0;
|
||||
if (coinAvailability.freshCoinCount == null) {
|
||||
coinAvailability.freshCoinCount = 0;
|
||||
}
|
||||
denom.freshCoinCount++;
|
||||
coinAvailability.freshCoinCount++;
|
||||
c.status = CoinStatus.Fresh;
|
||||
}
|
||||
await tx.coins.put(c);
|
||||
await tx.denominations.put(denom);
|
||||
await tx.coinAvailability.put(coinAvailability);
|
||||
});
|
||||
}
|
||||
|
||||
@ -1195,7 +1220,12 @@ async function dispatchRequestInternal(
|
||||
const req = codecForForceRefreshRequest().decode(payload);
|
||||
const coinPubs = req.coinPubList.map((x) => ({ coinPub: x }));
|
||||
const refreshGroupId = await ws.db
|
||||
.mktx((x) => [x.refreshGroups, x.denominations, x.coins])
|
||||
.mktx((x) => [
|
||||
x.refreshGroups,
|
||||
x.coinAvailability,
|
||||
x.denominations,
|
||||
x.coins,
|
||||
])
|
||||
.runReadWrite(async (tx) => {
|
||||
return await createRefreshGroup(
|
||||
ws,
|
||||
|
Loading…
Reference in New Issue
Block a user