aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/operations
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2022-06-10 13:03:47 +0200
committerFlorian Dold <florian@dold.me>2022-06-10 13:03:47 +0200
commitf57dc7bf7a1e3a14c67512ba67d92fa350c95c0e (patch)
treec4f94cd64373e787d8b43645e9fdca469e713a98 /packages/taler-wallet-core/src/operations
parent3ebb1d18154375471e329f2bad40534d858cbe1e (diff)
downloadwallet-core-f57dc7bf7a1e3a14c67512ba67d92fa350c95c0e.tar.xz
wallet-core: implement and test forced coin/denom selection
Diffstat (limited to 'packages/taler-wallet-core/src/operations')
-rw-r--r--packages/taler-wallet-core/src/operations/backup/import.ts3
-rw-r--r--packages/taler-wallet-core/src/operations/deposits.ts3
-rw-r--r--packages/taler-wallet-core/src/operations/pay.ts37
-rw-r--r--packages/taler-wallet-core/src/operations/reserves.ts108
-rw-r--r--packages/taler-wallet-core/src/operations/testing.ts76
5 files changed, 147 insertions, 80 deletions
diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts
index 16a88fe7c..3a9121502 100644
--- a/packages/taler-wallet-core/src/operations/backup/import.ts
+++ b/packages/taler-wallet-core/src/operations/backup/import.ts
@@ -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
diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts
index 41f051cb3..a016cb8e5 100644
--- a/packages/taler-wallet-core/src/operations/deposits.ts
+++ b/packages/taler-wallet-core/src/operations/deposits.ts
@@ -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";
diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts
index f22d51a9d..b6bae7518 100644
--- a/packages/taler-wallet-core/src/operations/pay.ts
+++ b/packages/taler-wallet-core/src/operations/pay.ts
@@ -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,15 +1483,28 @@ export async function confirmPay(
wireMethod: contractData.wireMethod,
});
- const res = selectPayCoins({
- candidates,
- contractTermsAmount: contractData.amount,
- depositFeeLimit: contractData.maxDepositFee,
- wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
- wireFeeLimit: contractData.maxWireFee,
- prevPayCoins: [],
- requiredMinimumAge: contractData.minimumAge,
- });
+ 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,
+ wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
+ wireFeeLimit: contractData.maxWireFee,
+ prevPayCoins: [],
+ requiredMinimumAge: contractData.minimumAge,
+ });
+ }
logger.trace("coin selection result", res);
diff --git a/packages/taler-wallet-core/src/operations/reserves.ts b/packages/taler-wallet-core/src/operations/reserves.ts
index d9fc8cf45..b33f574f4 100644
--- a/packages/taler-wallet-core/src/operations/reserves.ts
+++ b/packages/taler-wallet-core/src/operations/reserves.ts
@@ -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) {
- await registerReserveWithBank(ws, reservePub);
- return await processReserveBankStatus(ws, reservePub);
- }
- } else {
+ // Bank still needs to know our reserve info
+ if (!status.selection_done) {
+ await registerReserveWithBank(ws, reservePub);
+ return await processReserveBankStatus(ws, reservePub);
+ }
+
+ // FIXME: Why do we do this?!
+ if (reserve.reserveStatus === ReserveRecordStatus.RegisteringBank) {
await registerReserveWithBank(ws, reservePub);
return await processReserveBankStatus(ws, reservePub);
}
@@ -482,29 +494,26 @@ async function processReserveBankStatus(
if (!r) {
return;
}
+ // Re-check reserve status within transaction
+ switch (r.reserveStatus) {
+ case ReserveRecordStatus.RegisteringBank:
+ case ReserveRecordStatus.WaitConfirmBank:
+ break;
+ default:
+ return;
+ }
if (status.transfer_done) {
- switch (r.reserveStatus) {
- case ReserveRecordStatus.RegisteringBank:
- case ReserveRecordStatus.WaitConfirmBank:
- break;
- default:
- return;
- }
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),
});
@@ -553,7 +564,7 @@ async function updateReserve(
if (
resp.status === 404 &&
result.talerErrorResponse.code ===
- TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN
+ TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN
) {
ws.notify({
type: NotificationType.ReserveNotYetFound,
@@ -589,6 +600,7 @@ async function updateReserve(
if (!newReserve) {
return;
}
+
let amountReservePlus = reserveBalance;
let amountReserveMinus = Amounts.getZero(currency);
@@ -628,30 +640,33 @@ async function updateReserve(
amountReservePlus,
amountReserveMinus,
).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 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(
+ 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 = {
@@ -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.
diff --git a/packages/taler-wallet-core/src/operations/testing.ts b/packages/taler-wallet-core/src/operations/testing.ts
index 555e2d73d..d609011ca 100644
--- a/packages/taler-wallet-core/src/operations/testing.ts
+++ b/packages/taler-wallet-core/src/operations/testing.ts
@@ -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,
+ };
}