get coin re-selection after accidental double spending to work

This commit is contained in:
Florian Dold 2021-04-07 19:29:51 +02:00
parent 29d710c392
commit 4fa88007f9
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
16 changed files with 300 additions and 251 deletions

View File

@ -17,12 +17,8 @@
/** /**
* Imports. * Imports.
*/ */
import { GlobalTestState, BankApi, BankAccessApi, WalletCli } from "./harness"; import { GlobalTestState, WalletCli } from "./harness";
import { import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers";
createSimpleTestkudosEnvironment,
makeTestPayment,
withdrawViaBank,
} from "./helpers";
import { SyncService } from "./sync"; import { SyncService } from "./sync";
/** /**
@ -122,4 +118,24 @@ export async function runWalletBackupBasicTest(t: GlobalTestState) {
t.assertTrue(bal.balances.length === 1); t.assertTrue(bal.balances.length === 1);
console.log(bal); 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");
}
} }

View File

@ -18,11 +18,7 @@
* Imports. * Imports.
*/ */
import { PreparePayResultType } from "@gnu-taler/taler-util"; import { PreparePayResultType } from "@gnu-taler/taler-util";
import { import { GlobalTestState, WalletCli, MerchantPrivateApi } from "./harness";
GlobalTestState,
WalletCli,
MerchantPrivateApi,
} from "./harness";
import { import {
createSimpleTestkudosEnvironment, createSimpleTestkudosEnvironment,
makeTestPayment, makeTestPayment,
@ -133,5 +129,19 @@ export async function runWalletBackupDoublespendTest(t: GlobalTestState) {
}); });
console.log(res); 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();
} }
} }

View File

@ -1274,6 +1274,7 @@ export enum AbortStatus {
AbortFinished = "abort-finished", AbortFinished = "abort-finished",
} }
/** /**
* Record that stores status information about one purchase, starting from when * Record that stores status information about one purchase, starting from when
* the customer accepts a proposal. Includes refund status if applicable. * the customer accepts a proposal. Includes refund status if applicable.

View File

@ -14,15 +14,6 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 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 * Implementation of wallet backups (export/import/upload) and sync
* server management. * server management.
@ -30,6 +21,51 @@ import { encodeCrock, stringToBytes, getRandomBytes } from "../../index.js";
* @author Florian Dold <dold@taler.net> * @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( export async function exportBackup(
ws: InternalWalletState, ws: InternalWalletState,
): Promise<WalletBackupContentV1> { ): Promise<WalletBackupContentV1> {

View File

@ -14,11 +14,42 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 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 {
import { Stores, WalletContractData, DenomSelectionState, ExchangeWireInfo, ExchangeUpdateStatus, DenominationStatus, CoinSource, CoinSourceType, CoinStatus, ReserveBankInfo, ReserveRecordStatus, ProposalDownload, ProposalStatus, WalletRefundItem, RefundState, AbortStatus, RefreshSessionRecord } from "../../db.js"; 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 { TransactionHandle } from "../../index.js";
import { PayCoinSelection } from "../../util/coinSelection"; import { PayCoinSelection } from "../../util/coinSelection";
import { j2s } from "../../util/helpers"; import { j2s } from "@gnu-taler/taler-util";
import { checkDbInvariant, checkLogicInvariant } from "../../util/invariants"; import { checkDbInvariant, checkLogicInvariant } from "../../util/invariants";
import { Logger } from "../../util/logging"; import { Logger } from "../../util/logging";
import { initRetryInfo } from "../../util/retries"; import { initRetryInfo } from "../../util/retries";
@ -271,6 +302,8 @@ export async function importBackup(
denomPubHash, denomPubHash,
]); ]);
if (!existingDenom) { if (!existingDenom) {
logger.info(`importing backup denomination: ${j2s(backupDenomination)}`);
await tx.put(Stores.denominations, { await tx.put(Stores.denominations, {
denomPub: backupDenomination.denom_pub, denomPub: backupDenomination.denom_pub,
denomPubHash: denomPubHash, denomPubHash: denomPubHash,

View File

@ -25,13 +25,14 @@
* Imports. * Imports.
*/ */
import { InternalWalletState } from "../state"; import { InternalWalletState } from "../state";
import { AmountString, BackupRecovery, codecForAmountString, WalletBackupContentV1 } from "@gnu-taler/taler-util";
import { TransactionHandle } from "../../util/query";
import { import {
BackupProviderRecord, AmountString,
ConfigRecord, BackupRecovery,
Stores, codecForAmountString,
} from "../../db.js"; 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 { checkDbInvariant, checkLogicInvariant } from "../../util/invariants";
import { import {
bytesToString, bytesToString,
@ -43,7 +44,7 @@ import {
rsaBlind, rsaBlind,
stringToBytes, stringToBytes,
} from "../../crypto/talerCrypto"; } from "../../crypto/talerCrypto";
import { canonicalizeBaseUrl, canonicalJson, j2s } from "../../util/helpers"; import { canonicalizeBaseUrl, canonicalJson, j2s } from "@gnu-taler/taler-util";
import { import {
durationAdd, durationAdd,
durationFromSpec, durationFromSpec,
@ -408,6 +409,9 @@ export async function runBackupCycle(ws: InternalWalletState): Promise<void> {
const providers = await ws.db.iter(Stores.backupProviders).toArray(); const providers = await ws.db.iter(Stores.backupProviders).toArray();
logger.trace("got backup providers", providers); logger.trace("got backup providers", providers);
const backupJson = await exportBackup(ws); const backupJson = await exportBackup(ws);
logger.trace(`running backup cycle with backup JSON: ${j2s(backupJson)}`);
const backupConfig = await provideBackupState(ws); const backupConfig = await provideBackupState(ws);
const encBackup = await encryptBackup(backupConfig, backupJson); const encBackup = await encryptBackup(backupConfig, backupJson);

View File

@ -21,7 +21,7 @@ import {
stringToBytes, stringToBytes,
} from "../crypto/talerCrypto"; } from "../crypto/talerCrypto";
import { selectPayCoins } from "../util/coinSelection"; import { selectPayCoins } from "../util/coinSelection";
import { canonicalJson } from "../util/helpers"; import { canonicalJson } from "@gnu-taler/taler-util";
import { readSuccessResponseJsonOrThrow } from "../util/http"; import { readSuccessResponseJsonOrThrow } from "../util/http";
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries"; import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries";
import { import {

View File

@ -48,7 +48,7 @@ import {
getExpiryTimestamp, getExpiryTimestamp,
readSuccessResponseTextOrThrow, readSuccessResponseTextOrThrow,
} from "../index.js"; } 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 { checkDbInvariant } from "../util/invariants.js";
import { updateRetryInfoTimeout, initRetryInfo } from "../util/retries.js"; import { updateRetryInfoTimeout, initRetryInfo } from "../util/retries.js";
import { import {

View File

@ -83,8 +83,9 @@ import {
CoinCandidateSelection, CoinCandidateSelection,
AvailableCoinInfo, AvailableCoinInfo,
selectPayCoins, selectPayCoins,
PreviousPayCoins,
} from "../util/coinSelection.js"; } from "../util/coinSelection.js";
import { canonicalJson } from "../util/helpers.js"; import { canonicalJson, j2s } from "@gnu-taler/taler-util";
import { import {
initRetryInfo, initRetryInfo,
updateRetryInfoTimeout, updateRetryInfoTimeout,
@ -350,6 +351,13 @@ export async function applyCoinSpend(
if (!coin) { if (!coin) {
throw Error("coin allocated for payment doesn't exist anymore"); 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; coin.status = CoinStatus.Dormant;
const remaining = Amounts.sub( const remaining = Amounts.sub(
coin.currentAmount, coin.currentAmount,
@ -867,7 +875,7 @@ async function storePayReplaySuccess(
* *
* We do this by going through the coin history provided by the exchange and * We do this by going through the coin history provided by the exchange and
* (1) verifying the signatures from the exchange * (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 * (3) re-do coin selection with the bad coin removed
*/ */
async function handleInsufficientFunds( async function handleInsufficientFunds(
@ -875,12 +883,99 @@ async function handleInsufficientFunds(
proposalId: string, proposalId: string,
err: TalerErrorDetails, err: TalerErrorDetails,
): Promise<void> { ): Promise<void> {
logger.trace("handling insufficient funds, trying to re-select coins");
const proposal = await ws.db.get(Stores.purchases, proposalId); const proposal = await ws.db.get(Stores.purchases, proposalId);
if (!proposal) { if (!proposal) {
return; 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", message: "unexpected exception",
hint: "unexpected exception", hint: "unexpected exception",
details: { details: {
exception: e, exception: e.toString(),
}, },
}); });
}); });

View File

@ -34,7 +34,7 @@ import {
TalerErrorDetails, TalerErrorDetails,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { AmountJson, Amounts } 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 { readSuccessResponseJsonOrThrow } from "../util/http";
import { checkDbInvariant } from "../util/invariants"; import { checkDbInvariant } from "../util/invariants";
import { Logger } from "../util/logging"; import { Logger } from "../util/logging";

View File

@ -33,15 +33,46 @@ import {
addPaytoQueryParams, addPaytoQueryParams,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { randomBytes } from "../crypto/primitives/nacl-fast.js"; import { randomBytes } from "../crypto/primitives/nacl-fast.js";
import { Stores, ReserveRecordStatus, ReserveBankInfo, ReserveRecord, CurrencyRecord, WithdrawalGroupRecord } from "../db.js"; import {
import { Logger, encodeCrock, getRandomBytes, readSuccessResponseJsonOrThrow, URL, readSuccessResponseJsonOrErrorCode, throwUnexpectedRequestError, TransactionHandle } from "../index.js"; 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 { assertUnreachable } from "../util/assertUnreachable.js";
import { canonicalizeBaseUrl } from "../util/helpers.js"; import { canonicalizeBaseUrl } from "@gnu-taler/taler-util";
import { initRetryInfo, getRetryDuration, updateRetryInfoTimeout } from "../util/retries.js"; import {
initRetryInfo,
getRetryDuration,
updateRetryInfoTimeout,
} from "../util/retries.js";
import { guardOperationException, OperationFailedError } from "./errors.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 { 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"); const logger = new Logger("reserves.ts");
@ -488,7 +519,10 @@ async function updateReserve(
const currency = balance.currency; const currency = balance.currency;
await updateWithdrawalDenoms(ws, reserve.exchangeBaseUrl); 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( const newWithdrawalGroup = await ws.db.runWithWriteTransaction(
[Stores.coins, Stores.planchets, Stores.withdrawalGroups, Stores.reserves], [Stores.coins, Stores.planchets, Stores.withdrawalGroups, Stores.reserves],

View File

@ -45,7 +45,7 @@ import {
getRandomBytes, getRandomBytes,
getHttpResponseErrorDetails, getHttpResponseErrorDetails,
} from "../index.js"; } from "../index.js";
import { j2s } from "../util/helpers.js"; import { j2s } from "@gnu-taler/taler-util";
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js"; import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js";
import { guardOperationException, makeErrorDetails } from "./errors.js"; import { guardOperationException, makeErrorDetails } from "./errors.js";

View File

@ -1,6 +1,6 @@
/* /*
This file is part of GNU Taler 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 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 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/> 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 { import {
DenominationRecord, DenominationRecord,
Stores, Stores,
@ -67,15 +75,17 @@ import { TalerErrorCode } from "@gnu-taler/taler-util";
import { updateRetryInfoTimeout, initRetryInfo } from "../util/retries"; import { updateRetryInfoTimeout, initRetryInfo } from "../util/retries";
import { compare } from "@gnu-taler/taler-util"; import { compare } from "@gnu-taler/taler-util";
/**
* Logger for this file.
*/
const logger = new Logger("withdraw.ts"); const logger = new Logger("withdraw.ts");
/** /**
* Information about what will happen when creating a reserve. * Information about what will happen when creating a reserve.
* *
* Sent to the wallet frontend to be rendered and shown to the user. * 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. * Exchange that the reserve will be created at.
*/ */
@ -631,6 +641,8 @@ export async function updateWithdrawalDenoms(
logger.error("exchange details not available"); logger.error("exchange details not available");
throw Error(`exchange ${exchangeBaseUrl} 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); const denominations = await getCandidateWithdrawalDenoms(ws, exchangeBaseUrl);
for (const denom of denominations) { for (const denom of denominations) {
if (denom.status === DenominationStatus.Unverified) { if (denom.status === DenominationStatus.Unverified) {
@ -639,6 +651,9 @@ export async function updateWithdrawalDenoms(
exchangeDetails.masterPublicKey, exchangeDetails.masterPublicKey,
); );
if (!valid) { if (!valid) {
logger.warn(
`Signature check for denomination h=${denom.denomPubHash} failed`,
);
denom.status = DenominationStatus.VerifiedBad; denom.status = DenominationStatus.VerifiedBad;
} else { } else {
denom.status = DenominationStatus.VerifiedGood; denom.status = DenominationStatus.VerifiedGood;
@ -648,11 +663,13 @@ export async function updateWithdrawalDenoms(
} }
// FIXME: This debug info should either be made conditional on some flag // FIXME: This debug info should either be made conditional on some flag
// or put into some wallet-core API. // or put into some wallet-core API.
logger.trace("updated withdrawable denominations");
const nextDenominations = await getCandidateWithdrawalDenoms( const nextDenominations = await getCandidateWithdrawalDenoms(
ws, ws,
exchangeBaseUrl, exchangeBaseUrl,
); );
logger.trace(
`updated withdrawable denominations for "${exchangeBaseUrl}, n=${nextDenominations.length}"`,
);
const now = getTimestampNow(); const now = getTimestampNow();
for (const denom of nextDenominations) { for (const denom of nextDenominations) {
const startDelay = getDurationRemaining(denom.stampStart, now); const startDelay = getDurationRemaining(denom.stampStart, now);

View File

@ -24,7 +24,7 @@
* Imports. * Imports.
*/ */
import { AmountJson, AmountLike, Amounts } from "@gnu-taler/taler-util"; 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"; import { Logger } from "./logging.js";
const logger = new Logger("coinSelection.ts"); const logger = new Logger("coinSelection.ts");
@ -89,7 +89,7 @@ export interface AvailableCoinInfo {
exchangeBaseUrl: string; exchangeBaseUrl: string;
} }
type PreviousPayCoins = { export type PreviousPayCoins = {
coinPub: string; coinPub: string;
contribution: AmountJson; contribution: AmountJson;
feeDeposit: AmountJson; feeDeposit: AmountJson;

View File

@ -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();
});

View File

@ -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);
}