get coin re-selection after accidental double spending to work
This commit is contained in:
parent
29d710c392
commit
4fa88007f9
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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> {
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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 };
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -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";
|
||||
|
@ -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],
|
||||
|
@ -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";
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
});
|
@ -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);
|
||||
}
|
Loading…
Reference in New Issue
Block a user