wallet-core: implement and test forced coin/denom selection
This commit is contained in:
parent
3ebb1d1815
commit
f57dc7bf7a
@ -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,10 +805,11 @@ export interface ApplyRefundFromPurchaseIdRequest {
|
|||||||
purchaseId: string;
|
purchaseId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const codecForApplyRefundFromPurchaseIdRequest = (): Codec<ApplyRefundFromPurchaseIdRequest> =>
|
export const codecForApplyRefundFromPurchaseIdRequest =
|
||||||
buildCodecForObject<ApplyRefundFromPurchaseIdRequest>()
|
(): Codec<ApplyRefundFromPurchaseIdRequest> =>
|
||||||
.property("purchaseId", codecForString())
|
buildCodecForObject<ApplyRefundFromPurchaseIdRequest>()
|
||||||
.build("ApplyRefundFromPurchaseIdRequest");
|
.property("purchaseId", codecForString())
|
||||||
|
.build("ApplyRefundFromPurchaseIdRequest");
|
||||||
|
|
||||||
export interface GetWithdrawalDetailsForUriRequest {
|
export interface GetWithdrawalDetailsForUriRequest {
|
||||||
talerWithdrawUri: string;
|
talerWithdrawUri: string;
|
||||||
@ -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,14 +1089,12 @@ 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())
|
.build("PrepareDepositRequest");
|
||||||
.build("PrepareDepositRequest");
|
|
||||||
|
|
||||||
export interface PrepareDepositResponse {
|
export interface PrepareDepositResponse {
|
||||||
totalDepositCost: AmountJson;
|
totalDepositCost: AmountJson;
|
||||||
@ -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;
|
||||||
|
}
|
@ -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 { 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,
|
||||||
|
@ -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";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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
|
||||||
|
@ -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";
|
||||||
|
@ -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,15 +1483,28 @@ export async function confirmPay(
|
|||||||
wireMethod: contractData.wireMethod,
|
wireMethod: contractData.wireMethod,
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = selectPayCoins({
|
let res: PayCoinSelection | undefined = undefined;
|
||||||
candidates,
|
|
||||||
contractTermsAmount: contractData.amount,
|
if (forcedCoinSel) {
|
||||||
depositFeeLimit: contractData.maxDepositFee,
|
res = selectForcedPayCoins(forcedCoinSel, {
|
||||||
wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
|
candidates,
|
||||||
wireFeeLimit: contractData.maxWireFee,
|
contractTermsAmount: contractData.amount,
|
||||||
prevPayCoins: [],
|
depositFeeLimit: contractData.maxDepositFee,
|
||||||
requiredMinimumAge: contractData.minimumAge,
|
wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
|
||||||
});
|
wireFeeLimit: contractData.maxWireFee,
|
||||||
|
requiredMinimumAge: contractData.minimumAge,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res = selectPayCoins({
|
||||||
|
candidates,
|
||||||
|
contractTermsAmount: contractData.amount,
|
||||||
|
depositFeeLimit: contractData.maxDepositFee,
|
||||||
|
wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
|
||||||
|
wireFeeLimit: contractData.maxWireFee,
|
||||||
|
prevPayCoins: [],
|
||||||
|
requiredMinimumAge: contractData.minimumAge,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
logger.trace("coin selection result", res);
|
logger.trace("coin selection result", res);
|
||||||
|
|
||||||
|
@ -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,29 +494,26 @@ async function processReserveBankStatus(
|
|||||||
if (!r) {
|
if (!r) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Re-check reserve status within transaction
|
||||||
|
switch (r.reserveStatus) {
|
||||||
|
case ReserveRecordStatus.RegisteringBank:
|
||||||
|
case ReserveRecordStatus.WaitConfirmBank:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (status.transfer_done) {
|
if (status.transfer_done) {
|
||||||
switch (r.reserveStatus) {
|
|
||||||
case ReserveRecordStatus.RegisteringBank:
|
|
||||||
case ReserveRecordStatus.WaitConfirmBank:
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
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),
|
||||||
});
|
});
|
||||||
@ -553,7 +564,7 @@ async function updateReserve(
|
|||||||
if (
|
if (
|
||||||
resp.status === 404 &&
|
resp.status === 404 &&
|
||||||
result.talerErrorResponse.code ===
|
result.talerErrorResponse.code ===
|
||||||
TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN
|
TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN
|
||||||
) {
|
) {
|
||||||
ws.notify({
|
ws.notify({
|
||||||
type: NotificationType.ReserveNotYetFound,
|
type: NotificationType.ReserveNotYetFound,
|
||||||
@ -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,30 +640,33 @@ async function updateReserve(
|
|||||||
amountReservePlus,
|
amountReservePlus,
|
||||||
amountReserveMinus,
|
amountReserveMinus,
|
||||||
).amount;
|
).amount;
|
||||||
const denomSel = selectWithdrawalDenominations(remainingAmount, denoms);
|
|
||||||
|
|
||||||
logger.trace(
|
|
||||||
`Remaining unclaimed amount in reseve is ${Amounts.stringify(
|
|
||||||
remainingAmount,
|
|
||||||
)} and can be withdrawn with ${denomSel.selectedDenoms.length} coins`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (denomSel.selectedDenoms.length === 0) {
|
|
||||||
newReserve.reserveStatus = ReserveRecordStatus.Dormant;
|
|
||||||
newReserve.operationStatus = OperationStatus.Finished;
|
|
||||||
delete newReserve.lastError;
|
|
||||||
delete newReserve.retryInfo;
|
|
||||||
await tx.reserves.put(newReserve);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let withdrawalGroupId: string;
|
let withdrawalGroupId: string;
|
||||||
|
let denomSel: DenomSelectionState;
|
||||||
|
|
||||||
if (!newReserve.initialWithdrawalStarted) {
|
if (!newReserve.initialWithdrawalStarted) {
|
||||||
withdrawalGroupId = newReserve.initialWithdrawalGroupId;
|
withdrawalGroupId = newReserve.initialWithdrawalGroupId;
|
||||||
newReserve.initialWithdrawalStarted = true;
|
newReserve.initialWithdrawalStarted = true;
|
||||||
|
denomSel = newReserve.initialDenomSel;
|
||||||
} else {
|
} else {
|
||||||
withdrawalGroupId = encodeCrock(randomBytes(32));
|
withdrawalGroupId = encodeCrock(randomBytes(32));
|
||||||
|
|
||||||
|
denomSel = selectWithdrawalDenominations(remainingAmount, denoms);
|
||||||
|
|
||||||
|
logger.trace(
|
||||||
|
`Remaining unclaimed amount in reseve is ${Amounts.stringify(
|
||||||
|
remainingAmount,
|
||||||
|
)} and can be withdrawn with ${denomSel.selectedDenoms.length} coins`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (denomSel.selectedDenoms.length === 0) {
|
||||||
|
newReserve.reserveStatus = ReserveRecordStatus.Dormant;
|
||||||
|
newReserve.operationStatus = OperationStatus.Finished;
|
||||||
|
delete newReserve.lastError;
|
||||||
|
delete newReserve.retryInfo;
|
||||||
|
await tx.reserves.put(newReserve);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
||||||
|
@ -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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -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: {
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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 }),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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,12 +280,12 @@ 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 {
|
||||||
call<Op extends WalletApiOperation & keyof WalletOperations>(
|
call<Op extends WalletApiOperation & keyof WalletOperations>(
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user