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