aboutsummaryrefslogtreecommitdiff
path: root/packages
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
parent29d710c392c2b28e8c8c2a177c8de40061a58e77 (diff)
get coin re-selection after accidental double spending to work
Diffstat (limited to 'packages')
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-basic.ts30
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-doublespend.ts20
-rw-r--r--packages/taler-wallet-core/src/db.ts1
-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
-rw-r--r--packages/taler-wallet-core/src/util/coinSelection.ts4
-rw-r--r--packages/taler-wallet-core/src/util/helpers.test.ts46
-rw-r--r--packages/taler-wallet-core/src/util/helpers.ts151
16 files changed, 300 insertions, 251 deletions
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-basic.ts b/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-basic.ts
index 2ed16fe19..dd448c87d 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-basic.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-basic.ts
@@ -17,12 +17,8 @@
/**
* Imports.
*/
-import { GlobalTestState, BankApi, BankAccessApi, WalletCli } from "./harness";
-import {
- createSimpleTestkudosEnvironment,
- makeTestPayment,
- withdrawViaBank,
-} from "./helpers";
+import { GlobalTestState, WalletCli } from "./harness";
+import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers";
import { SyncService } from "./sync";
/**
@@ -101,7 +97,7 @@ export async function runWalletBackupBasicTest(t: GlobalTestState) {
const bi = await wallet.getBackupInfo();
console.log(bi);
}
-
+
const backupRecovery = await wallet.exportBackupRecovery();
const wallet2 = new WalletCli(t, "wallet2");
@@ -122,4 +118,24 @@ export async function runWalletBackupBasicTest(t: GlobalTestState) {
t.assertTrue(bal.balances.length === 1);
console.log(bal);
}
+
+ // Now do some basic checks that the restored wallet is still functional
+ {
+ const bal1 = await wallet2.getBalances();
+
+ t.assertAmountEquals(bal1.balances[0].available, "TESTKUDOS:14.1");
+
+ await withdrawViaBank(t, {
+ wallet: wallet2,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:10",
+ });
+
+ await wallet2.runUntilDone();
+
+ const bal2 = await wallet2.getBalances();
+
+ t.assertAmountEquals(bal2.balances[0].available, "TESTKUDOS:23.82");
+ }
}
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-doublespend.ts b/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-doublespend.ts
index ef53046cd..b9bc30a95 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-doublespend.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-doublespend.ts
@@ -18,11 +18,7 @@
* Imports.
*/
import { PreparePayResultType } from "@gnu-taler/taler-util";
-import {
- GlobalTestState,
- WalletCli,
- MerchantPrivateApi,
-} from "./harness";
+import { GlobalTestState, WalletCli, MerchantPrivateApi } from "./harness";
import {
createSimpleTestkudosEnvironment,
makeTestPayment,
@@ -133,5 +129,19 @@ export async function runWalletBackupDoublespendTest(t: GlobalTestState) {
});
console.log(res);
+
+ // FIXME: wait for a notification that indicates insufficient funds!
+
+ await withdrawViaBank(t, {
+ wallet: wallet2,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:50",
+ });
+
+ const bal = await wallet2.getBalances();
+ console.log("bal", bal);
+
+ await wallet2.runUntilDone();
}
}
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index c1076b900..640ff24af 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -1274,6 +1274,7 @@ export enum AbortStatus {
AbortFinished = "abort-finished",
}
+
/**
* Record that stores status information about one purchase, starting from when
* the customer accepts a proposal. Includes refund status if applicable.
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);
diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts b/packages/taler-wallet-core/src/util/coinSelection.ts
index e1fec5c97..c5a75878f 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.ts
+++ b/packages/taler-wallet-core/src/util/coinSelection.ts
@@ -24,7 +24,7 @@
* Imports.
*/
import { AmountJson, AmountLike, Amounts } from "@gnu-taler/taler-util";
-import { strcmp } from "./helpers.js";
+import { strcmp } from "@gnu-taler/taler-util";
import { Logger } from "./logging.js";
const logger = new Logger("coinSelection.ts");
@@ -89,7 +89,7 @@ export interface AvailableCoinInfo {
exchangeBaseUrl: string;
}
-type PreviousPayCoins = {
+export type PreviousPayCoins = {
coinPub: string;
contribution: AmountJson;
feeDeposit: AmountJson;
diff --git a/packages/taler-wallet-core/src/util/helpers.test.ts b/packages/taler-wallet-core/src/util/helpers.test.ts
deleted file mode 100644
index dbecf14b8..000000000
--- a/packages/taler-wallet-core/src/util/helpers.test.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- This file is part of TALER
- (C) 2017 Inria and GNUnet e.V.
-
- 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.
-
- 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
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import test from "ava";
-import * as helpers from "./helpers";
-
-test("URL canonicalization", (t) => {
- // converts to relative, adds https
- t.is(
- "https://alice.example.com/exchange/",
- helpers.canonicalizeBaseUrl("alice.example.com/exchange"),
- );
-
- // keeps http, adds trailing slash
- t.is(
- "http://alice.example.com/exchange/",
- helpers.canonicalizeBaseUrl("http://alice.example.com/exchange"),
- );
-
- // keeps http, adds trailing slash
- t.is(
- "http://alice.example.com/exchange/",
- helpers.canonicalizeBaseUrl("http://alice.example.com/exchange#foobar"),
- );
-
- // Remove search component
- t.is(
- "http://alice.example.com/exchange/",
- helpers.canonicalizeBaseUrl("http://alice.example.com/exchange?foo=bar"),
- );
-
- t.pass();
-});
diff --git a/packages/taler-wallet-core/src/util/helpers.ts b/packages/taler-wallet-core/src/util/helpers.ts
deleted file mode 100644
index 87fa2e93f..000000000
--- a/packages/taler-wallet-core/src/util/helpers.ts
+++ /dev/null
@@ -1,151 +0,0 @@
-/*
- This file is part of TALER
- (C) 2016 GNUnet e.V.
-
- 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.
-
- 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
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Small helper functions that don't fit anywhere else.
- */
-
-/**
- * Imports.
- */
-import { amountFractionalBase, AmountJson, Amounts } from "@gnu-taler/taler-util";
-import { URL } from "./url";
-
-/**
- * Show an amount in a form suitable for the user.
- * FIXME: In the future, this should consider currency-specific
- * settings such as significant digits or currency symbols.
- */
-export function amountToPretty(amount: AmountJson): string {
- const x = amount.value + amount.fraction / amountFractionalBase;
- return `${x} ${amount.currency}`;
-}
-
-/**
- * Canonicalize a base url, typically for the exchange.
- *
- * See http://api.taler.net/wallet.html#general
- */
-export function canonicalizeBaseUrl(url: string): string {
- if (!url.startsWith("http") && !url.startsWith("https")) {
- url = "https://" + url;
- }
- const x = new URL(url);
- if (!x.pathname.endsWith("/")) {
- x.pathname = x.pathname + "/";
- }
- x.search = "";
- x.hash = "";
- return x.href;
-}
-
-/**
- * Convert object to JSON with canonical ordering of keys
- * and whitespace omitted.
- */
-export function canonicalJson(obj: any): string {
- // Check for cycles, etc.
- obj = JSON.parse(JSON.stringify(obj));
- if (typeof obj === "string" || typeof obj === "number" || obj === null) {
- return JSON.stringify(obj);
- }
- if (Array.isArray(obj)) {
- const objs: string[] = obj.map((e) => canonicalJson(e));
- return `[${objs.join(",")}]`;
- }
- const keys: string[] = [];
- for (const key in obj) {
- keys.push(key);
- }
- keys.sort();
- let s = "{";
- for (let i = 0; i < keys.length; i++) {
- const key = keys[i];
- s += JSON.stringify(key) + ":" + canonicalJson(obj[key]);
- if (i !== keys.length - 1) {
- s += ",";
- }
- }
- return s + "}";
-}
-
-/**
- * Check for deep equality of two objects.
- * Only arrays, objects and primitives are supported.
- */
-export function deepEquals(x: any, y: any): boolean {
- if (x === y) {
- return true;
- }
-
- if (Array.isArray(x) && x.length !== y.length) {
- return false;
- }
-
- const p = Object.keys(x);
- return (
- Object.keys(y).every((i) => p.indexOf(i) !== -1) &&
- p.every((i) => deepEquals(x[i], y[i]))
- );
-}
-
-export function deepCopy(x: any): any {
- // FIXME: this has many issues ...
- return JSON.parse(JSON.stringify(x));
-}
-
-/**
- * Map from a collection to a list or results and then
- * concatenate the results.
- */
-export function flatMap<T, U>(xs: T[], f: (x: T) => U[]): U[] {
- return xs.reduce((acc: U[], next: T) => [...f(next), ...acc], []);
-}
-
-/**
- * Compute the hash function of a JSON object.
- */
-export function hash(val: any): number {
- const str = canonicalJson(val);
- // https://github.com/darkskyapp/string-hash
- let h = 5381;
- let i = str.length;
- while (i) {
- h = (h * 33) ^ str.charCodeAt(--i);
- }
-
- /* JavaScript does bitwise operations (like XOR, above) on 32-bit signed
- * integers. Since we want the results to be always positive, convert the
- * signed int to an unsigned by doing an unsigned bitshift. */
- return h >>> 0;
-}
-
-/**
- * Lexically compare two strings.
- */
-export function strcmp(s1: string, s2: string): number {
- if (s1 < s2) {
- return -1;
- }
- if (s1 > s2) {
- return 1;
- }
- return 0;
-}
-
-export function j2s(x: any): string {
- return JSON.stringify(x, undefined, 2);
-}