wallet: allow forced denom selection for tests

This commit is contained in:
Florian Dold 2022-03-29 21:21:57 +02:00
parent fdd272af20
commit bbd6ccf1c7
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
6 changed files with 105 additions and 68 deletions

View File

@ -212,6 +212,12 @@ export interface CreateReserveRequest {
* URL to fetch the withdraw status from the bank.
*/
bankWithdrawStatusUrl?: string;
/**
* Forced denomination selection for the first withdrawal
* from this reserve, only used for testing.
*/
forcedDenomSel?: ForcedDenomSel;
}
export const codecForCreateReserveRequest = (): Codec<CreateReserveRequest> =>
@ -727,6 +733,7 @@ export interface GetWithdrawalDetailsForAmountRequest {
export interface AcceptBankIntegratedWithdrawalRequest {
talerWithdrawUri: string;
exchangeBaseUrl: string;
forcedDenomSel?: ForcedDenomSel;
}
export const codecForAcceptBankIntegratedWithdrawalRequest =
@ -734,6 +741,7 @@ export const codecForAcceptBankIntegratedWithdrawalRequest =
buildCodecForObject<AcceptBankIntegratedWithdrawalRequest>()
.property("exchangeBaseUrl", codecForString())
.property("talerWithdrawUri", codecForString())
.property("forcedDenomSel", codecForAny())
.build("AcceptBankIntegratedWithdrawalRequest");
export const codecForGetWithdrawalDetailsForAmountRequest =
@ -1134,6 +1142,9 @@ export const codecForImportDbRequest = (): Codec<ImportDb> =>
.property("dump", codecForAny())
.build("ImportDbRequest");
export interface ForcedDenomSel {
denoms: {
value: AmountString;
count: number;
}[];
}

View File

@ -106,11 +106,12 @@ export function getTotalRefreshCost(
amountLeft,
refreshedDenom.feeRefresh,
).amount;
const denomMap = Object.fromEntries(denoms.map((x) => [x.denomPubHash, x]));
const withdrawDenoms = selectWithdrawalDenominations(withdrawAmount, denoms);
const resultingAmount = Amounts.add(
Amounts.getZero(withdrawAmount.currency),
...withdrawDenoms.selectedDenoms.map(
(d) => Amounts.mult(d.denom.value, d.count).amount,
(d) => Amounts.mult(denomMap[d.denomPubHash].value, d.count).amount,
),
).amount;
const totalCost = Amounts.sub(amountLeft, resultingAmount).amount;
@ -277,7 +278,7 @@ async function refreshCreateSession(
sessionSecretSeed: sessionSecretSeed,
newDenoms: newCoinDenoms.selectedDenoms.map((x) => ({
count: x.count,
denomPubHash: x.denom.denomPubHash,
denomPubHash: x.denomPubHash,
})),
amountRefreshOutput: newCoinDenoms.totalCoinValue,
};

View File

@ -37,6 +37,8 @@ import {
TalerErrorDetail,
AbsoluteTime,
URL,
AmountString,
ForcedDenomSel,
} from "@gnu-taler/taler-util";
import { InternalWalletState } from "../internal-wallet-state.js";
import {
@ -68,7 +70,6 @@ import {
updateExchangeFromUrl,
} from "./exchanges.js";
import {
denomSelectionInfoToState,
getBankWithdrawalInfo,
getCandidateWithdrawalDenoms,
processWithdrawGroup,
@ -180,8 +181,7 @@ export async function createReserve(
await updateWithdrawalDenoms(ws, canonExchange);
const denoms = await getCandidateWithdrawalDenoms(ws, canonExchange);
const denomSelInfo = selectWithdrawalDenominations(req.amount, denoms);
const initialDenomSel = denomSelectionInfoToState(denomSelInfo);
const initialDenomSel = selectWithdrawalDenominations(req.amount, denoms);
const reserveRecord: ReserveRecord = {
instructedAmount: req.amount,
@ -630,7 +630,7 @@ async function updateReserve(
amountReservePlus,
amountReserveMinus,
).amount;
const denomSelInfo = selectWithdrawalDenominations(
const denomSel = selectWithdrawalDenominations(
remainingAmount,
denoms,
);
@ -639,11 +639,11 @@ async function updateReserve(
`Remaining unclaimed amount in reseve is ${Amounts.stringify(
remainingAmount,
)} and can be withdrawn with ${
denomSelInfo.selectedDenoms.length
denomSel.selectedDenoms.length
} coins`,
);
if (denomSelInfo.selectedDenoms.length === 0) {
if (denomSel.selectedDenoms.length === 0) {
newReserve.reserveStatus = ReserveRecordStatus.Dormant;
newReserve.operationStatus = OperationStatus.Finished;
delete newReserve.lastError;
@ -669,7 +669,7 @@ async function updateReserve(
timestampStart: AbsoluteTime.toTimestamp(AbsoluteTime.now()),
retryInfo: resetRetryInfo(),
lastError: undefined,
denomsSel: denomSelectionInfoToState(denomSelInfo),
denomsSel: denomSel,
secretSeed: encodeCrock(getRandomBytes(64)),
denomSelUid: encodeCrock(getRandomBytes(32)),
operationStatus: OperationStatus.Pending,
@ -755,6 +755,9 @@ export async function createTalerWithdrawReserve(
ws: InternalWalletState,
talerWithdrawUri: string,
selectedExchange: string,
options: {
forcedDenomSel?: ForcedDenomSel;
} = {},
): Promise<AcceptWithdrawalResponse> {
await updateExchangeFromUrl(ws, selectedExchange);
const withdrawInfo = await getBankWithdrawalInfo(ws.http, talerWithdrawUri);

View File

@ -56,7 +56,6 @@ import {
updateWithdrawalDenoms,
getCandidateWithdrawalDenoms,
selectWithdrawalDenominations,
denomSelectionInfoToState,
} from "./withdraw.js";
import {
getHttpResponseErrorDetails,
@ -133,7 +132,7 @@ export async function prepareTip(
tipAmountEffective: selectedDenoms.totalCoinValue,
retryInfo: resetRetryInfo(),
lastError: undefined,
denomsSel: denomSelectionInfoToState(selectedDenoms),
denomsSel: selectedDenoms,
pickedUpTimestamp: undefined,
secretSeed,
denomSelUid,

View File

@ -31,6 +31,7 @@ import {
durationFromSpec,
ExchangeListItem,
ExchangeWithdrawRequest,
ForcedDenomSel,
LibtoolVersion,
Logger,
NotificationType,
@ -68,6 +69,7 @@ import {
HttpRequestLibrary,
readSuccessResponseJsonOrThrow,
} from "../util/http.js";
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
import {
resetRetryInfo,
RetryInfo,
@ -84,21 +86,6 @@ import { guardOperationException } from "./common.js";
*/
const logger = new Logger("operations/withdraw.ts");
/**
* FIXME: Eliminate this in favor of DenomSelectionState.
*/
interface DenominationSelectionInfo {
totalCoinValue: AmountJson;
totalWithdrawCost: AmountJson;
selectedDenoms: {
/**
* How many times do we withdraw this denomination?
*/
count: number;
denom: DenominationRecord;
}[];
}
/**
* Information about what will happen when creating a reserve.
*
@ -122,7 +109,7 @@ export interface ExchangeWithdrawDetails {
/**
* Selected denominations for withdraw.
*/
selectedDenoms: DenominationSelectionInfo;
selectedDenoms: DenomSelectionState;
/**
* Does the wallet know about an auditor for
@ -213,12 +200,12 @@ export function isWithdrawableDenom(d: DenominationRecord): boolean {
export function selectWithdrawalDenominations(
amountAvailable: AmountJson,
denoms: DenominationRecord[],
): DenominationSelectionInfo {
): DenomSelectionState {
let remaining = Amounts.copy(amountAvailable);
const selectedDenoms: {
count: number;
denom: DenominationRecord;
denomPubHash: string;
}[] = [];
let totalCoinValue = Amounts.getZero(amountAvailable.currency);
@ -248,7 +235,7 @@ export function selectWithdrawalDenominations(
).amount;
selectedDenoms.push({
count,
denom: d,
denomPubHash: d.denomPubHash,
});
}
@ -262,9 +249,7 @@ export function selectWithdrawalDenominations(
`selected withdrawal denoms for ${Amounts.stringify(totalCoinValue)}`,
);
for (const sd of selectedDenoms) {
logger.trace(
`denom_pub_hash=${sd.denom.denomPubHash}, count=${sd.count}`,
);
logger.trace(`denom_pub_hash=${sd.denomPubHash}, count=${sd.count}`);
}
logger.trace("(end of withdrawal denom list)");
}
@ -276,6 +261,56 @@ export function selectWithdrawalDenominations(
};
}
export function selectForcedWithdrawalDenominations(
amountAvailable: AmountJson,
denoms: DenominationRecord[],
forcedDenomSel: ForcedDenomSel,
): DenomSelectionState {
let remaining = Amounts.copy(amountAvailable);
const selectedDenoms: {
count: number;
denomPubHash: string;
}[] = [];
let totalCoinValue = Amounts.getZero(amountAvailable.currency);
let totalWithdrawCost = Amounts.getZero(amountAvailable.currency);
denoms = denoms.filter(isWithdrawableDenom);
denoms.sort((d1, d2) => Amounts.cmp(d2.value, d1.value));
for (const fds of forcedDenomSel.denoms) {
const count = fds.count;
const denom = denoms.find((x) => {
return Amounts.cmp(x.value, fds.value) == 0;
});
if (!denom) {
throw Error(
`unable to find denom for forced selection (value ${fds.value})`,
);
}
const cost = Amounts.add(denom.value, denom.feeWithdraw).amount;
totalCoinValue = Amounts.add(
totalCoinValue,
Amounts.mult(denom.value, count).amount,
).amount;
totalWithdrawCost = Amounts.add(
totalWithdrawCost,
Amounts.mult(cost, count).amount,
).amount;
selectedDenoms.push({
count,
denomPubHash: denom.denomPubHash,
});
}
return {
selectedDenoms,
totalCoinValue,
totalWithdrawCost,
};
}
/**
* Get information about a withdrawal from
* a taler://withdraw URI by asking the bank.
@ -695,21 +730,6 @@ async function processPlanchetVerifyAndStoreCoin(
}
}
export function denomSelectionInfoToState(
dsi: DenominationSelectionInfo,
): DenomSelectionState {
return {
selectedDenoms: dsi.selectedDenoms.map((x) => {
return {
count: x.count,
denomPubHash: x.denom.denomPubHash,
};
}),
totalCoinValue: dsi.totalCoinValue,
totalWithdrawCost: dsi.totalWithdrawCost,
};
}
/**
* Make sure that denominations that currently can be used for withdrawal
* are validated, and the result of validation is stored in the database.
@ -1006,11 +1026,21 @@ export async function getExchangeWithdrawalInfo(
exchange,
);
let earliestDepositExpiration =
selectedDenoms.selectedDenoms[0].denom.stampExpireDeposit;
let earliestDepositExpiration: TalerProtocolTimestamp | undefined;
for (let i = 1; i < selectedDenoms.selectedDenoms.length; i++) {
const expireDeposit =
selectedDenoms.selectedDenoms[i].denom.stampExpireDeposit;
const ds = selectedDenoms.selectedDenoms[i];
// FIXME: Do in one transaction!
const denom = await ws.db
.mktx((x) => ({ denominations: x.denominations }))
.runReadOnly(async (tx) => {
return ws.getDenomInfo(ws, tx, exchangeBaseUrl, ds.denomPubHash);
});
checkDbInvariant(!!denom);
const expireDeposit = denom.stampExpireDeposit;
if (!earliestDepositExpiration) {
earliestDepositExpiration = expireDeposit;
continue;
}
if (
AbsoluteTime.cmp(
AbsoluteTime.fromTimestamp(expireDeposit),
@ -1021,6 +1051,8 @@ export async function getExchangeWithdrawalInfo(
}
}
checkLogicInvariant(!!earliestDepositExpiration);
const possibleDenoms = await ws.db
.mktx((x) => ({ denominations: x.denominations }))
.runReadOnly(async (tx) => {

View File

@ -598,18 +598,6 @@ async function getExchanges(
return { exchanges };
}
async function acceptWithdrawal(
ws: InternalWalletState,
talerWithdrawUri: string,
selectedExchange: string,
): Promise<AcceptWithdrawalResponse> {
try {
return createTalerWithdrawReserve(ws, talerWithdrawUri, selectedExchange);
} finally {
ws.latch.trigger();
}
}
/**
* Inform the wallet that the status of a reserve has changed (e.g. due to a
* confirmation from the bank.).
@ -849,10 +837,13 @@ async function dispatchRequestInternal(
case "acceptBankIntegratedWithdrawal": {
const req =
codecForAcceptBankIntegratedWithdrawalRequest().decode(payload);
return await acceptWithdrawal(
return await createTalerWithdrawReserve(
ws,
req.talerWithdrawUri,
req.exchangeBaseUrl,
{
forcedDenomSel: req.forcedDenomSel,
},
);
}
case "getExchangeTos": {