From 4fa88007f958796d7fe65d0fe4f6f45fcf953887 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 7 Apr 2021 19:29:51 +0200 Subject: [PATCH] get coin re-selection after accidental double spending to work --- .../test-wallet-backup-basic.ts | 30 +++- .../test-wallet-backup-doublespend.ts | 20 ++- packages/taler-wallet-core/src/db.ts | 1 + .../src/operations/backup/export.ts | 54 +++++-- .../src/operations/backup/import.ts | 39 ++++- .../src/operations/backup/index.ts | 18 ++- .../src/operations/deposits.ts | 4 +- .../src/operations/exchanges.ts | 2 +- .../taler-wallet-core/src/operations/pay.ts | 103 +++++++++++- .../src/operations/refresh.ts | 2 +- .../src/operations/reserves.ts | 48 +++++- .../taler-wallet-core/src/operations/tip.ts | 2 +- .../src/operations/withdraw.ts | 27 +++- .../src/util/coinSelection.ts | 4 +- .../src/util/helpers.test.ts | 46 ------ .../taler-wallet-core/src/util/helpers.ts | 151 ------------------ 16 files changed, 300 insertions(+), 251 deletions(-) delete mode 100644 packages/taler-wallet-core/src/util/helpers.test.ts delete mode 100644 packages/taler-wallet-core/src/util/helpers.ts 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 */ -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 */ +/** + * 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 { 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 */ -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 { 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 { + 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 */ -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 - */ - -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 - */ - -/** - * 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(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); -}