wallet-core: implement and test forced coin/denom selection

This commit is contained in:
Florian Dold 2022-06-10 13:03:47 +02:00
parent 3ebb1d1815
commit f57dc7bf7a
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
14 changed files with 463 additions and 173 deletions

View File

@ -33,7 +33,6 @@ import {
codecForAmountString, codecForAmountString,
} from "./amounts.js"; } from "./amounts.js";
import { import {
AbsoluteTime,
codecForTimestamp, codecForTimestamp,
TalerProtocolTimestamp, TalerProtocolTimestamp,
} from "./time.js"; } from "./time.js";
@ -231,6 +230,7 @@ export const codecForCreateReserveRequest = (): Codec<CreateReserveRequest> =>
.property("exchangePaytoUri", codecForString()) .property("exchangePaytoUri", codecForString())
.property("senderWire", codecOptional(codecForString())) .property("senderWire", codecOptional(codecForString()))
.property("bankWithdrawStatusUrl", codecOptional(codecForString())) .property("bankWithdrawStatusUrl", codecOptional(codecForString()))
.property("forcedDenomSel", codecForAny())
.build("CreateReserveRequest"); .build("CreateReserveRequest");
/** /**
@ -674,6 +674,7 @@ export interface TestPayArgs {
merchantAuthToken?: string; merchantAuthToken?: string;
amount: string; amount: string;
summary: string; summary: string;
forcedCoinSel?: ForcedCoinSel;
} }
export const codecForTestPayArgs = (): Codec<TestPayArgs> => export const codecForTestPayArgs = (): Codec<TestPayArgs> =>
@ -682,6 +683,7 @@ export const codecForTestPayArgs = (): Codec<TestPayArgs> =>
.property("merchantAuthToken", codecOptional(codecForString())) .property("merchantAuthToken", codecOptional(codecForString()))
.property("amount", codecForString()) .property("amount", codecForString())
.property("summary", codecForString()) .property("summary", codecForString())
.property("forcedCoinSel", codecForAny())
.build("TestPayArgs"); .build("TestPayArgs");
export interface IntegrationTestArgs { export interface IntegrationTestArgs {
@ -738,7 +740,7 @@ export const codecForGetExchangeTosRequest = (): Codec<GetExchangeTosRequest> =>
export interface AcceptManualWithdrawalRequest { export interface AcceptManualWithdrawalRequest {
exchangeBaseUrl: string; exchangeBaseUrl: string;
amount: string; amount: string;
restrictAge?: number, restrictAge?: number;
} }
export const codecForAcceptManualWithdrawalRequet = export const codecForAcceptManualWithdrawalRequet =
@ -803,7 +805,8 @@ export interface ApplyRefundFromPurchaseIdRequest {
purchaseId: string; purchaseId: string;
} }
export const codecForApplyRefundFromPurchaseIdRequest = (): Codec<ApplyRefundFromPurchaseIdRequest> => export const codecForApplyRefundFromPurchaseIdRequest =
(): Codec<ApplyRefundFromPurchaseIdRequest> =>
buildCodecForObject<ApplyRefundFromPurchaseIdRequest>() buildCodecForObject<ApplyRefundFromPurchaseIdRequest>()
.property("purchaseId", codecForString()) .property("purchaseId", codecForString())
.build("ApplyRefundFromPurchaseIdRequest"); .build("ApplyRefundFromPurchaseIdRequest");
@ -866,12 +869,14 @@ export const codecForPreparePayRequest = (): Codec<PreparePayRequest> =>
export interface ConfirmPayRequest { export interface ConfirmPayRequest {
proposalId: string; proposalId: string;
sessionId?: string; sessionId?: string;
forcedCoinSel?: ForcedCoinSel;
} }
export const codecForConfirmPayRequest = (): Codec<ConfirmPayRequest> => export const codecForConfirmPayRequest = (): Codec<ConfirmPayRequest> =>
buildCodecForObject<ConfirmPayRequest>() buildCodecForObject<ConfirmPayRequest>()
.property("proposalId", codecForString()) .property("proposalId", codecForString())
.property("sessionId", codecOptional(codecForString())) .property("sessionId", codecOptional(codecForString()))
.property("forcedCoinSel", codecForAny())
.build("ConfirmPay"); .build("ConfirmPay");
export type CoreApiResponse = CoreApiResponseSuccess | CoreApiResponseError; export type CoreApiResponse = CoreApiResponseSuccess | CoreApiResponseError;
@ -903,6 +908,7 @@ export interface WithdrawTestBalanceRequest {
amount: string; amount: string;
bankBaseUrl: string; bankBaseUrl: string;
exchangeBaseUrl: string; exchangeBaseUrl: string;
forcedDenomSel?: ForcedDenomSel;
} }
export const withdrawTestBalanceDefaults = { export const withdrawTestBalanceDefaults = {
@ -976,6 +982,7 @@ export const codecForWithdrawTestBalance =
.property("amount", codecForString()) .property("amount", codecForString())
.property("bankBaseUrl", codecForString()) .property("bankBaseUrl", codecForString())
.property("exchangeBaseUrl", codecForString()) .property("exchangeBaseUrl", codecForString())
.property("forcedDenomSel", codecForAny())
.build("WithdrawTestBalanceRequest"); .build("WithdrawTestBalanceRequest");
export interface ApplyRefundResponse { export interface ApplyRefundResponse {
@ -1026,8 +1033,6 @@ export const codecForForceRefreshRequest = (): Codec<ForceRefreshRequest> =>
.property("coinPubList", codecForList(codecForString())) .property("coinPubList", codecForList(codecForString()))
.build("ForceRefreshRequest"); .build("ForceRefreshRequest");
export interface PrepareRefundRequest { export interface PrepareRefundRequest {
talerRefundUri: string; talerRefundUri: string;
} }
@ -1084,10 +1089,8 @@ export const codecForGetFeeForDeposit = (): Codec<GetFeeForDepositRequest> =>
export interface PrepareDepositRequest { export interface PrepareDepositRequest {
depositPaytoUri: string; depositPaytoUri: string;
amount: AmountString; amount: AmountString;
} }
export const codecForPrepareDepositRequest = export const codecForPrepareDepositRequest = (): Codec<PrepareDepositRequest> =>
(): Codec<PrepareDepositRequest> =>
buildCodecForObject<PrepareDepositRequest>() buildCodecForObject<PrepareDepositRequest>()
.property("amount", codecForAmountString()) .property("amount", codecForAmountString())
.property("depositPaytoUri", codecForString()) .property("depositPaytoUri", codecForString())
@ -1203,6 +1206,7 @@ export const codecForWithdrawFakebankRequest =
export interface ImportDb { export interface ImportDb {
dump: any; dump: any;
} }
export const codecForImportDbRequest = (): Codec<ImportDb> => export const codecForImportDbRequest = (): Codec<ImportDb> =>
buildCodecForObject<ImportDb>() buildCodecForObject<ImportDb>()
.property("dump", codecForAny()) .property("dump", codecForAny())
@ -1214,3 +1218,49 @@ export interface ForcedDenomSel {
count: number; count: number;
}[]; }[];
} }
/**
* Forced coin selection for deposits/payments.
*/
export interface ForcedCoinSel {
coins: {
value: AmountString;
contribution: AmountString;
}[];
}
export interface TestPayResult {
payCoinSelection: PayCoinSelection,
}
/**
* Result of selecting coins, contains the exchange, and selected
* coins with their denomination.
*/
export interface PayCoinSelection {
/**
* Amount requested by the merchant.
*/
paymentAmount: AmountJson;
/**
* Public keys of the coins that were selected.
*/
coinPubs: string[];
/**
* Amount that each coin contributes.
*/
coinContributions: AmountJson[];
/**
* How much of the wire fees is the customer paying?
*/
customerWireFees: AmountJson;
/**
* How much of the deposit fees is the customer paying?
*/
customerDepositFees: AmountJson;
}

View File

@ -0,0 +1,94 @@
/*
This file is part of GNU Taler
(C) 2020 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Imports.
*/
import {
ConfirmPayResultType,
j2s,
PreparePayResultType,
} from "@gnu-taler/taler-util";
import { Wallet, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import {
GlobalTestState,
MerchantPrivateApi,
WithAuthorization,
} from "../harness/harness.js";
import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
/**
* Run test for forced denom/coin selection.
*/
export async function runForcedSelectionTest(t: GlobalTestState) {
// Set up test environment
const { wallet, bank, exchange, merchant } =
await createSimpleTestkudosEnvironment(t);
await wallet.client.call(WalletApiOperation.AddExchange, {
exchangeBaseUrl: exchange.baseUrl,
});
await wallet.client.call(WalletApiOperation.WithdrawTestBalance, {
exchangeBaseUrl: exchange.baseUrl,
amount: "TESTKUDOS:10",
bankBaseUrl: bank.baseUrl,
forcedDenomSel: {
denoms: [
{
value: "TESTKUDOS:2",
count: 3,
},
],
},
});
await wallet.runUntilDone();
const coinDump = await wallet.client.call(WalletApiOperation.DumpCoins, {});
console.log(coinDump);
t.assertDeepEqual(coinDump.coins.length, 3);
const payResp = await wallet.client.call(WalletApiOperation.TestPay, {
amount: "TESTKUDOS:3",
merchantBaseUrl: merchant.makeInstanceBaseUrl(),
summary: "bla",
forcedCoinSel: {
coins: [
{
value: "TESTKUDOS:2",
contribution: "TESTKUDOS:1",
},
{
value: "TESTKUDOS:2",
contribution: "TESTKUDOS:1",
},
{
value: "TESTKUDOS:2",
contribution: "TESTKUDOS:1",
},
],
},
});
console.log(j2s(payResp));
// Without forced selection, we would only use 2 coins.
t.assertDeepEqual(payResp.payCoinSelection.coinContributions.length, 3);
}
runForcedSelectionTest.suites = ["wallet"];

View File

@ -34,6 +34,7 @@ import { runDepositTest } from "./test-deposit";
import { runExchangeManagementTest } from "./test-exchange-management"; import { runExchangeManagementTest } from "./test-exchange-management";
import { runExchangeTimetravelTest } from "./test-exchange-timetravel.js"; import { runExchangeTimetravelTest } from "./test-exchange-timetravel.js";
import { runFeeRegressionTest } from "./test-fee-regression"; import { runFeeRegressionTest } from "./test-fee-regression";
import { runForcedSelectionTest } from "./test-forced-selection.js";
import { runLibeufinApiBankaccountTest } from "./test-libeufin-api-bankaccount"; import { runLibeufinApiBankaccountTest } from "./test-libeufin-api-bankaccount";
import { runLibeufinApiBankconnectionTest } from "./test-libeufin-api-bankconnection"; import { runLibeufinApiBankconnectionTest } from "./test-libeufin-api-bankconnection";
import { runLibeufinApiFacadeTest } from "./test-libeufin-api-facade"; import { runLibeufinApiFacadeTest } from "./test-libeufin-api-facade";
@ -113,6 +114,7 @@ const allTests: TestMainFunction[] = [
runExchangeManagementTest, runExchangeManagementTest,
runExchangeTimetravelTest, runExchangeTimetravelTest,
runFeeRegressionTest, runFeeRegressionTest,
runForcedSelectionTest,
runLibeufinBasicTest, runLibeufinBasicTest,
runLibeufinKeyrotationTest, runLibeufinKeyrotationTest,
runLibeufinTutorialTest, runLibeufinTutorialTest,

View File

@ -41,9 +41,9 @@ import {
TalerProtocolTimestamp, TalerProtocolTimestamp,
TalerProtocolDuration, TalerProtocolDuration,
AgeCommitmentProof, AgeCommitmentProof,
PayCoinSelection,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { RetryInfo } from "./util/retries.js"; import { RetryInfo } from "./util/retries.js";
import { PayCoinSelection } from "./util/coinSelection.js";
import { Event, IDBDatabase } from "@gnu-taler/idb-bridge"; import { Event, IDBDatabase } from "@gnu-taler/idb-bridge";
/** /**

View File

@ -18,7 +18,7 @@ import {
AmountJson, AmountJson,
Amounts, BackupCoinSourceType, BackupDenomSel, BackupProposalStatus, Amounts, BackupCoinSourceType, BackupDenomSel, BackupProposalStatus,
BackupPurchase, BackupRefreshReason, BackupRefundState, codecForContractTerms, BackupPurchase, BackupRefreshReason, BackupRefundState, codecForContractTerms,
DenomKeyType, j2s, Logger, RefreshReason, TalerProtocolTimestamp, DenomKeyType, j2s, Logger, PayCoinSelection, RefreshReason, TalerProtocolTimestamp,
WalletBackupContentV1 WalletBackupContentV1
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { import {
@ -29,7 +29,6 @@ import {
ReserveRecordStatus, WalletContractData, WalletRefundItem, WalletStoresV1, WireInfo ReserveRecordStatus, WalletContractData, WalletRefundItem, WalletStoresV1, WireInfo
} from "../../db.js"; } from "../../db.js";
import { InternalWalletState } from "../../internal-wallet-state.js"; import { InternalWalletState } from "../../internal-wallet-state.js";
import { PayCoinSelection } from "../../util/coinSelection.js";
import { import {
checkDbInvariant, checkDbInvariant,
checkLogicInvariant checkLogicInvariant

View File

@ -35,6 +35,7 @@ import {
Logger, Logger,
NotificationType, NotificationType,
parsePaytoUri, parsePaytoUri,
PayCoinSelection,
PrepareDepositRequest, PrepareDepositRequest,
PrepareDepositResponse, PrepareDepositResponse,
TalerErrorDetail, TalerErrorDetail,
@ -45,7 +46,7 @@ import {
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { DepositGroupRecord, OperationStatus, WireFee } from "../db.js"; import { DepositGroupRecord, OperationStatus, WireFee } from "../db.js";
import { InternalWalletState } from "../internal-wallet-state.js"; import { InternalWalletState } from "../internal-wallet-state.js";
import { PayCoinSelection, selectPayCoins } from "../util/coinSelection.js"; import { selectPayCoins } from "../util/coinSelection.js";
import { readSuccessResponseJsonOrThrow } from "../util/http.js"; import { readSuccessResponseJsonOrThrow } from "../util/http.js";
import { RetryInfo } from "../util/retries.js"; import { RetryInfo } from "../util/retries.js";
import { guardOperationException } from "./common.js"; import { guardOperationException } from "./common.js";

View File

@ -40,12 +40,14 @@ import {
durationMin, durationMin,
durationMul, durationMul,
encodeCrock, encodeCrock,
ForcedCoinSel,
getRandomBytes, getRandomBytes,
HttpStatusCode, HttpStatusCode,
j2s, j2s,
Logger, Logger,
NotificationType, NotificationType,
parsePayUri, parsePayUri,
PayCoinSelection,
PreparePayResult, PreparePayResult,
PreparePayResultType, PreparePayResultType,
RefreshReason, RefreshReason,
@ -81,8 +83,8 @@ import {
import { import {
AvailableCoinInfo, AvailableCoinInfo,
CoinCandidateSelection, CoinCandidateSelection,
PayCoinSelection,
PreviousPayCoins, PreviousPayCoins,
selectForcedPayCoins,
selectPayCoins, selectPayCoins,
} from "../util/coinSelection.js"; } from "../util/coinSelection.js";
import { ContractTermsUtil } from "../util/contractTerms.js"; import { ContractTermsUtil } from "../util/contractTerms.js";
@ -305,6 +307,7 @@ export async function getCandidatePayCoins(
} }
candidateCoins.push({ candidateCoins.push({
availableAmount: coin.currentAmount, availableAmount: coin.currentAmount,
value: denom.value,
coinPub: coin.coinPub, coinPub: coin.coinPub,
denomPub: denom.denomPub, denomPub: denom.denomPub,
feeDeposit: denom.feeDeposit, feeDeposit: denom.feeDeposit,
@ -1423,6 +1426,7 @@ export async function confirmPay(
ws: InternalWalletState, ws: InternalWalletState,
proposalId: string, proposalId: string,
sessionIdOverride?: string, sessionIdOverride?: string,
forcedCoinSel?: ForcedCoinSel,
): Promise<ConfirmPayResult> { ): Promise<ConfirmPayResult> {
logger.trace( logger.trace(
`executing confirmPay with proposalId ${proposalId} and sessionIdOverride ${sessionIdOverride}`, `executing confirmPay with proposalId ${proposalId} and sessionIdOverride ${sessionIdOverride}`,
@ -1479,7 +1483,19 @@ export async function confirmPay(
wireMethod: contractData.wireMethod, wireMethod: contractData.wireMethod,
}); });
const res = selectPayCoins({ let res: PayCoinSelection | undefined = undefined;
if (forcedCoinSel) {
res = selectForcedPayCoins(forcedCoinSel, {
candidates,
contractTermsAmount: contractData.amount,
depositFeeLimit: contractData.maxDepositFee,
wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
wireFeeLimit: contractData.maxWireFee,
requiredMinimumAge: contractData.minimumAge,
});
} else {
res = selectPayCoins({
candidates, candidates,
contractTermsAmount: contractData.amount, contractTermsAmount: contractData.amount,
depositFeeLimit: contractData.maxDepositFee, depositFeeLimit: contractData.maxDepositFee,
@ -1488,6 +1504,7 @@ export async function confirmPay(
prevPayCoins: [], prevPayCoins: [],
requiredMinimumAge: contractData.minimumAge, requiredMinimumAge: contractData.minimumAge,
}); });
}
logger.trace("coin selection result", res); logger.trace("coin selection result", res);

View File

@ -15,6 +15,7 @@
*/ */
import { import {
AbsoluteTime,
AcceptWithdrawalResponse, AcceptWithdrawalResponse,
addPaytoQueryParams, addPaytoQueryParams,
Amounts, Amounts,
@ -28,6 +29,7 @@ import {
durationMax, durationMax,
durationMin, durationMin,
encodeCrock, encodeCrock,
ForcedDenomSel,
getRandomBytes, getRandomBytes,
j2s, j2s,
Logger, Logger,
@ -35,13 +37,10 @@ import {
randomBytes, randomBytes,
TalerErrorCode, TalerErrorCode,
TalerErrorDetail, TalerErrorDetail,
AbsoluteTime,
URL, URL,
AmountString,
ForcedDenomSel,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { InternalWalletState } from "../internal-wallet-state.js";
import { import {
DenomSelectionState,
OperationStatus, OperationStatus,
ReserveBankInfo, ReserveBankInfo,
ReserveRecord, ReserveRecord,
@ -50,6 +49,7 @@ import {
WithdrawalGroupRecord, WithdrawalGroupRecord,
} from "../db.js"; } from "../db.js";
import { TalerError } from "../errors.js"; import { TalerError } from "../errors.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { assertUnreachable } from "../util/assertUnreachable.js"; import { assertUnreachable } from "../util/assertUnreachable.js";
import { import {
readSuccessResponseJsonOrErrorCode, readSuccessResponseJsonOrErrorCode,
@ -57,9 +57,8 @@ import {
throwUnexpectedRequestError, throwUnexpectedRequestError,
} from "../util/http.js"; } from "../util/http.js";
import { GetReadOnlyAccess } from "../util/query.js"; import { GetReadOnlyAccess } from "../util/query.js";
import { import { RetryInfo } from "../util/retries.js";
RetryInfo, import { guardOperationException } from "./common.js";
} from "../util/retries.js";
import { import {
getExchangeDetails, getExchangeDetails,
getExchangePaytoUri, getExchangePaytoUri,
@ -70,10 +69,10 @@ import {
getBankWithdrawalInfo, getBankWithdrawalInfo,
getCandidateWithdrawalDenoms, getCandidateWithdrawalDenoms,
processWithdrawGroup, processWithdrawGroup,
selectForcedWithdrawalDenominations,
selectWithdrawalDenominations, selectWithdrawalDenominations,
updateWithdrawalDenoms, updateWithdrawalDenoms,
} from "./withdraw.js"; } from "./withdraw.js";
import { guardOperationException } from "./common.js";
const logger = new Logger("taler-wallet-core:reserves.ts"); const logger = new Logger("taler-wallet-core:reserves.ts");
@ -178,7 +177,18 @@ export async function createReserve(
await updateWithdrawalDenoms(ws, canonExchange); await updateWithdrawalDenoms(ws, canonExchange);
const denoms = await getCandidateWithdrawalDenoms(ws, canonExchange); const denoms = await getCandidateWithdrawalDenoms(ws, canonExchange);
const initialDenomSel = selectWithdrawalDenominations(req.amount, denoms);
let initialDenomSel: DenomSelectionState;
if (req.forcedDenomSel) {
logger.warn("using forced denom selection");
initialDenomSel = selectForcedWithdrawalDenominations(
req.amount,
denoms,
req.forcedDenomSel,
);
} else {
initialDenomSel = selectWithdrawalDenominations(req.amount, denoms);
}
const reserveRecord: ReserveRecord = { const reserveRecord: ReserveRecord = {
instructedAmount: req.amount, instructedAmount: req.amount,
@ -436,7 +446,7 @@ async function processReserveBankStatus(
); );
if (status.aborted) { if (status.aborted) {
logger.trace("bank aborted the withdrawal"); logger.info("bank aborted the withdrawal");
await ws.db await ws.db
.mktx((x) => ({ .mktx((x) => ({
reserves: x.reserves, reserves: x.reserves,
@ -463,12 +473,14 @@ async function processReserveBankStatus(
return; return;
} }
if (status.selection_done) { // Bank still needs to know our reserve info
if (reserve.reserveStatus === ReserveRecordStatus.RegisteringBank) { if (!status.selection_done) {
await registerReserveWithBank(ws, reservePub); await registerReserveWithBank(ws, reservePub);
return await processReserveBankStatus(ws, reservePub); return await processReserveBankStatus(ws, reservePub);
} }
} else {
// FIXME: Why do we do this?!
if (reserve.reserveStatus === ReserveRecordStatus.RegisteringBank) {
await registerReserveWithBank(ws, reservePub); await registerReserveWithBank(ws, reservePub);
return await processReserveBankStatus(ws, reservePub); return await processReserveBankStatus(ws, reservePub);
} }
@ -482,7 +494,7 @@ async function processReserveBankStatus(
if (!r) { if (!r) {
return; return;
} }
if (status.transfer_done) { // Re-check reserve status within transaction
switch (r.reserveStatus) { switch (r.reserveStatus) {
case ReserveRecordStatus.RegisteringBank: case ReserveRecordStatus.RegisteringBank:
case ReserveRecordStatus.WaitConfirmBank: case ReserveRecordStatus.WaitConfirmBank:
@ -490,21 +502,18 @@ async function processReserveBankStatus(
default: default:
return; return;
} }
if (status.transfer_done) {
const now = AbsoluteTime.toTimestamp(AbsoluteTime.now()); const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
r.timestampBankConfirmed = now; r.timestampBankConfirmed = now;
r.reserveStatus = ReserveRecordStatus.QueryingStatus; r.reserveStatus = ReserveRecordStatus.QueryingStatus;
r.operationStatus = OperationStatus.Pending; r.operationStatus = OperationStatus.Pending;
r.retryInfo = RetryInfo.reset(); r.retryInfo = RetryInfo.reset();
} else { } else {
switch (r.reserveStatus) { logger.info("Withdrawal operation not yet confirmed by bank");
case ReserveRecordStatus.WaitConfirmBank:
break;
default:
return;
}
if (r.bankInfo) { if (r.bankInfo) {
r.bankInfo.confirmUrl = status.confirm_transfer_url; r.bankInfo.confirmUrl = status.confirm_transfer_url;
} }
r.retryInfo = RetryInfo.increment(r.retryInfo);
} }
await tx.reserves.put(r); await tx.reserves.put(r);
}); });
@ -540,6 +549,8 @@ async function updateReserve(
const reserveUrl = new URL(`reserves/${reservePub}`, reserve.exchangeBaseUrl); const reserveUrl = new URL(`reserves/${reservePub}`, reserve.exchangeBaseUrl);
reserveUrl.searchParams.set("timeout_ms", "30000"); reserveUrl.searchParams.set("timeout_ms", "30000");
logger.info(`querying reserve status via ${reserveUrl}`);
const resp = await ws.http.get(reserveUrl.href, { const resp = await ws.http.get(reserveUrl.href, {
timeout: getReserveRequestTimeout(reserve), timeout: getReserveRequestTimeout(reserve),
}); });
@ -589,6 +600,7 @@ async function updateReserve(
if (!newReserve) { if (!newReserve) {
return; return;
} }
let amountReservePlus = reserveBalance; let amountReservePlus = reserveBalance;
let amountReserveMinus = Amounts.getZero(currency); let amountReserveMinus = Amounts.getZero(currency);
@ -628,7 +640,18 @@ async function updateReserve(
amountReservePlus, amountReservePlus,
amountReserveMinus, amountReserveMinus,
).amount; ).amount;
const denomSel = selectWithdrawalDenominations(remainingAmount, denoms);
let withdrawalGroupId: string;
let denomSel: DenomSelectionState;
if (!newReserve.initialWithdrawalStarted) {
withdrawalGroupId = newReserve.initialWithdrawalGroupId;
newReserve.initialWithdrawalStarted = true;
denomSel = newReserve.initialDenomSel;
} else {
withdrawalGroupId = encodeCrock(randomBytes(32));
denomSel = selectWithdrawalDenominations(remainingAmount, denoms);
logger.trace( logger.trace(
`Remaining unclaimed amount in reseve is ${Amounts.stringify( `Remaining unclaimed amount in reseve is ${Amounts.stringify(
@ -644,14 +667,6 @@ async function updateReserve(
await tx.reserves.put(newReserve); await tx.reserves.put(newReserve);
return; return;
} }
let withdrawalGroupId: string;
if (!newReserve.initialWithdrawalStarted) {
withdrawalGroupId = newReserve.initialWithdrawalGroupId;
newReserve.initialWithdrawalStarted = true;
} else {
withdrawalGroupId = encodeCrock(randomBytes(32));
} }
const withdrawalRecord: WithdrawalGroupRecord = { const withdrawalRecord: WithdrawalGroupRecord = {
@ -768,6 +783,7 @@ export async function createTalerWithdrawReserve(
senderWire: withdrawInfo.senderWire, senderWire: withdrawInfo.senderWire,
exchangePaytoUri: exchangePaytoUri, exchangePaytoUri: exchangePaytoUri,
restrictAge: options.restrictAge, restrictAge: options.restrictAge,
forcedDenomSel: options.forcedDenomSel,
}); });
// We do this here, as the reserve should be registered before we return, // We do this here, as the reserve should be registered before we return,
// so that we can redirect the user to the bank's status page. // so that we can redirect the user to the bank's status page.

View File

@ -17,7 +17,12 @@
/** /**
* Imports. * Imports.
*/ */
import { Logger } from "@gnu-taler/taler-util"; import {
ConfirmPayResultType,
Logger,
TestPayResult,
WithdrawTestBalanceRequest,
} from "@gnu-taler/taler-util";
import { import {
HttpRequestLibrary, HttpRequestLibrary,
readSuccessResponseJsonOrThrow, readSuccessResponseJsonOrThrow,
@ -39,6 +44,7 @@ import { InternalWalletState } from "../internal-wallet-state.js";
import { confirmPay, preparePayForUri } from "./pay.js"; import { confirmPay, preparePayForUri } from "./pay.js";
import { getBalances } from "./balance.js"; import { getBalances } from "./balance.js";
import { applyRefund } from "./refund.js"; import { applyRefund } from "./refund.js";
import { checkLogicInvariant } from "../util/invariants.js";
const logger = new Logger("operations/testing.ts"); const logger = new Logger("operations/testing.ts");
@ -82,10 +88,12 @@ function makeBasicAuthHeader(username: string, password: string): string {
export async function withdrawTestBalance( export async function withdrawTestBalance(
ws: InternalWalletState, ws: InternalWalletState,
amount = "TESTKUDOS:10", req: WithdrawTestBalanceRequest,
bankBaseUrl = "https://bank.test.taler.net/",
exchangeBaseUrl = "https://exchange.test.taler.net/",
): Promise<void> { ): Promise<void> {
const bankBaseUrl = req.bankBaseUrl;
const amount = req.amount;
const exchangeBaseUrl = req.exchangeBaseUrl;
const bankUser = await registerRandomBankUser(ws.http, bankBaseUrl); const bankUser = await registerRandomBankUser(ws.http, bankBaseUrl);
logger.trace(`Registered bank user ${JSON.stringify(bankUser)}`); logger.trace(`Registered bank user ${JSON.stringify(bankUser)}`);
@ -100,6 +108,9 @@ export async function withdrawTestBalance(
ws, ws,
wresp.taler_withdraw_uri, wresp.taler_withdraw_uri,
exchangeBaseUrl, exchangeBaseUrl,
{
forcedDenomSel: req.forcedDenomSel,
},
); );
await confirmBankWithdrawalUri( await confirmBankWithdrawalUri(
@ -140,7 +151,10 @@ export async function createDemoBankWithdrawalUri(
}, },
{ {
headers: { headers: {
Authorization: makeBasicAuthHeader(bankUser.username, bankUser.password), Authorization: makeBasicAuthHeader(
bankUser.username,
bankUser.password,
),
}, },
}, },
); );
@ -163,7 +177,10 @@ async function confirmBankWithdrawalUri(
{}, {},
{ {
headers: { headers: {
Authorization: makeBasicAuthHeader(bankUser.username, bankUser.password), Authorization: makeBasicAuthHeader(
bankUser.username,
bankUser.password,
),
}, },
}, },
); );
@ -331,12 +348,11 @@ export async function runIntegrationTest(
const currency = parsedSpendAmount.currency; const currency = parsedSpendAmount.currency;
logger.info("withdrawing test balance"); logger.info("withdrawing test balance");
await withdrawTestBalance( await withdrawTestBalance(ws, {
ws, amount: args.amountToWithdraw,
args.amountToWithdraw, bankBaseUrl: args.bankBaseUrl,
args.bankBaseUrl, exchangeBaseUrl: args.exchangeBaseUrl,
args.exchangeBaseUrl, });
);
await ws.runUntilDone(); await ws.runUntilDone();
logger.info("done withdrawing test balance"); logger.info("done withdrawing test balance");
@ -360,12 +376,11 @@ export async function runIntegrationTest(
const refundAmount = Amounts.parseOrThrow(`${currency}:6`); const refundAmount = Amounts.parseOrThrow(`${currency}:6`);
const spendAmountThree = Amounts.parseOrThrow(`${currency}:3`); const spendAmountThree = Amounts.parseOrThrow(`${currency}:3`);
await withdrawTestBalance( await withdrawTestBalance(ws, {
ws, amount: Amounts.stringify(withdrawAmountTwo),
Amounts.stringify(withdrawAmountTwo), bankBaseUrl: args.bankBaseUrl,
args.bankBaseUrl, exchangeBaseUrl: args.exchangeBaseUrl,
args.exchangeBaseUrl, });
);
// Wait until the withdraw is done // Wait until the withdraw is done
await ws.runUntilDone(); await ws.runUntilDone();
@ -410,7 +425,10 @@ export async function runIntegrationTest(
logger.trace("integration test: all done!"); logger.trace("integration test: all done!");
} }
export async function testPay(ws: InternalWalletState, args: TestPayArgs) { export async function testPay(
ws: InternalWalletState,
args: TestPayArgs,
): Promise<TestPayResult> {
logger.trace("creating order"); logger.trace("creating order");
const merchant = { const merchant = {
authToken: args.merchantAuthToken, authToken: args.merchantAuthToken,
@ -429,12 +447,28 @@ export async function testPay(ws: InternalWalletState, args: TestPayArgs) {
if (!talerPayUri) { if (!talerPayUri) {
console.error("fatal: no taler pay URI received from backend"); console.error("fatal: no taler pay URI received from backend");
process.exit(1); process.exit(1);
return;
} }
logger.trace("taler pay URI:", talerPayUri); logger.trace("taler pay URI:", talerPayUri);
const result = await preparePayForUri(ws, talerPayUri); const result = await preparePayForUri(ws, talerPayUri);
if (result.status !== PreparePayResultType.PaymentPossible) { if (result.status !== PreparePayResultType.PaymentPossible) {
throw Error(`unexpected prepare pay status: ${result.status}`); throw Error(`unexpected prepare pay status: ${result.status}`);
} }
await confirmPay(ws, result.proposalId, undefined); const r = await confirmPay(
ws,
result.proposalId,
undefined,
args.forcedCoinSel,
);
if (r.type != ConfirmPayResultType.Done) {
throw Error("payment not done");
}
const purchase = await ws.db
.mktx((x) => ({ purchases: x.purchases }))
.runReadOnly(async (tx) => {
return tx.purchases.get(result.proposalId);
});
checkLogicInvariant(!!purchase);
return {
payCoinSelection: purchase.payCoinSelection,
};
} }

View File

@ -31,6 +31,7 @@ function a(x: string): AmountJson {
function fakeAci(current: string, feeDeposit: string): AvailableCoinInfo { function fakeAci(current: string, feeDeposit: string): AvailableCoinInfo {
return { return {
value: a(current),
availableAmount: a(current), availableAmount: a(current),
coinPub: "foobar", coinPub: "foobar",
denomPub: { denomPub: {
@ -45,6 +46,7 @@ function fakeAci(current: string, feeDeposit: string): AvailableCoinInfo {
function fakeAciWithAgeRestriction(current: string, feeDeposit: string): AvailableCoinInfo { function fakeAciWithAgeRestriction(current: string, feeDeposit: string): AvailableCoinInfo {
return { return {
value: a(current),
availableAmount: a(current), availableAmount: a(current),
coinPub: "foobar", coinPub: "foobar",
denomPub: { denomPub: {

View File

@ -29,42 +29,14 @@ import {
AmountJson, AmountJson,
Amounts, Amounts,
DenominationPubKey, DenominationPubKey,
ForcedCoinSel,
Logger, Logger,
PayCoinSelection,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { checkLogicInvariant } from "./invariants.js";
const logger = new Logger("coinSelection.ts"); const logger = new Logger("coinSelection.ts");
/**
* Result of selecting coins, contains the exchange, and selected
* coins with their denomination.
*/
export interface PayCoinSelection {
/**
* Amount requested by the merchant.
*/
paymentAmount: AmountJson;
/**
* Public keys of the coins that were selected.
*/
coinPubs: string[];
/**
* Amount that each coin contributes.
*/
coinContributions: AmountJson[];
/**
* How much of the wire fees is the customer paying?
*/
customerWireFees: AmountJson;
/**
* How much of the deposit fees is the customer paying?
*/
customerDepositFees: AmountJson;
}
/** /**
* Structure to describe a coin that is available to be * Structure to describe a coin that is available to be
* used in a payment. * used in a payment.
@ -82,6 +54,11 @@ export interface AvailableCoinInfo {
*/ */
denomPub: DenominationPubKey; denomPub: DenominationPubKey;
/**
* Full value of the coin.
*/
value: AmountJson;
/** /**
* Amount still remaining (typically the full amount, * Amount still remaining (typically the full amount,
* as coins are always refreshed after use.) * as coins are always refreshed after use.)
@ -356,3 +333,102 @@ export function selectPayCoins(
} }
return undefined; return undefined;
} }
export function selectForcedPayCoins(
forcedCoinSel: ForcedCoinSel,
req: SelectPayCoinRequest,
): PayCoinSelection | undefined {
const {
candidates,
contractTermsAmount,
depositFeeLimit,
wireFeeLimit,
wireFeeAmortization,
} = req;
if (candidates.candidateCoins.length === 0) {
return undefined;
}
const coinPubs: string[] = [];
const coinContributions: AmountJson[] = [];
const currency = contractTermsAmount.currency;
let tally: CoinSelectionTally = {
amountPayRemaining: contractTermsAmount,
amountWireFeeLimitRemaining: wireFeeLimit,
amountDepositFeeLimitRemaining: depositFeeLimit,
customerDepositFees: Amounts.getZero(currency),
customerWireFees: Amounts.getZero(currency),
wireFeeCoveredForExchange: new Set(),
};
// Not supported by forced coin selection
checkLogicInvariant(!req.prevPayCoins);
// Sort by available amount (descending), deposit fee (ascending) and
// denomPub (ascending) if deposit fee is the same
// (to guarantee deterministic results)
const candidateCoins = [...candidates.candidateCoins].sort(
(o1, o2) =>
-Amounts.cmp(o1.availableAmount, o2.availableAmount) ||
Amounts.cmp(o1.feeDeposit, o2.feeDeposit) ||
DenominationPubKey.cmp(o1.denomPub, o2.denomPub),
);
// FIXME: Here, we should select coins in a smarter way.
// Instead of always spending the next-largest coin,
// we should try to find the smallest coin that covers the
// amount.
// Set of spent coin indices from candidate coins
const spentSet: Set<number> = new Set();
for (const forcedCoin of forcedCoinSel.coins) {
let aci: AvailableCoinInfo | undefined = undefined;
for (let i = 0; i < candidateCoins.length; i++) {
if (spentSet.has(i)) {
continue;
}
if (
Amounts.cmp(forcedCoin.value, candidateCoins[i].availableAmount) != 0
) {
continue;
}
spentSet.add(i);
aci = candidateCoins[i];
break;
}
if (!aci) {
throw Error("can't find coin for forced coin selection");
}
tally = tallyFees(
tally,
candidates.wireFeesPerExchange,
wireFeeAmortization,
aci.exchangeBaseUrl,
aci.feeDeposit,
);
let coinSpend = Amounts.parseOrThrow(forcedCoin.contribution);
tally.amountPayRemaining = Amounts.sub(
tally.amountPayRemaining,
coinSpend,
).amount;
coinPubs.push(aci.coinPub);
coinContributions.push(coinSpend);
}
if (Amounts.isZero(tally.amountPayRemaining)) {
return {
paymentAmount: contractTermsAmount,
coinContributions,
coinPubs,
customerDepositFees: tally.customerDepositFees,
customerWireFees: tally.customerWireFees,
};
}
return undefined;
}

View File

@ -37,7 +37,7 @@ export interface RetryPolicy {
const defaultRetryPolicy: RetryPolicy = { const defaultRetryPolicy: RetryPolicy = {
backoffBase: 1.5, backoffBase: 1.5,
backoffDelta: Duration.fromSpec({ seconds: 30 }), backoffDelta: Duration.fromSpec({ seconds: 1 }),
maxTimeout: Duration.fromSpec({ minutes: 2 }), maxTimeout: Duration.fromSpec({ minutes: 2 }),
}; };

View File

@ -57,6 +57,7 @@ import {
SetCoinSuspendedRequest, SetCoinSuspendedRequest,
SetWalletDeviceIdRequest, SetWalletDeviceIdRequest,
TestPayArgs, TestPayArgs,
TestPayResult,
TrackDepositGroupRequest, TrackDepositGroupRequest,
TrackDepositGroupResponse, TrackDepositGroupResponse,
TransactionsRequest, TransactionsRequest,
@ -270,7 +271,7 @@ export type WalletOperations = {
}; };
[WalletApiOperation.TestPay]: { [WalletApiOperation.TestPay]: {
request: TestPayArgs; request: TestPayArgs;
response: {}; response: TestPayResult;
}; };
[WalletApiOperation.ExportDb]: { [WalletApiOperation.ExportDb]: {
request: {}; request: {};
@ -279,11 +280,11 @@ export type WalletOperations = {
}; };
export type RequestType< export type RequestType<
Op extends WalletApiOperation & keyof WalletOperations Op extends WalletApiOperation & keyof WalletOperations,
> = WalletOperations[Op] extends { request: infer T } ? T : never; > = WalletOperations[Op] extends { request: infer T } ? T : never;
export type ResponseType< export type ResponseType<
Op extends WalletApiOperation & keyof WalletOperations Op extends WalletApiOperation & keyof WalletOperations,
> = WalletOperations[Op] extends { response: infer T } ? T : never; > = WalletOperations[Op] extends { response: infer T } ? T : never;
export interface WalletCoreApiClient { export interface WalletCoreApiClient {

View File

@ -23,7 +23,9 @@
* Imports. * Imports.
*/ */
import { import {
AbsoluteTime, AcceptManualWithdrawalResult, AmountJson, AbsoluteTime,
AcceptManualWithdrawalResult,
AmountJson,
Amounts, Amounts,
BalancesResponse, BalancesResponse,
codecForAbortPayWithRefundRequest, codecForAbortPayWithRefundRequest,
@ -48,7 +50,9 @@ import {
codecForIntegrationTestArgs, codecForIntegrationTestArgs,
codecForListKnownBankAccounts, codecForListKnownBankAccounts,
codecForPrepareDepositRequest, codecForPrepareDepositRequest,
codecForPreparePayRequest, codecForPrepareRefundRequest, codecForPrepareTipRequest, codecForPreparePayRequest,
codecForPrepareRefundRequest,
codecForPrepareTipRequest,
codecForRetryTransactionRequest, codecForRetryTransactionRequest,
codecForSetCoinSuspendedRequest, codecForSetCoinSuspendedRequest,
codecForSetWalletDeviceIdRequest, codecForSetWalletDeviceIdRequest,
@ -58,7 +62,9 @@ import {
codecForWithdrawFakebankRequest, codecForWithdrawFakebankRequest,
codecForWithdrawTestBalance, codecForWithdrawTestBalance,
CoinDumpJson, CoinDumpJson,
CoreApiResponse, Duration, durationFromSpec, CoreApiResponse,
Duration,
durationFromSpec,
durationMin, durationMin,
ExchangeListItem, ExchangeListItem,
ExchangesListRespose, ExchangesListRespose,
@ -71,13 +77,14 @@ import {
parsePaytoUri, parsePaytoUri,
PaytoUri, PaytoUri,
RefreshReason, RefreshReason,
TalerErrorCode, URL, TalerErrorCode,
WalletNotification URL,
WalletNotification,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js"; import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
import { import {
CryptoDispatcher, CryptoDispatcher,
CryptoWorkerFactory CryptoWorkerFactory,
} from "./crypto/workers/cryptoDispatcher.js"; } from "./crypto/workers/cryptoDispatcher.js";
import { import {
AuditorTrustRecord, AuditorTrustRecord,
@ -85,7 +92,7 @@ import {
exportDb, exportDb,
importDb, importDb,
ReserveRecordStatus, ReserveRecordStatus,
WalletStoresV1 WalletStoresV1,
} from "./db.js"; } from "./db.js";
import { getErrorDetailFromException, TalerError } from "./errors.js"; import { getErrorDetailFromException, TalerError } from "./errors.js";
import { import {
@ -96,7 +103,7 @@ import {
MerchantOperations, MerchantOperations,
NotificationListener, NotificationListener,
RecoupOperations, RecoupOperations,
ReserveOperations ReserveOperations,
} from "./internal-wallet-state.js"; } from "./internal-wallet-state.js";
import { exportBackup } from "./operations/backup/export.js"; import { exportBackup } from "./operations/backup/export.js";
import { import {
@ -109,7 +116,7 @@ import {
loadBackupRecovery, loadBackupRecovery,
processBackupForProvider, processBackupForProvider,
removeBackupProvider, removeBackupProvider,
runBackupCycle runBackupCycle,
} from "./operations/backup/index.js"; } from "./operations/backup/index.js";
import { setWalletDeviceId } from "./operations/backup/state.js"; import { setWalletDeviceId } from "./operations/backup/state.js";
import { getBalances } from "./operations/balance.js"; import { getBalances } from "./operations/balance.js";
@ -118,7 +125,7 @@ import {
getFeeForDeposit, getFeeForDeposit,
prepareDepositGroup, prepareDepositGroup,
processDepositGroup, processDepositGroup,
trackDepositGroup trackDepositGroup,
} from "./operations/deposits.js"; } from "./operations/deposits.js";
import { import {
acceptExchangeTermsOfService, acceptExchangeTermsOfService,
@ -127,66 +134,66 @@ import {
getExchangeRequestTimeout, getExchangeRequestTimeout,
getExchangeTrust, getExchangeTrust,
updateExchangeFromUrl, updateExchangeFromUrl,
updateExchangeTermsOfService updateExchangeTermsOfService,
} from "./operations/exchanges.js"; } from "./operations/exchanges.js";
import { getMerchantInfo } from "./operations/merchants.js"; import { getMerchantInfo } from "./operations/merchants.js";
import { import {
confirmPay, confirmPay,
preparePayForUri, preparePayForUri,
processDownloadProposal, processDownloadProposal,
processPurchasePay processPurchasePay,
} from "./operations/pay.js"; } from "./operations/pay.js";
import { getPendingOperations } from "./operations/pending.js"; import { getPendingOperations } from "./operations/pending.js";
import { createRecoupGroup, processRecoupGroup } from "./operations/recoup.js"; import { createRecoupGroup, processRecoupGroup } from "./operations/recoup.js";
import { import {
autoRefresh, autoRefresh,
createRefreshGroup, createRefreshGroup,
processRefreshGroup processRefreshGroup,
} from "./operations/refresh.js"; } from "./operations/refresh.js";
import { import {
abortFailedPayWithRefund, abortFailedPayWithRefund,
applyRefund, applyRefund,
applyRefundFromPurchaseId, applyRefundFromPurchaseId,
prepareRefund, prepareRefund,
processPurchaseQueryRefund processPurchaseQueryRefund,
} from "./operations/refund.js"; } from "./operations/refund.js";
import { import {
createReserve, createReserve,
createTalerWithdrawReserve, createTalerWithdrawReserve,
getFundingPaytoUris, getFundingPaytoUris,
processReserve processReserve,
} from "./operations/reserves.js"; } from "./operations/reserves.js";
import { import {
runIntegrationTest, runIntegrationTest,
testPay, testPay,
withdrawTestBalance withdrawTestBalance,
} from "./operations/testing.js"; } from "./operations/testing.js";
import { acceptTip, prepareTip, processTip } from "./operations/tip.js"; import { acceptTip, prepareTip, processTip } from "./operations/tip.js";
import { import {
deleteTransaction, deleteTransaction,
getTransactions, getTransactions,
retryTransaction retryTransaction,
} from "./operations/transactions.js"; } from "./operations/transactions.js";
import { import {
getExchangeWithdrawalInfo, getExchangeWithdrawalInfo,
getWithdrawalDetailsForUri, getWithdrawalDetailsForUri,
processWithdrawGroup processWithdrawGroup,
} from "./operations/withdraw.js"; } from "./operations/withdraw.js";
import { import {
PendingOperationsResponse, PendingOperationsResponse,
PendingTaskInfo, PendingTaskInfo,
PendingTaskType PendingTaskType,
} from "./pending-types.js"; } from "./pending-types.js";
import { assertUnreachable } from "./util/assertUnreachable.js"; import { assertUnreachable } from "./util/assertUnreachable.js";
import { AsyncOpMemoMap, AsyncOpMemoSingle } from "./util/asyncMemo.js"; import { AsyncOpMemoMap, AsyncOpMemoSingle } from "./util/asyncMemo.js";
import { import {
HttpRequestLibrary, HttpRequestLibrary,
readSuccessResponseJsonOrThrow readSuccessResponseJsonOrThrow,
} from "./util/http.js"; } from "./util/http.js";
import { import {
AsyncCondition, AsyncCondition,
OpenedPromise, OpenedPromise,
openPromise openPromise,
} from "./util/promiseUtils.js"; } from "./util/promiseUtils.js";
import { DbAccess, GetReadWriteAccess } from "./util/query.js"; import { DbAccess, GetReadWriteAccess } from "./util/query.js";
import { TimerAPI, TimerGroup } from "./util/timer.js"; import { TimerAPI, TimerGroup } from "./util/timer.js";
@ -355,7 +362,6 @@ async function runTaskLoop(
if (p.givesLifeness) { if (p.givesLifeness) {
numGivingLiveness++; numGivingLiveness++;
} }
} }
if (opts.stopWhenDone && numGivingLiveness === 0 && iteration !== 0) { if (opts.stopWhenDone && numGivingLiveness === 0 && iteration !== 0) {
@ -459,13 +465,12 @@ async function acceptManualWithdrawal(
exchangeBaseUrl: string, exchangeBaseUrl: string,
amount: AmountJson, amount: AmountJson,
restrictAge?: number, restrictAge?: number,
): Promise<AcceptManualWithdrawalResult> { ): Promise<AcceptManualWithdrawalResult> {
try { try {
const resp = await createReserve(ws, { const resp = await createReserve(ws, {
amount, amount,
exchange: exchangeBaseUrl, exchange: exchangeBaseUrl,
restrictAge restrictAge,
}); });
const exchangePaytoUris = await ws.db const exchangePaytoUris = await ws.db
.mktx((x) => ({ .mktx((x) => ({
@ -688,7 +693,7 @@ async function dumpCoins(ws: InternalWalletState): Promise<CoinDumpJson> {
c.denomPubHash, c.denomPubHash,
); );
if (!denomInfo) { if (!denomInfo) {
console.error("no denomination found for coin") console.error("no denomination found for coin");
continue; continue;
} }
coinsJson.coins.push({ coinsJson.coins.push({
@ -749,22 +754,16 @@ async function dispatchRequestInternal(
return {}; return {};
} }
case "withdrawTestkudos": { case "withdrawTestkudos": {
await withdrawTestBalance( await withdrawTestBalance(ws, {
ws, amount: "TESTKUDOS:10",
"TESTKUDOS:10", bankBaseUrl: "https://bank.test.taler.net/",
"https://bank.test.taler.net/", exchangeBaseUrl: "https://exchange.test.taler.net/",
"https://exchange.test.taler.net/", });
);
return {}; return {};
} }
case "withdrawTestBalance": { case "withdrawTestBalance": {
const req = codecForWithdrawTestBalance().decode(payload); const req = codecForWithdrawTestBalance().decode(payload);
await withdrawTestBalance( await withdrawTestBalance(ws, req);
ws,
req.amount,
req.bankBaseUrl,
req.exchangeBaseUrl,
);
return {}; return {};
} }
case "runIntegrationTest": { case "runIntegrationTest": {
@ -774,8 +773,7 @@ async function dispatchRequestInternal(
} }
case "testPay": { case "testPay": {
const req = codecForTestPayArgs().decode(payload); const req = codecForTestPayArgs().decode(payload);
await testPay(ws, req); return await testPay(ws, req);
return {};
} }
case "getTransactions": { case "getTransactions": {
const req = codecForTransactionsRequest().decode(payload); const req = codecForTransactionsRequest().decode(payload);
@ -813,7 +811,7 @@ async function dispatchRequestInternal(
ws, ws,
req.exchangeBaseUrl, req.exchangeBaseUrl,
Amounts.parseOrThrow(req.amount), Amounts.parseOrThrow(req.amount),
req.restrictAge req.restrictAge,
); );
return res; return res;
} }