wallet-core: implement and test forced coin/denom selection
This commit is contained in:
parent
3ebb1d1815
commit
f57dc7bf7a
@ -33,7 +33,6 @@ import {
|
||||
codecForAmountString,
|
||||
} from "./amounts.js";
|
||||
import {
|
||||
AbsoluteTime,
|
||||
codecForTimestamp,
|
||||
TalerProtocolTimestamp,
|
||||
} from "./time.js";
|
||||
@ -231,6 +230,7 @@ export const codecForCreateReserveRequest = (): Codec<CreateReserveRequest> =>
|
||||
.property("exchangePaytoUri", codecForString())
|
||||
.property("senderWire", codecOptional(codecForString()))
|
||||
.property("bankWithdrawStatusUrl", codecOptional(codecForString()))
|
||||
.property("forcedDenomSel", codecForAny())
|
||||
.build("CreateReserveRequest");
|
||||
|
||||
/**
|
||||
@ -674,6 +674,7 @@ export interface TestPayArgs {
|
||||
merchantAuthToken?: string;
|
||||
amount: string;
|
||||
summary: string;
|
||||
forcedCoinSel?: ForcedCoinSel;
|
||||
}
|
||||
|
||||
export const codecForTestPayArgs = (): Codec<TestPayArgs> =>
|
||||
@ -682,6 +683,7 @@ export const codecForTestPayArgs = (): Codec<TestPayArgs> =>
|
||||
.property("merchantAuthToken", codecOptional(codecForString()))
|
||||
.property("amount", codecForString())
|
||||
.property("summary", codecForString())
|
||||
.property("forcedCoinSel", codecForAny())
|
||||
.build("TestPayArgs");
|
||||
|
||||
export interface IntegrationTestArgs {
|
||||
@ -738,7 +740,7 @@ export const codecForGetExchangeTosRequest = (): Codec<GetExchangeTosRequest> =>
|
||||
export interface AcceptManualWithdrawalRequest {
|
||||
exchangeBaseUrl: string;
|
||||
amount: string;
|
||||
restrictAge?: number,
|
||||
restrictAge?: number;
|
||||
}
|
||||
|
||||
export const codecForAcceptManualWithdrawalRequet =
|
||||
@ -803,7 +805,8 @@ export interface ApplyRefundFromPurchaseIdRequest {
|
||||
purchaseId: string;
|
||||
}
|
||||
|
||||
export const codecForApplyRefundFromPurchaseIdRequest = (): Codec<ApplyRefundFromPurchaseIdRequest> =>
|
||||
export const codecForApplyRefundFromPurchaseIdRequest =
|
||||
(): Codec<ApplyRefundFromPurchaseIdRequest> =>
|
||||
buildCodecForObject<ApplyRefundFromPurchaseIdRequest>()
|
||||
.property("purchaseId", codecForString())
|
||||
.build("ApplyRefundFromPurchaseIdRequest");
|
||||
@ -866,12 +869,14 @@ export const codecForPreparePayRequest = (): Codec<PreparePayRequest> =>
|
||||
export interface ConfirmPayRequest {
|
||||
proposalId: string;
|
||||
sessionId?: string;
|
||||
forcedCoinSel?: ForcedCoinSel;
|
||||
}
|
||||
|
||||
export const codecForConfirmPayRequest = (): Codec<ConfirmPayRequest> =>
|
||||
buildCodecForObject<ConfirmPayRequest>()
|
||||
.property("proposalId", codecForString())
|
||||
.property("sessionId", codecOptional(codecForString()))
|
||||
.property("forcedCoinSel", codecForAny())
|
||||
.build("ConfirmPay");
|
||||
|
||||
export type CoreApiResponse = CoreApiResponseSuccess | CoreApiResponseError;
|
||||
@ -903,6 +908,7 @@ export interface WithdrawTestBalanceRequest {
|
||||
amount: string;
|
||||
bankBaseUrl: string;
|
||||
exchangeBaseUrl: string;
|
||||
forcedDenomSel?: ForcedDenomSel;
|
||||
}
|
||||
|
||||
export const withdrawTestBalanceDefaults = {
|
||||
@ -976,6 +982,7 @@ export const codecForWithdrawTestBalance =
|
||||
.property("amount", codecForString())
|
||||
.property("bankBaseUrl", codecForString())
|
||||
.property("exchangeBaseUrl", codecForString())
|
||||
.property("forcedDenomSel", codecForAny())
|
||||
.build("WithdrawTestBalanceRequest");
|
||||
|
||||
export interface ApplyRefundResponse {
|
||||
@ -1026,8 +1033,6 @@ export const codecForForceRefreshRequest = (): Codec<ForceRefreshRequest> =>
|
||||
.property("coinPubList", codecForList(codecForString()))
|
||||
.build("ForceRefreshRequest");
|
||||
|
||||
|
||||
|
||||
export interface PrepareRefundRequest {
|
||||
talerRefundUri: string;
|
||||
}
|
||||
@ -1084,10 +1089,8 @@ export const codecForGetFeeForDeposit = (): Codec<GetFeeForDepositRequest> =>
|
||||
export interface PrepareDepositRequest {
|
||||
depositPaytoUri: string;
|
||||
amount: AmountString;
|
||||
|
||||
}
|
||||
export const codecForPrepareDepositRequest =
|
||||
(): Codec<PrepareDepositRequest> =>
|
||||
export const codecForPrepareDepositRequest = (): Codec<PrepareDepositRequest> =>
|
||||
buildCodecForObject<PrepareDepositRequest>()
|
||||
.property("amount", codecForAmountString())
|
||||
.property("depositPaytoUri", codecForString())
|
||||
@ -1203,6 +1206,7 @@ export const codecForWithdrawFakebankRequest =
|
||||
export interface ImportDb {
|
||||
dump: any;
|
||||
}
|
||||
|
||||
export const codecForImportDbRequest = (): Codec<ImportDb> =>
|
||||
buildCodecForObject<ImportDb>()
|
||||
.property("dump", codecForAny())
|
||||
@ -1214,3 +1218,49 @@ export interface ForcedDenomSel {
|
||||
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;
|
||||
}
|
@ -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"];
|
@ -34,6 +34,7 @@ import { runDepositTest } from "./test-deposit";
|
||||
import { runExchangeManagementTest } from "./test-exchange-management";
|
||||
import { runExchangeTimetravelTest } from "./test-exchange-timetravel.js";
|
||||
import { runFeeRegressionTest } from "./test-fee-regression";
|
||||
import { runForcedSelectionTest } from "./test-forced-selection.js";
|
||||
import { runLibeufinApiBankaccountTest } from "./test-libeufin-api-bankaccount";
|
||||
import { runLibeufinApiBankconnectionTest } from "./test-libeufin-api-bankconnection";
|
||||
import { runLibeufinApiFacadeTest } from "./test-libeufin-api-facade";
|
||||
@ -113,6 +114,7 @@ const allTests: TestMainFunction[] = [
|
||||
runExchangeManagementTest,
|
||||
runExchangeTimetravelTest,
|
||||
runFeeRegressionTest,
|
||||
runForcedSelectionTest,
|
||||
runLibeufinBasicTest,
|
||||
runLibeufinKeyrotationTest,
|
||||
runLibeufinTutorialTest,
|
||||
|
@ -41,9 +41,9 @@ import {
|
||||
TalerProtocolTimestamp,
|
||||
TalerProtocolDuration,
|
||||
AgeCommitmentProof,
|
||||
PayCoinSelection,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { RetryInfo } from "./util/retries.js";
|
||||
import { PayCoinSelection } from "./util/coinSelection.js";
|
||||
import { Event, IDBDatabase } from "@gnu-taler/idb-bridge";
|
||||
|
||||
/**
|
||||
|
@ -18,7 +18,7 @@ import {
|
||||
AmountJson,
|
||||
Amounts, BackupCoinSourceType, BackupDenomSel, BackupProposalStatus,
|
||||
BackupPurchase, BackupRefreshReason, BackupRefundState, codecForContractTerms,
|
||||
DenomKeyType, j2s, Logger, RefreshReason, TalerProtocolTimestamp,
|
||||
DenomKeyType, j2s, Logger, PayCoinSelection, RefreshReason, TalerProtocolTimestamp,
|
||||
WalletBackupContentV1
|
||||
} from "@gnu-taler/taler-util";
|
||||
import {
|
||||
@ -29,7 +29,6 @@ import {
|
||||
ReserveRecordStatus, WalletContractData, WalletRefundItem, WalletStoresV1, WireInfo
|
||||
} from "../../db.js";
|
||||
import { InternalWalletState } from "../../internal-wallet-state.js";
|
||||
import { PayCoinSelection } from "../../util/coinSelection.js";
|
||||
import {
|
||||
checkDbInvariant,
|
||||
checkLogicInvariant
|
||||
|
@ -35,6 +35,7 @@ import {
|
||||
Logger,
|
||||
NotificationType,
|
||||
parsePaytoUri,
|
||||
PayCoinSelection,
|
||||
PrepareDepositRequest,
|
||||
PrepareDepositResponse,
|
||||
TalerErrorDetail,
|
||||
@ -45,7 +46,7 @@ import {
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { DepositGroupRecord, OperationStatus, WireFee } from "../db.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 { RetryInfo } from "../util/retries.js";
|
||||
import { guardOperationException } from "./common.js";
|
||||
|
@ -40,12 +40,14 @@ import {
|
||||
durationMin,
|
||||
durationMul,
|
||||
encodeCrock,
|
||||
ForcedCoinSel,
|
||||
getRandomBytes,
|
||||
HttpStatusCode,
|
||||
j2s,
|
||||
Logger,
|
||||
NotificationType,
|
||||
parsePayUri,
|
||||
PayCoinSelection,
|
||||
PreparePayResult,
|
||||
PreparePayResultType,
|
||||
RefreshReason,
|
||||
@ -81,8 +83,8 @@ import {
|
||||
import {
|
||||
AvailableCoinInfo,
|
||||
CoinCandidateSelection,
|
||||
PayCoinSelection,
|
||||
PreviousPayCoins,
|
||||
selectForcedPayCoins,
|
||||
selectPayCoins,
|
||||
} from "../util/coinSelection.js";
|
||||
import { ContractTermsUtil } from "../util/contractTerms.js";
|
||||
@ -305,6 +307,7 @@ export async function getCandidatePayCoins(
|
||||
}
|
||||
candidateCoins.push({
|
||||
availableAmount: coin.currentAmount,
|
||||
value: denom.value,
|
||||
coinPub: coin.coinPub,
|
||||
denomPub: denom.denomPub,
|
||||
feeDeposit: denom.feeDeposit,
|
||||
@ -1423,6 +1426,7 @@ export async function confirmPay(
|
||||
ws: InternalWalletState,
|
||||
proposalId: string,
|
||||
sessionIdOverride?: string,
|
||||
forcedCoinSel?: ForcedCoinSel,
|
||||
): Promise<ConfirmPayResult> {
|
||||
logger.trace(
|
||||
`executing confirmPay with proposalId ${proposalId} and sessionIdOverride ${sessionIdOverride}`,
|
||||
@ -1479,7 +1483,19 @@ export async function confirmPay(
|
||||
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,
|
||||
contractTermsAmount: contractData.amount,
|
||||
depositFeeLimit: contractData.maxDepositFee,
|
||||
@ -1488,6 +1504,7 @@ export async function confirmPay(
|
||||
prevPayCoins: [],
|
||||
requiredMinimumAge: contractData.minimumAge,
|
||||
});
|
||||
}
|
||||
|
||||
logger.trace("coin selection result", res);
|
||||
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
AbsoluteTime,
|
||||
AcceptWithdrawalResponse,
|
||||
addPaytoQueryParams,
|
||||
Amounts,
|
||||
@ -28,6 +29,7 @@ import {
|
||||
durationMax,
|
||||
durationMin,
|
||||
encodeCrock,
|
||||
ForcedDenomSel,
|
||||
getRandomBytes,
|
||||
j2s,
|
||||
Logger,
|
||||
@ -35,13 +37,10 @@ import {
|
||||
randomBytes,
|
||||
TalerErrorCode,
|
||||
TalerErrorDetail,
|
||||
AbsoluteTime,
|
||||
URL,
|
||||
AmountString,
|
||||
ForcedDenomSel,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||
import {
|
||||
DenomSelectionState,
|
||||
OperationStatus,
|
||||
ReserveBankInfo,
|
||||
ReserveRecord,
|
||||
@ -50,6 +49,7 @@ import {
|
||||
WithdrawalGroupRecord,
|
||||
} from "../db.js";
|
||||
import { TalerError } from "../errors.js";
|
||||
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||
import { assertUnreachable } from "../util/assertUnreachable.js";
|
||||
import {
|
||||
readSuccessResponseJsonOrErrorCode,
|
||||
@ -57,9 +57,8 @@ import {
|
||||
throwUnexpectedRequestError,
|
||||
} from "../util/http.js";
|
||||
import { GetReadOnlyAccess } from "../util/query.js";
|
||||
import {
|
||||
RetryInfo,
|
||||
} from "../util/retries.js";
|
||||
import { RetryInfo } from "../util/retries.js";
|
||||
import { guardOperationException } from "./common.js";
|
||||
import {
|
||||
getExchangeDetails,
|
||||
getExchangePaytoUri,
|
||||
@ -70,10 +69,10 @@ import {
|
||||
getBankWithdrawalInfo,
|
||||
getCandidateWithdrawalDenoms,
|
||||
processWithdrawGroup,
|
||||
selectForcedWithdrawalDenominations,
|
||||
selectWithdrawalDenominations,
|
||||
updateWithdrawalDenoms,
|
||||
} from "./withdraw.js";
|
||||
import { guardOperationException } from "./common.js";
|
||||
|
||||
const logger = new Logger("taler-wallet-core:reserves.ts");
|
||||
|
||||
@ -178,7 +177,18 @@ export async function createReserve(
|
||||
|
||||
await updateWithdrawalDenoms(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 = {
|
||||
instructedAmount: req.amount,
|
||||
@ -436,7 +446,7 @@ async function processReserveBankStatus(
|
||||
);
|
||||
|
||||
if (status.aborted) {
|
||||
logger.trace("bank aborted the withdrawal");
|
||||
logger.info("bank aborted the withdrawal");
|
||||
await ws.db
|
||||
.mktx((x) => ({
|
||||
reserves: x.reserves,
|
||||
@ -463,12 +473,14 @@ async function processReserveBankStatus(
|
||||
return;
|
||||
}
|
||||
|
||||
if (status.selection_done) {
|
||||
if (reserve.reserveStatus === ReserveRecordStatus.RegisteringBank) {
|
||||
// Bank still needs to know our reserve info
|
||||
if (!status.selection_done) {
|
||||
await registerReserveWithBank(ws, reservePub);
|
||||
return await processReserveBankStatus(ws, reservePub);
|
||||
}
|
||||
} else {
|
||||
|
||||
// FIXME: Why do we do this?!
|
||||
if (reserve.reserveStatus === ReserveRecordStatus.RegisteringBank) {
|
||||
await registerReserveWithBank(ws, reservePub);
|
||||
return await processReserveBankStatus(ws, reservePub);
|
||||
}
|
||||
@ -482,7 +494,7 @@ async function processReserveBankStatus(
|
||||
if (!r) {
|
||||
return;
|
||||
}
|
||||
if (status.transfer_done) {
|
||||
// Re-check reserve status within transaction
|
||||
switch (r.reserveStatus) {
|
||||
case ReserveRecordStatus.RegisteringBank:
|
||||
case ReserveRecordStatus.WaitConfirmBank:
|
||||
@ -490,21 +502,18 @@ async function processReserveBankStatus(
|
||||
default:
|
||||
return;
|
||||
}
|
||||
if (status.transfer_done) {
|
||||
const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
|
||||
r.timestampBankConfirmed = now;
|
||||
r.reserveStatus = ReserveRecordStatus.QueryingStatus;
|
||||
r.operationStatus = OperationStatus.Pending;
|
||||
r.retryInfo = RetryInfo.reset();
|
||||
} else {
|
||||
switch (r.reserveStatus) {
|
||||
case ReserveRecordStatus.WaitConfirmBank:
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
logger.info("Withdrawal operation not yet confirmed by bank");
|
||||
if (r.bankInfo) {
|
||||
r.bankInfo.confirmUrl = status.confirm_transfer_url;
|
||||
}
|
||||
r.retryInfo = RetryInfo.increment(r.retryInfo);
|
||||
}
|
||||
await tx.reserves.put(r);
|
||||
});
|
||||
@ -540,6 +549,8 @@ async function updateReserve(
|
||||
const reserveUrl = new URL(`reserves/${reservePub}`, reserve.exchangeBaseUrl);
|
||||
reserveUrl.searchParams.set("timeout_ms", "30000");
|
||||
|
||||
logger.info(`querying reserve status via ${reserveUrl}`);
|
||||
|
||||
const resp = await ws.http.get(reserveUrl.href, {
|
||||
timeout: getReserveRequestTimeout(reserve),
|
||||
});
|
||||
@ -589,6 +600,7 @@ async function updateReserve(
|
||||
if (!newReserve) {
|
||||
return;
|
||||
}
|
||||
|
||||
let amountReservePlus = reserveBalance;
|
||||
let amountReserveMinus = Amounts.getZero(currency);
|
||||
|
||||
@ -628,7 +640,18 @@ async function updateReserve(
|
||||
amountReservePlus,
|
||||
amountReserveMinus,
|
||||
).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(
|
||||
`Remaining unclaimed amount in reseve is ${Amounts.stringify(
|
||||
@ -644,14 +667,6 @@ async function updateReserve(
|
||||
await tx.reserves.put(newReserve);
|
||||
return;
|
||||
}
|
||||
|
||||
let withdrawalGroupId: string;
|
||||
|
||||
if (!newReserve.initialWithdrawalStarted) {
|
||||
withdrawalGroupId = newReserve.initialWithdrawalGroupId;
|
||||
newReserve.initialWithdrawalStarted = true;
|
||||
} else {
|
||||
withdrawalGroupId = encodeCrock(randomBytes(32));
|
||||
}
|
||||
|
||||
const withdrawalRecord: WithdrawalGroupRecord = {
|
||||
@ -768,6 +783,7 @@ export async function createTalerWithdrawReserve(
|
||||
senderWire: withdrawInfo.senderWire,
|
||||
exchangePaytoUri: exchangePaytoUri,
|
||||
restrictAge: options.restrictAge,
|
||||
forcedDenomSel: options.forcedDenomSel,
|
||||
});
|
||||
// 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.
|
||||
|
@ -17,7 +17,12 @@
|
||||
/**
|
||||
* Imports.
|
||||
*/
|
||||
import { Logger } from "@gnu-taler/taler-util";
|
||||
import {
|
||||
ConfirmPayResultType,
|
||||
Logger,
|
||||
TestPayResult,
|
||||
WithdrawTestBalanceRequest,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import {
|
||||
HttpRequestLibrary,
|
||||
readSuccessResponseJsonOrThrow,
|
||||
@ -39,6 +44,7 @@ import { InternalWalletState } from "../internal-wallet-state.js";
|
||||
import { confirmPay, preparePayForUri } from "./pay.js";
|
||||
import { getBalances } from "./balance.js";
|
||||
import { applyRefund } from "./refund.js";
|
||||
import { checkLogicInvariant } from "../util/invariants.js";
|
||||
|
||||
const logger = new Logger("operations/testing.ts");
|
||||
|
||||
@ -82,10 +88,12 @@ function makeBasicAuthHeader(username: string, password: string): string {
|
||||
|
||||
export async function withdrawTestBalance(
|
||||
ws: InternalWalletState,
|
||||
amount = "TESTKUDOS:10",
|
||||
bankBaseUrl = "https://bank.test.taler.net/",
|
||||
exchangeBaseUrl = "https://exchange.test.taler.net/",
|
||||
req: WithdrawTestBalanceRequest,
|
||||
): Promise<void> {
|
||||
const bankBaseUrl = req.bankBaseUrl;
|
||||
const amount = req.amount;
|
||||
const exchangeBaseUrl = req.exchangeBaseUrl;
|
||||
|
||||
const bankUser = await registerRandomBankUser(ws.http, bankBaseUrl);
|
||||
logger.trace(`Registered bank user ${JSON.stringify(bankUser)}`);
|
||||
|
||||
@ -100,6 +108,9 @@ export async function withdrawTestBalance(
|
||||
ws,
|
||||
wresp.taler_withdraw_uri,
|
||||
exchangeBaseUrl,
|
||||
{
|
||||
forcedDenomSel: req.forcedDenomSel,
|
||||
},
|
||||
);
|
||||
|
||||
await confirmBankWithdrawalUri(
|
||||
@ -140,7 +151,10 @@ export async function createDemoBankWithdrawalUri(
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: makeBasicAuthHeader(bankUser.username, bankUser.password),
|
||||
Authorization: makeBasicAuthHeader(
|
||||
bankUser.username,
|
||||
bankUser.password,
|
||||
),
|
||||
},
|
||||
},
|
||||
);
|
||||
@ -163,7 +177,10 @@ async function confirmBankWithdrawalUri(
|
||||
{},
|
||||
{
|
||||
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;
|
||||
|
||||
logger.info("withdrawing test balance");
|
||||
await withdrawTestBalance(
|
||||
ws,
|
||||
args.amountToWithdraw,
|
||||
args.bankBaseUrl,
|
||||
args.exchangeBaseUrl,
|
||||
);
|
||||
await withdrawTestBalance(ws, {
|
||||
amount: args.amountToWithdraw,
|
||||
bankBaseUrl: args.bankBaseUrl,
|
||||
exchangeBaseUrl: args.exchangeBaseUrl,
|
||||
});
|
||||
await ws.runUntilDone();
|
||||
logger.info("done withdrawing test balance");
|
||||
|
||||
@ -360,12 +376,11 @@ export async function runIntegrationTest(
|
||||
const refundAmount = Amounts.parseOrThrow(`${currency}:6`);
|
||||
const spendAmountThree = Amounts.parseOrThrow(`${currency}:3`);
|
||||
|
||||
await withdrawTestBalance(
|
||||
ws,
|
||||
Amounts.stringify(withdrawAmountTwo),
|
||||
args.bankBaseUrl,
|
||||
args.exchangeBaseUrl,
|
||||
);
|
||||
await withdrawTestBalance(ws, {
|
||||
amount: Amounts.stringify(withdrawAmountTwo),
|
||||
bankBaseUrl: args.bankBaseUrl,
|
||||
exchangeBaseUrl: args.exchangeBaseUrl,
|
||||
});
|
||||
|
||||
// Wait until the withdraw is done
|
||||
await ws.runUntilDone();
|
||||
@ -410,7 +425,10 @@ export async function runIntegrationTest(
|
||||
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");
|
||||
const merchant = {
|
||||
authToken: args.merchantAuthToken,
|
||||
@ -429,12 +447,28 @@ export async function testPay(ws: InternalWalletState, args: TestPayArgs) {
|
||||
if (!talerPayUri) {
|
||||
console.error("fatal: no taler pay URI received from backend");
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
logger.trace("taler pay URI:", talerPayUri);
|
||||
const result = await preparePayForUri(ws, talerPayUri);
|
||||
if (result.status !== PreparePayResultType.PaymentPossible) {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ function a(x: string): AmountJson {
|
||||
|
||||
function fakeAci(current: string, feeDeposit: string): AvailableCoinInfo {
|
||||
return {
|
||||
value: a(current),
|
||||
availableAmount: a(current),
|
||||
coinPub: "foobar",
|
||||
denomPub: {
|
||||
@ -45,6 +46,7 @@ function fakeAci(current: string, feeDeposit: string): AvailableCoinInfo {
|
||||
|
||||
function fakeAciWithAgeRestriction(current: string, feeDeposit: string): AvailableCoinInfo {
|
||||
return {
|
||||
value: a(current),
|
||||
availableAmount: a(current),
|
||||
coinPub: "foobar",
|
||||
denomPub: {
|
||||
|
@ -29,42 +29,14 @@ import {
|
||||
AmountJson,
|
||||
Amounts,
|
||||
DenominationPubKey,
|
||||
ForcedCoinSel,
|
||||
Logger,
|
||||
PayCoinSelection,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { checkLogicInvariant } from "./invariants.js";
|
||||
|
||||
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
|
||||
* used in a payment.
|
||||
@ -82,6 +54,11 @@ export interface AvailableCoinInfo {
|
||||
*/
|
||||
denomPub: DenominationPubKey;
|
||||
|
||||
/**
|
||||
* Full value of the coin.
|
||||
*/
|
||||
value: AmountJson;
|
||||
|
||||
/**
|
||||
* Amount still remaining (typically the full amount,
|
||||
* as coins are always refreshed after use.)
|
||||
@ -356,3 +333,102 @@ export function selectPayCoins(
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ export interface RetryPolicy {
|
||||
|
||||
const defaultRetryPolicy: RetryPolicy = {
|
||||
backoffBase: 1.5,
|
||||
backoffDelta: Duration.fromSpec({ seconds: 30 }),
|
||||
backoffDelta: Duration.fromSpec({ seconds: 1 }),
|
||||
maxTimeout: Duration.fromSpec({ minutes: 2 }),
|
||||
};
|
||||
|
||||
|
@ -57,6 +57,7 @@ import {
|
||||
SetCoinSuspendedRequest,
|
||||
SetWalletDeviceIdRequest,
|
||||
TestPayArgs,
|
||||
TestPayResult,
|
||||
TrackDepositGroupRequest,
|
||||
TrackDepositGroupResponse,
|
||||
TransactionsRequest,
|
||||
@ -270,7 +271,7 @@ export type WalletOperations = {
|
||||
};
|
||||
[WalletApiOperation.TestPay]: {
|
||||
request: TestPayArgs;
|
||||
response: {};
|
||||
response: TestPayResult;
|
||||
};
|
||||
[WalletApiOperation.ExportDb]: {
|
||||
request: {};
|
||||
@ -279,11 +280,11 @@ export type WalletOperations = {
|
||||
};
|
||||
|
||||
export type RequestType<
|
||||
Op extends WalletApiOperation & keyof WalletOperations
|
||||
Op extends WalletApiOperation & keyof WalletOperations,
|
||||
> = WalletOperations[Op] extends { request: infer T } ? T : never;
|
||||
|
||||
export type ResponseType<
|
||||
Op extends WalletApiOperation & keyof WalletOperations
|
||||
Op extends WalletApiOperation & keyof WalletOperations,
|
||||
> = WalletOperations[Op] extends { response: infer T } ? T : never;
|
||||
|
||||
export interface WalletCoreApiClient {
|
||||
|
@ -23,7 +23,9 @@
|
||||
* Imports.
|
||||
*/
|
||||
import {
|
||||
AbsoluteTime, AcceptManualWithdrawalResult, AmountJson,
|
||||
AbsoluteTime,
|
||||
AcceptManualWithdrawalResult,
|
||||
AmountJson,
|
||||
Amounts,
|
||||
BalancesResponse,
|
||||
codecForAbortPayWithRefundRequest,
|
||||
@ -48,7 +50,9 @@ import {
|
||||
codecForIntegrationTestArgs,
|
||||
codecForListKnownBankAccounts,
|
||||
codecForPrepareDepositRequest,
|
||||
codecForPreparePayRequest, codecForPrepareRefundRequest, codecForPrepareTipRequest,
|
||||
codecForPreparePayRequest,
|
||||
codecForPrepareRefundRequest,
|
||||
codecForPrepareTipRequest,
|
||||
codecForRetryTransactionRequest,
|
||||
codecForSetCoinSuspendedRequest,
|
||||
codecForSetWalletDeviceIdRequest,
|
||||
@ -58,7 +62,9 @@ import {
|
||||
codecForWithdrawFakebankRequest,
|
||||
codecForWithdrawTestBalance,
|
||||
CoinDumpJson,
|
||||
CoreApiResponse, Duration, durationFromSpec,
|
||||
CoreApiResponse,
|
||||
Duration,
|
||||
durationFromSpec,
|
||||
durationMin,
|
||||
ExchangeListItem,
|
||||
ExchangesListRespose,
|
||||
@ -71,13 +77,14 @@ import {
|
||||
parsePaytoUri,
|
||||
PaytoUri,
|
||||
RefreshReason,
|
||||
TalerErrorCode, URL,
|
||||
WalletNotification
|
||||
TalerErrorCode,
|
||||
URL,
|
||||
WalletNotification,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
|
||||
import {
|
||||
CryptoDispatcher,
|
||||
CryptoWorkerFactory
|
||||
CryptoWorkerFactory,
|
||||
} from "./crypto/workers/cryptoDispatcher.js";
|
||||
import {
|
||||
AuditorTrustRecord,
|
||||
@ -85,7 +92,7 @@ import {
|
||||
exportDb,
|
||||
importDb,
|
||||
ReserveRecordStatus,
|
||||
WalletStoresV1
|
||||
WalletStoresV1,
|
||||
} from "./db.js";
|
||||
import { getErrorDetailFromException, TalerError } from "./errors.js";
|
||||
import {
|
||||
@ -96,7 +103,7 @@ import {
|
||||
MerchantOperations,
|
||||
NotificationListener,
|
||||
RecoupOperations,
|
||||
ReserveOperations
|
||||
ReserveOperations,
|
||||
} from "./internal-wallet-state.js";
|
||||
import { exportBackup } from "./operations/backup/export.js";
|
||||
import {
|
||||
@ -109,7 +116,7 @@ import {
|
||||
loadBackupRecovery,
|
||||
processBackupForProvider,
|
||||
removeBackupProvider,
|
||||
runBackupCycle
|
||||
runBackupCycle,
|
||||
} from "./operations/backup/index.js";
|
||||
import { setWalletDeviceId } from "./operations/backup/state.js";
|
||||
import { getBalances } from "./operations/balance.js";
|
||||
@ -118,7 +125,7 @@ import {
|
||||
getFeeForDeposit,
|
||||
prepareDepositGroup,
|
||||
processDepositGroup,
|
||||
trackDepositGroup
|
||||
trackDepositGroup,
|
||||
} from "./operations/deposits.js";
|
||||
import {
|
||||
acceptExchangeTermsOfService,
|
||||
@ -127,66 +134,66 @@ import {
|
||||
getExchangeRequestTimeout,
|
||||
getExchangeTrust,
|
||||
updateExchangeFromUrl,
|
||||
updateExchangeTermsOfService
|
||||
updateExchangeTermsOfService,
|
||||
} from "./operations/exchanges.js";
|
||||
import { getMerchantInfo } from "./operations/merchants.js";
|
||||
import {
|
||||
confirmPay,
|
||||
preparePayForUri,
|
||||
processDownloadProposal,
|
||||
processPurchasePay
|
||||
processPurchasePay,
|
||||
} from "./operations/pay.js";
|
||||
import { getPendingOperations } from "./operations/pending.js";
|
||||
import { createRecoupGroup, processRecoupGroup } from "./operations/recoup.js";
|
||||
import {
|
||||
autoRefresh,
|
||||
createRefreshGroup,
|
||||
processRefreshGroup
|
||||
processRefreshGroup,
|
||||
} from "./operations/refresh.js";
|
||||
import {
|
||||
abortFailedPayWithRefund,
|
||||
applyRefund,
|
||||
applyRefundFromPurchaseId,
|
||||
prepareRefund,
|
||||
processPurchaseQueryRefund
|
||||
processPurchaseQueryRefund,
|
||||
} from "./operations/refund.js";
|
||||
import {
|
||||
createReserve,
|
||||
createTalerWithdrawReserve,
|
||||
getFundingPaytoUris,
|
||||
processReserve
|
||||
processReserve,
|
||||
} from "./operations/reserves.js";
|
||||
import {
|
||||
runIntegrationTest,
|
||||
testPay,
|
||||
withdrawTestBalance
|
||||
withdrawTestBalance,
|
||||
} from "./operations/testing.js";
|
||||
import { acceptTip, prepareTip, processTip } from "./operations/tip.js";
|
||||
import {
|
||||
deleteTransaction,
|
||||
getTransactions,
|
||||
retryTransaction
|
||||
retryTransaction,
|
||||
} from "./operations/transactions.js";
|
||||
import {
|
||||
getExchangeWithdrawalInfo,
|
||||
getWithdrawalDetailsForUri,
|
||||
processWithdrawGroup
|
||||
processWithdrawGroup,
|
||||
} from "./operations/withdraw.js";
|
||||
import {
|
||||
PendingOperationsResponse,
|
||||
PendingTaskInfo,
|
||||
PendingTaskType
|
||||
PendingTaskType,
|
||||
} from "./pending-types.js";
|
||||
import { assertUnreachable } from "./util/assertUnreachable.js";
|
||||
import { AsyncOpMemoMap, AsyncOpMemoSingle } from "./util/asyncMemo.js";
|
||||
import {
|
||||
HttpRequestLibrary,
|
||||
readSuccessResponseJsonOrThrow
|
||||
readSuccessResponseJsonOrThrow,
|
||||
} from "./util/http.js";
|
||||
import {
|
||||
AsyncCondition,
|
||||
OpenedPromise,
|
||||
openPromise
|
||||
openPromise,
|
||||
} from "./util/promiseUtils.js";
|
||||
import { DbAccess, GetReadWriteAccess } from "./util/query.js";
|
||||
import { TimerAPI, TimerGroup } from "./util/timer.js";
|
||||
@ -355,7 +362,6 @@ async function runTaskLoop(
|
||||
if (p.givesLifeness) {
|
||||
numGivingLiveness++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (opts.stopWhenDone && numGivingLiveness === 0 && iteration !== 0) {
|
||||
@ -459,13 +465,12 @@ async function acceptManualWithdrawal(
|
||||
exchangeBaseUrl: string,
|
||||
amount: AmountJson,
|
||||
restrictAge?: number,
|
||||
|
||||
): Promise<AcceptManualWithdrawalResult> {
|
||||
try {
|
||||
const resp = await createReserve(ws, {
|
||||
amount,
|
||||
exchange: exchangeBaseUrl,
|
||||
restrictAge
|
||||
restrictAge,
|
||||
});
|
||||
const exchangePaytoUris = await ws.db
|
||||
.mktx((x) => ({
|
||||
@ -688,7 +693,7 @@ async function dumpCoins(ws: InternalWalletState): Promise<CoinDumpJson> {
|
||||
c.denomPubHash,
|
||||
);
|
||||
if (!denomInfo) {
|
||||
console.error("no denomination found for coin")
|
||||
console.error("no denomination found for coin");
|
||||
continue;
|
||||
}
|
||||
coinsJson.coins.push({
|
||||
@ -749,22 +754,16 @@ async function dispatchRequestInternal(
|
||||
return {};
|
||||
}
|
||||
case "withdrawTestkudos": {
|
||||
await withdrawTestBalance(
|
||||
ws,
|
||||
"TESTKUDOS:10",
|
||||
"https://bank.test.taler.net/",
|
||||
"https://exchange.test.taler.net/",
|
||||
);
|
||||
await withdrawTestBalance(ws, {
|
||||
amount: "TESTKUDOS:10",
|
||||
bankBaseUrl: "https://bank.test.taler.net/",
|
||||
exchangeBaseUrl: "https://exchange.test.taler.net/",
|
||||
});
|
||||
return {};
|
||||
}
|
||||
case "withdrawTestBalance": {
|
||||
const req = codecForWithdrawTestBalance().decode(payload);
|
||||
await withdrawTestBalance(
|
||||
ws,
|
||||
req.amount,
|
||||
req.bankBaseUrl,
|
||||
req.exchangeBaseUrl,
|
||||
);
|
||||
await withdrawTestBalance(ws, req);
|
||||
return {};
|
||||
}
|
||||
case "runIntegrationTest": {
|
||||
@ -774,8 +773,7 @@ async function dispatchRequestInternal(
|
||||
}
|
||||
case "testPay": {
|
||||
const req = codecForTestPayArgs().decode(payload);
|
||||
await testPay(ws, req);
|
||||
return {};
|
||||
return await testPay(ws, req);
|
||||
}
|
||||
case "getTransactions": {
|
||||
const req = codecForTransactionsRequest().decode(payload);
|
||||
@ -813,7 +811,7 @@ async function dispatchRequestInternal(
|
||||
ws,
|
||||
req.exchangeBaseUrl,
|
||||
Amounts.parseOrThrow(req.amount),
|
||||
req.restrictAge
|
||||
req.restrictAge,
|
||||
);
|
||||
return res;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user