wallet: allow forced denom selection for tests
This commit is contained in:
parent
fdd272af20
commit
bbd6ccf1c7
@ -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;
|
||||
}[];
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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) => {
|
||||
|
@ -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": {
|
||||
|
Loading…
Reference in New Issue
Block a user