summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/operations
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2021-04-07 19:29:51 +0200
committerFlorian Dold <florian@dold.me>2021-04-07 19:29:51 +0200
commit4fa88007f958796d7fe65d0fe4f6f45fcf953887 (patch)
tree4f6e5798cc74b19b6eda13dfcd5daa855a5c8c9a /packages/taler-wallet-core/src/operations
parent29d710c392c2b28e8c8c2a177c8de40061a58e77 (diff)
downloadwallet-core-4fa88007f958796d7fe65d0fe4f6f45fcf953887.tar.gz
wallet-core-4fa88007f958796d7fe65d0fe4f6f45fcf953887.tar.bz2
wallet-core-4fa88007f958796d7fe65d0fe4f6f45fcf953887.zip
get coin re-selection after accidental double spending to work
Diffstat (limited to 'packages/taler-wallet-core/src/operations')
-rw-r--r--packages/taler-wallet-core/src/operations/backup/export.ts54
-rw-r--r--packages/taler-wallet-core/src/operations/backup/import.ts39
-rw-r--r--packages/taler-wallet-core/src/operations/backup/index.ts18
-rw-r--r--packages/taler-wallet-core/src/operations/deposits.ts4
-rw-r--r--packages/taler-wallet-core/src/operations/exchanges.ts2
-rw-r--r--packages/taler-wallet-core/src/operations/pay.ts103
-rw-r--r--packages/taler-wallet-core/src/operations/refresh.ts2
-rw-r--r--packages/taler-wallet-core/src/operations/reserves.ts48
-rw-r--r--packages/taler-wallet-core/src/operations/tip.ts2
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw.ts27
10 files changed, 259 insertions, 40 deletions
diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts b/packages/taler-wallet-core/src/operations/backup/export.ts
index c6e24289f..07c7b9ece 100644
--- a/packages/taler-wallet-core/src/operations/backup/export.ts
+++ b/packages/taler-wallet-core/src/operations/backup/export.ts
@@ -14,15 +14,6 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { hash } from "../../crypto/primitives/nacl-fast";
-import { WalletBackupContentV1, BackupExchange, BackupCoin, BackupDenomination, BackupReserve, BackupPurchase, BackupProposal, BackupRefreshGroup, BackupBackupProvider, BackupTip, BackupRecoupGroup, BackupWithdrawalGroup, BackupBackupProviderTerms, BackupCoinSource, BackupCoinSourceType, BackupExchangeWireFee, BackupRefundItem, BackupRefundState, BackupProposalStatus, BackupRefreshOldCoin, BackupRefreshSession } from "@gnu-taler/taler-util";
-import { canonicalizeBaseUrl, canonicalJson } from "../../util/helpers";
-import { InternalWalletState } from "../state";
-import { provideBackupState, getWalletBackupState, WALLET_BACKUP_STATE_KEY } from "./state";
-import { Amounts, getTimestampNow } from "@gnu-taler/taler-util";
-import { Stores, CoinSourceType, CoinStatus, RefundState, AbortStatus, ProposalStatus } from "../../db.js";
-import { encodeCrock, stringToBytes, getRandomBytes } from "../../index.js";
-
/**
* Implementation of wallet backups (export/import/upload) and sync
* server management.
@@ -30,6 +21,51 @@ import { encodeCrock, stringToBytes, getRandomBytes } from "../../index.js";
* @author Florian Dold <dold@taler.net>
*/
+/**
+ * Imports.
+ */
+import { hash } from "../../crypto/primitives/nacl-fast";
+import {
+ WalletBackupContentV1,
+ BackupExchange,
+ BackupCoin,
+ BackupDenomination,
+ BackupReserve,
+ BackupPurchase,
+ BackupProposal,
+ BackupRefreshGroup,
+ BackupBackupProvider,
+ BackupTip,
+ BackupRecoupGroup,
+ BackupWithdrawalGroup,
+ BackupBackupProviderTerms,
+ BackupCoinSource,
+ BackupCoinSourceType,
+ BackupExchangeWireFee,
+ BackupRefundItem,
+ BackupRefundState,
+ BackupProposalStatus,
+ BackupRefreshOldCoin,
+ BackupRefreshSession,
+} from "@gnu-taler/taler-util";
+import { InternalWalletState } from "../state";
+import {
+ provideBackupState,
+ getWalletBackupState,
+ WALLET_BACKUP_STATE_KEY,
+} from "./state";
+import { Amounts, getTimestampNow } from "@gnu-taler/taler-util";
+import {
+ Stores,
+ CoinSourceType,
+ CoinStatus,
+ RefundState,
+ AbortStatus,
+ ProposalStatus,
+} from "../../db.js";
+import { encodeCrock, stringToBytes, getRandomBytes } from "../../index.js";
+import { canonicalizeBaseUrl, canonicalJson } from "@gnu-taler/taler-util";
+
export async function exportBackup(
ws: InternalWalletState,
): Promise<WalletBackupContentV1> {
diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts
index 05b6da084..e0ae379ab 100644
--- a/packages/taler-wallet-core/src/operations/backup/import.ts
+++ b/packages/taler-wallet-core/src/operations/backup/import.ts
@@ -14,11 +14,42 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { BackupPurchase, AmountJson, Amounts, BackupDenomSel, WalletBackupContentV1, getTimestampNow, BackupCoinSourceType, BackupProposalStatus, codecForContractTerms, BackupRefundState, RefreshReason, BackupRefreshReason } from "@gnu-taler/taler-util";
-import { Stores, WalletContractData, DenomSelectionState, ExchangeWireInfo, ExchangeUpdateStatus, DenominationStatus, CoinSource, CoinSourceType, CoinStatus, ReserveBankInfo, ReserveRecordStatus, ProposalDownload, ProposalStatus, WalletRefundItem, RefundState, AbortStatus, RefreshSessionRecord } from "../../db.js";
+import {
+ BackupPurchase,
+ AmountJson,
+ Amounts,
+ BackupDenomSel,
+ WalletBackupContentV1,
+ getTimestampNow,
+ BackupCoinSourceType,
+ BackupProposalStatus,
+ codecForContractTerms,
+ BackupRefundState,
+ RefreshReason,
+ BackupRefreshReason,
+} from "@gnu-taler/taler-util";
+import {
+ Stores,
+ WalletContractData,
+ DenomSelectionState,
+ ExchangeWireInfo,
+ ExchangeUpdateStatus,
+ DenominationStatus,
+ CoinSource,
+ CoinSourceType,
+ CoinStatus,
+ ReserveBankInfo,
+ ReserveRecordStatus,
+ ProposalDownload,
+ ProposalStatus,
+ WalletRefundItem,
+ RefundState,
+ AbortStatus,
+ RefreshSessionRecord,
+} from "../../db.js";
import { TransactionHandle } from "../../index.js";
import { PayCoinSelection } from "../../util/coinSelection";
-import { j2s } from "../../util/helpers";
+import { j2s } from "@gnu-taler/taler-util";
import { checkDbInvariant, checkLogicInvariant } from "../../util/invariants";
import { Logger } from "../../util/logging";
import { initRetryInfo } from "../../util/retries";
@@ -271,6 +302,8 @@ export async function importBackup(
denomPubHash,
]);
if (!existingDenom) {
+ logger.info(`importing backup denomination: ${j2s(backupDenomination)}`);
+
await tx.put(Stores.denominations, {
denomPub: backupDenomination.denom_pub,
denomPubHash: denomPubHash,
diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts b/packages/taler-wallet-core/src/operations/backup/index.ts
index 77a3219a5..49129d7de 100644
--- a/packages/taler-wallet-core/src/operations/backup/index.ts
+++ b/packages/taler-wallet-core/src/operations/backup/index.ts
@@ -25,13 +25,14 @@
* Imports.
*/
import { InternalWalletState } from "../state";
-import { AmountString, BackupRecovery, codecForAmountString, WalletBackupContentV1 } from "@gnu-taler/taler-util";
-import { TransactionHandle } from "../../util/query";
import {
- BackupProviderRecord,
- ConfigRecord,
- Stores,
-} from "../../db.js";
+ AmountString,
+ BackupRecovery,
+ codecForAmountString,
+ WalletBackupContentV1,
+} from "@gnu-taler/taler-util";
+import { TransactionHandle } from "../../util/query";
+import { BackupProviderRecord, ConfigRecord, Stores } from "../../db.js";
import { checkDbInvariant, checkLogicInvariant } from "../../util/invariants";
import {
bytesToString,
@@ -43,7 +44,7 @@ import {
rsaBlind,
stringToBytes,
} from "../../crypto/talerCrypto";
-import { canonicalizeBaseUrl, canonicalJson, j2s } from "../../util/helpers";
+import { canonicalizeBaseUrl, canonicalJson, j2s } from "@gnu-taler/taler-util";
import {
durationAdd,
durationFromSpec,
@@ -408,6 +409,9 @@ export async function runBackupCycle(ws: InternalWalletState): Promise<void> {
const providers = await ws.db.iter(Stores.backupProviders).toArray();
logger.trace("got backup providers", providers);
const backupJson = await exportBackup(ws);
+
+ logger.trace(`running backup cycle with backup JSON: ${j2s(backupJson)}`);
+
const backupConfig = await provideBackupState(ws);
const encBackup = await encryptBackup(backupConfig, backupJson);
diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts
index 6bb4f3d59..4c87f122f 100644
--- a/packages/taler-wallet-core/src/operations/deposits.ts
+++ b/packages/taler-wallet-core/src/operations/deposits.ts
@@ -21,7 +21,7 @@ import {
stringToBytes,
} from "../crypto/talerCrypto";
import { selectPayCoins } from "../util/coinSelection";
-import { canonicalJson } from "../util/helpers";
+import { canonicalJson } from "@gnu-taler/taler-util";
import { readSuccessResponseJsonOrThrow } from "../util/http";
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries";
import {
@@ -433,4 +433,4 @@ export async function createDepositGroup(
await ws.db.put(Stores.depositGroups, depositGroup);
return { depositGroupId };
-} \ No newline at end of file
+}
diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts
index 08c554160..f48b08ff7 100644
--- a/packages/taler-wallet-core/src/operations/exchanges.ts
+++ b/packages/taler-wallet-core/src/operations/exchanges.ts
@@ -48,7 +48,7 @@ import {
getExpiryTimestamp,
readSuccessResponseTextOrThrow,
} from "../index.js";
-import { j2s, canonicalizeBaseUrl } from "../util/helpers.js";
+import { j2s, canonicalizeBaseUrl } from "@gnu-taler/taler-util";
import { checkDbInvariant } from "../util/invariants.js";
import { updateRetryInfoTimeout, initRetryInfo } from "../util/retries.js";
import {
diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts
index da3980565..1e93f413b 100644
--- a/packages/taler-wallet-core/src/operations/pay.ts
+++ b/packages/taler-wallet-core/src/operations/pay.ts
@@ -83,8 +83,9 @@ import {
CoinCandidateSelection,
AvailableCoinInfo,
selectPayCoins,
+ PreviousPayCoins,
} from "../util/coinSelection.js";
-import { canonicalJson } from "../util/helpers.js";
+import { canonicalJson, j2s } from "@gnu-taler/taler-util";
import {
initRetryInfo,
updateRetryInfoTimeout,
@@ -350,6 +351,13 @@ export async function applyCoinSpend(
if (!coin) {
throw Error("coin allocated for payment doesn't exist anymore");
}
+ if (coin.status !== CoinStatus.Fresh) {
+ // applyCoinSpend was called again, probably
+ // because of a coin re-selection to recover after
+ // accidental double spending.
+ // Ignore coins we already marked as spent.
+ continue;
+ }
coin.status = CoinStatus.Dormant;
const remaining = Amounts.sub(
coin.currentAmount,
@@ -867,7 +875,7 @@ async function storePayReplaySuccess(
*
* We do this by going through the coin history provided by the exchange and
* (1) verifying the signatures from the exchange
- * (2) adjusting the remaining coin value
+ * (2) adjusting the remaining coin value and refreshing it
* (3) re-do coin selection with the bad coin removed
*/
async function handleInsufficientFunds(
@@ -875,12 +883,99 @@ async function handleInsufficientFunds(
proposalId: string,
err: TalerErrorDetails,
): Promise<void> {
+ logger.trace("handling insufficient funds, trying to re-select coins");
+
const proposal = await ws.db.get(Stores.purchases, proposalId);
if (!proposal) {
return;
}
- throw Error("payment re-denomination not implemented yet");
+ const brokenCoinPub = (err as any).coin_pub;
+
+ const exchangeReply = (err as any).exchange_reply;
+ if (
+ exchangeReply.code !== TalerErrorCode.EXCHANGE_DEPOSIT_INSUFFICIENT_FUNDS
+ ) {
+ // FIXME: set as failed
+ throw Error("can't handle error code");
+ }
+
+ logger.trace(`got error details: ${j2s(err)}`);
+
+ const { contractData } = proposal.download;
+
+ const candidates = await getCandidatePayCoins(ws, {
+ allowedAuditors: contractData.allowedAuditors,
+ allowedExchanges: contractData.allowedExchanges,
+ amount: contractData.amount,
+ maxDepositFee: contractData.maxDepositFee,
+ maxWireFee: contractData.maxWireFee,
+ timestamp: contractData.timestamp,
+ wireFeeAmortization: contractData.wireFeeAmortization,
+ wireMethod: contractData.wireMethod,
+ });
+
+ const prevPayCoins: PreviousPayCoins = [];
+
+ for (let i = 0; i < proposal.payCoinSelection.coinPubs.length; i++) {
+ const coinPub = proposal.payCoinSelection.coinPubs[i];
+ if (coinPub === brokenCoinPub) {
+ continue;
+ }
+ const contrib = proposal.payCoinSelection.coinContributions[i];
+ const coin = await ws.db.get(Stores.coins, coinPub);
+ if (!coin) {
+ continue;
+ }
+ const denom = await ws.db.get(Stores.denominations, [
+ coin.exchangeBaseUrl,
+ coin.denomPubHash,
+ ]);
+ if (!denom) {
+ continue;
+ }
+ prevPayCoins.push({
+ coinPub,
+ contribution: contrib,
+ exchangeBaseUrl: coin.exchangeBaseUrl,
+ feeDeposit: denom.feeDeposit,
+ });
+ }
+
+ const res = selectPayCoins({
+ candidates,
+ contractTermsAmount: contractData.amount,
+ depositFeeLimit: contractData.maxDepositFee,
+ wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
+ wireFeeLimit: contractData.maxWireFee,
+ prevPayCoins,
+ });
+
+ if (!res) {
+ logger.trace("insufficient funds for coin re-selection");
+ return;
+ }
+
+ logger.trace("re-selected coins");
+
+ await ws.db.runWithWriteTransaction(
+ [
+ Stores.purchases,
+ Stores.coins,
+ Stores.denominations,
+ Stores.refreshGroups,
+ ],
+ async (tx) => {
+ const p = await tx.get(Stores.purchases, proposalId);
+ if (!p) {
+ return;
+ }
+ p.payCoinSelection = res;
+ p.coinDepositPermissions = undefined;
+ await tx.put(Stores.purchases, p);
+ await applyCoinSpend(ws, tx, res);
+ },
+ );
}
/**
@@ -973,7 +1068,7 @@ async function submitPay(
message: "unexpected exception",
hint: "unexpected exception",
details: {
- exception: e,
+ exception: e.toString(),
},
});
});
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts
index d82ff946e..84460fb88 100644
--- a/packages/taler-wallet-core/src/operations/refresh.ts
+++ b/packages/taler-wallet-core/src/operations/refresh.ts
@@ -34,7 +34,7 @@ import {
TalerErrorDetails,
} from "@gnu-taler/taler-util";
import { AmountJson, Amounts } from "@gnu-taler/taler-util";
-import { amountToPretty } from "../util/helpers";
+import { amountToPretty } from "@gnu-taler/taler-util";
import { readSuccessResponseJsonOrThrow } from "../util/http";
import { checkDbInvariant } from "../util/invariants";
import { Logger } from "../util/logging";
diff --git a/packages/taler-wallet-core/src/operations/reserves.ts b/packages/taler-wallet-core/src/operations/reserves.ts
index fe6f323c8..9467287a7 100644
--- a/packages/taler-wallet-core/src/operations/reserves.ts
+++ b/packages/taler-wallet-core/src/operations/reserves.ts
@@ -33,15 +33,46 @@ import {
addPaytoQueryParams,
} from "@gnu-taler/taler-util";
import { randomBytes } from "../crypto/primitives/nacl-fast.js";
-import { Stores, ReserveRecordStatus, ReserveBankInfo, ReserveRecord, CurrencyRecord, WithdrawalGroupRecord } from "../db.js";
-import { Logger, encodeCrock, getRandomBytes, readSuccessResponseJsonOrThrow, URL, readSuccessResponseJsonOrErrorCode, throwUnexpectedRequestError, TransactionHandle } from "../index.js";
+import {
+ Stores,
+ ReserveRecordStatus,
+ ReserveBankInfo,
+ ReserveRecord,
+ CurrencyRecord,
+ WithdrawalGroupRecord,
+} from "../db.js";
+import {
+ Logger,
+ encodeCrock,
+ getRandomBytes,
+ readSuccessResponseJsonOrThrow,
+ URL,
+ readSuccessResponseJsonOrErrorCode,
+ throwUnexpectedRequestError,
+ TransactionHandle,
+} from "../index.js";
import { assertUnreachable } from "../util/assertUnreachable.js";
-import { canonicalizeBaseUrl } from "../util/helpers.js";
-import { initRetryInfo, getRetryDuration, updateRetryInfoTimeout } from "../util/retries.js";
+import { canonicalizeBaseUrl } from "@gnu-taler/taler-util";
+import {
+ initRetryInfo,
+ getRetryDuration,
+ updateRetryInfoTimeout,
+} from "../util/retries.js";
import { guardOperationException, OperationFailedError } from "./errors.js";
-import { updateExchangeFromUrl, getExchangeTrust, getExchangePaytoUri } from "./exchanges.js";
+import {
+ updateExchangeFromUrl,
+ getExchangeTrust,
+ getExchangePaytoUri,
+} from "./exchanges.js";
import { InternalWalletState } from "./state.js";
-import { updateWithdrawalDenoms, getCandidateWithdrawalDenoms, selectWithdrawalDenominations, denomSelectionInfoToState, processWithdrawGroup, getBankWithdrawalInfo } from "./withdraw.js";
+import {
+ updateWithdrawalDenoms,
+ getCandidateWithdrawalDenoms,
+ selectWithdrawalDenominations,
+ denomSelectionInfoToState,
+ processWithdrawGroup,
+ getBankWithdrawalInfo,
+} from "./withdraw.js";
const logger = new Logger("reserves.ts");
@@ -488,7 +519,10 @@ async function updateReserve(
const currency = balance.currency;
await updateWithdrawalDenoms(ws, reserve.exchangeBaseUrl);
- const denoms = await getCandidateWithdrawalDenoms(ws, reserve.exchangeBaseUrl);
+ const denoms = await getCandidateWithdrawalDenoms(
+ ws,
+ reserve.exchangeBaseUrl,
+ );
const newWithdrawalGroup = await ws.db.runWithWriteTransaction(
[Stores.coins, Stores.planchets, Stores.withdrawalGroups, Stores.reserves],
diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts
index 5ea92912b..cc5274647 100644
--- a/packages/taler-wallet-core/src/operations/tip.ts
+++ b/packages/taler-wallet-core/src/operations/tip.ts
@@ -45,7 +45,7 @@ import {
getRandomBytes,
getHttpResponseErrorDetails,
} from "../index.js";
-import { j2s } from "../util/helpers.js";
+import { j2s } from "@gnu-taler/taler-util";
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js";
import { guardOperationException, makeErrorDetails } from "./errors.js";
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts
index 0c1acf8ec..fcaa0e6d5 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2019-2020 Taler Systems SA
+ (C) 2019-2021 Taler Systems SA
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
@@ -14,7 +14,15 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AmountJson, Amounts, parseWithdrawUri, Timestamp } from "@gnu-taler/taler-util";
+/**
+ * Imports.
+ */
+import {
+ AmountJson,
+ Amounts,
+ parseWithdrawUri,
+ Timestamp,
+} from "@gnu-taler/taler-util";
import {
DenominationRecord,
Stores,
@@ -67,15 +75,17 @@ import { TalerErrorCode } from "@gnu-taler/taler-util";
import { updateRetryInfoTimeout, initRetryInfo } from "../util/retries";
import { compare } from "@gnu-taler/taler-util";
+/**
+ * Logger for this file.
+ */
const logger = new Logger("withdraw.ts");
-
/**
* Information about what will happen when creating a reserve.
*
* Sent to the wallet frontend to be rendered and shown to the user.
*/
- interface ExchangeWithdrawDetails {
+interface ExchangeWithdrawDetails {
/**
* Exchange that the reserve will be created at.
*/
@@ -631,6 +641,8 @@ export async function updateWithdrawalDenoms(
logger.error("exchange details not available");
throw Error(`exchange ${exchangeBaseUrl} details not available`);
}
+ // First do a pass where the validity of candidate denominations
+ // is checked and the result is stored in the database.
const denominations = await getCandidateWithdrawalDenoms(ws, exchangeBaseUrl);
for (const denom of denominations) {
if (denom.status === DenominationStatus.Unverified) {
@@ -639,6 +651,9 @@ export async function updateWithdrawalDenoms(
exchangeDetails.masterPublicKey,
);
if (!valid) {
+ logger.warn(
+ `Signature check for denomination h=${denom.denomPubHash} failed`,
+ );
denom.status = DenominationStatus.VerifiedBad;
} else {
denom.status = DenominationStatus.VerifiedGood;
@@ -648,11 +663,13 @@ export async function updateWithdrawalDenoms(
}
// FIXME: This debug info should either be made conditional on some flag
// or put into some wallet-core API.
- logger.trace("updated withdrawable denominations");
const nextDenominations = await getCandidateWithdrawalDenoms(
ws,
exchangeBaseUrl,
);
+ logger.trace(
+ `updated withdrawable denominations for "${exchangeBaseUrl}, n=${nextDenominations.length}"`,
+ );
const now = getTimestampNow();
for (const denom of nextDenominations) {
const startDelay = getDurationRemaining(denom.stampStart, now);