From 28b4489bead6cd88db1b91e0e0ae8b8e0d1d0007 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 21 Sep 2022 12:40:02 +0200 Subject: wallet-core: make basic backup work again --- .../test-wallet-backup-doublespend.ts | 10 +- packages/taler-wallet-core/src/errors.ts | 13 ++ .../src/operations/backup/import.ts | 216 +++++++-------------- .../src/operations/backup/index.ts | 2 + packages/taler-wallet-core/src/operations/pay.ts | 17 +- packages/taler-wallet-core/src/wallet.ts | 3 +- 6 files changed, 112 insertions(+), 149 deletions(-) diff --git a/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-doublespend.ts b/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-doublespend.ts index ec1d6417b..f5c9af07e 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-doublespend.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-doublespend.ts @@ -31,9 +31,6 @@ import { } from "../harness/helpers.js"; import { SyncService } from "../harness/sync"; -/** - * Run test for basic, bank-integrated withdrawal. - */ export async function runWalletBackupDoublespendTest(t: GlobalTestState) { // Set up test environment @@ -131,6 +128,13 @@ export async function runWalletBackupDoublespendTest(t: GlobalTestState) { // Make wallet pay for the order + { + console.log( + "wallet2 balance before preparePay:", + await wallet2.client.call(WalletApiOperation.GetBalances, {}), + ); + } + const preparePayResult = await wallet2.client.call( WalletApiOperation.PreparePayForUri, { diff --git a/packages/taler-wallet-core/src/errors.ts b/packages/taler-wallet-core/src/errors.ts index 62bde667d..0f4c480cd 100644 --- a/packages/taler-wallet-core/src/errors.ts +++ b/packages/taler-wallet-core/src/errors.ts @@ -113,6 +113,19 @@ function getDefaultHint(code: number): string { } } +export class TalerProtocolViolationError extends Error { + constructor(hint?: string) { + let msg: string; + if (hint) { + msg = `Taler protocol violation error (${hint})`; + } else { + msg = `Taler protocol violation error`; + } + super(msg); + Object.setPrototypeOf(this, TalerProtocolViolationError.prototype); + } +} + export class TalerError extends Error { errorDetail: TalerErrorDetail & T; private constructor(d: TalerErrorDetail & T) { diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts index 507a6cf10..20c7316c1 100644 --- a/packages/taler-wallet-core/src/operations/backup/import.ts +++ b/packages/taler-wallet-core/src/operations/backup/import.ts @@ -18,6 +18,7 @@ import { AgeRestriction, AmountJson, Amounts, + BackupCoin, BackupCoinSourceType, BackupDenomSel, BackupProposalStatus, @@ -37,6 +38,7 @@ import { } from "@gnu-taler/taler-util"; import { AbortStatus, + CoinRecord, CoinSource, CoinSourceType, CoinStatus, @@ -65,6 +67,7 @@ import { } from "../../util/invariants.js"; import { GetReadOnlyAccess, GetReadWriteAccess } from "../../util/query.js"; import { RetryInfo } from "../../util/retries.js"; +import { makeCoinAvailable } from "../../wallet.js"; import { getExchangeDetails } from "../exchanges.js"; import { makeEventId, TombstoneTag } from "../transactions.js"; import { provideBackupState } from "./state.js"; @@ -226,6 +229,71 @@ export interface BackupCryptoPrecomputedData { reservePrivToPub: Record; } +export async function importCoin( + ws: InternalWalletState, + tx: GetReadWriteAccess<{ + coins: typeof WalletStoresV1.coins; + coinAvailability: typeof WalletStoresV1.coinAvailability; + denominations: typeof WalletStoresV1.denominations; + }>, + cryptoComp: BackupCryptoPrecomputedData, + args: { + backupCoin: BackupCoin; + exchangeBaseUrl: string; + denomPubHash: string; + }, +): Promise { + const { backupCoin, exchangeBaseUrl, denomPubHash } = args; + const compCoin = cryptoComp.coinPrivToCompletedCoin[backupCoin.coin_priv]; + checkLogicInvariant(!!compCoin); + const existingCoin = await tx.coins.get(compCoin.coinPub); + if (!existingCoin) { + let coinSource: CoinSource; + switch (backupCoin.coin_source.type) { + case BackupCoinSourceType.Refresh: + coinSource = { + type: CoinSourceType.Refresh, + oldCoinPub: backupCoin.coin_source.old_coin_pub, + }; + break; + case BackupCoinSourceType.Tip: + coinSource = { + type: CoinSourceType.Tip, + coinIndex: backupCoin.coin_source.coin_index, + walletTipId: backupCoin.coin_source.wallet_tip_id, + }; + break; + case BackupCoinSourceType.Withdraw: + coinSource = { + type: CoinSourceType.Withdraw, + coinIndex: backupCoin.coin_source.coin_index, + reservePub: backupCoin.coin_source.reserve_pub, + withdrawalGroupId: backupCoin.coin_source.withdrawal_group_id, + }; + break; + } + const coinRecord: CoinRecord = { + blindingKey: backupCoin.blinding_key, + coinEvHash: compCoin.coinEvHash, + coinPriv: backupCoin.coin_priv, + currentAmount: Amounts.parseOrThrow(backupCoin.current_amount), + denomSig: backupCoin.denom_sig, + coinPub: compCoin.coinPub, + exchangeBaseUrl, + denomPubHash, + status: backupCoin.fresh ? CoinStatus.Fresh : CoinStatus.Dormant, + coinSource, + // FIXME! + maxAge: AgeRestriction.AGE_UNRESTRICTED, + }; + if (coinRecord.status === CoinStatus.Fresh) { + await makeCoinAvailable(ws, tx, coinRecord); + } else { + await tx.coins.put(coinRecord); + } + } +} + export async function importBackup( ws: InternalWalletState, backupBlobArg: any, @@ -241,6 +309,7 @@ export async function importBackup( x.exchangeDetails, x.exchanges, x.coins, + x.coinAvailability, x.denominations, x.purchases, x.proposals, @@ -360,10 +429,6 @@ export async function importBackup( denomPubHash, ]); if (!existingDenom) { - logger.info( - `importing backup denomination: ${j2s(backupDenomination)}`, - ); - const value = Amounts.parseOrThrow(backupDenomination.value); await tx.denominations.put({ @@ -398,53 +463,11 @@ export async function importBackup( }); } for (const backupCoin of backupDenomination.coins) { - const compCoin = - cryptoComp.coinPrivToCompletedCoin[backupCoin.coin_priv]; - checkLogicInvariant(!!compCoin); - const existingCoin = await tx.coins.get(compCoin.coinPub); - if (!existingCoin) { - let coinSource: CoinSource; - switch (backupCoin.coin_source.type) { - case BackupCoinSourceType.Refresh: - coinSource = { - type: CoinSourceType.Refresh, - oldCoinPub: backupCoin.coin_source.old_coin_pub, - }; - break; - case BackupCoinSourceType.Tip: - coinSource = { - type: CoinSourceType.Tip, - coinIndex: backupCoin.coin_source.coin_index, - walletTipId: backupCoin.coin_source.wallet_tip_id, - }; - break; - case BackupCoinSourceType.Withdraw: - coinSource = { - type: CoinSourceType.Withdraw, - coinIndex: backupCoin.coin_source.coin_index, - reservePub: backupCoin.coin_source.reserve_pub, - withdrawalGroupId: - backupCoin.coin_source.withdrawal_group_id, - }; - break; - } - await tx.coins.put({ - blindingKey: backupCoin.blinding_key, - coinEvHash: compCoin.coinEvHash, - coinPriv: backupCoin.coin_priv, - currentAmount: Amounts.parseOrThrow(backupCoin.current_amount), - denomSig: backupCoin.denom_sig, - coinPub: compCoin.coinPub, - exchangeBaseUrl: backupExchangeDetails.base_url, - denomPubHash, - status: backupCoin.fresh - ? CoinStatus.Fresh - : CoinStatus.Dormant, - coinSource, - // FIXME! - maxAge: AgeRestriction.AGE_UNRESTRICTED, - }); - } + await importCoin(ws, tx, cryptoComp, { + backupCoin, + denomPubHash, + exchangeBaseUrl: backupExchangeDetails.base_url, + }); } } @@ -532,97 +555,6 @@ export async function importBackup( timestampFinish: backupWg.timestamp_finish, }); } - - // FIXME: import reserves with new schema - - // for (const backupReserve of backupExchangeDetails.reserves) { - // const reservePub = - // cryptoComp.reservePrivToPub[backupReserve.reserve_priv]; - // const ts = makeEventId(TombstoneTag.DeleteReserve, reservePub); - // if (tombstoneSet.has(ts)) { - // continue; - // } - // checkLogicInvariant(!!reservePub); - // const existingReserve = await tx.reserves.get(reservePub); - // const instructedAmount = Amounts.parseOrThrow( - // backupReserve.instructed_amount, - // ); - // if (!existingReserve) { - // let bankInfo: ReserveBankInfo | undefined; - // if (backupReserve.bank_info) { - // bankInfo = { - // exchangePaytoUri: backupReserve.bank_info.exchange_payto_uri, - // statusUrl: backupReserve.bank_info.status_url, - // confirmUrl: backupReserve.bank_info.confirm_url, - // }; - // } - // await tx.reserves.put({ - // currency: instructedAmount.currency, - // instructedAmount, - // exchangeBaseUrl: backupExchangeDetails.base_url, - // reservePub, - // reservePriv: backupReserve.reserve_priv, - // bankInfo, - // timestampCreated: backupReserve.timestamp_created, - // timestampBankConfirmed: - // backupReserve.bank_info?.timestamp_bank_confirmed, - // timestampReserveInfoPosted: - // backupReserve.bank_info?.timestamp_reserve_info_posted, - // senderWire: backupReserve.sender_wire, - // retryInfo: RetryInfo.reset(), - // lastError: undefined, - // initialWithdrawalGroupId: - // backupReserve.initial_withdrawal_group_id, - // initialWithdrawalStarted: - // backupReserve.withdrawal_groups.length > 0, - // // FIXME! - // reserveStatus: ReserveRecordStatus.QueryingStatus, - // initialDenomSel: await getDenomSelStateFromBackup( - // tx, - // backupExchangeDetails.base_url, - // backupReserve.initial_selected_denoms, - // ), - // // FIXME! - // operationStatus: OperationStatus.Pending, - // }); - // } - // for (const backupWg of backupReserve.withdrawal_groups) { - // const ts = makeEventId( - // TombstoneTag.DeleteWithdrawalGroup, - // backupWg.withdrawal_group_id, - // ); - // if (tombstoneSet.has(ts)) { - // continue; - // } - // const existingWg = await tx.withdrawalGroups.get( - // backupWg.withdrawal_group_id, - // ); - // if (!existingWg) { - // await tx.withdrawalGroups.put({ - // denomsSel: await getDenomSelStateFromBackup( - // tx, - // backupExchangeDetails.base_url, - // backupWg.selected_denoms, - // ), - // exchangeBaseUrl: backupExchangeDetails.base_url, - // lastError: undefined, - // rawWithdrawalAmount: Amounts.parseOrThrow( - // backupWg.raw_withdrawal_amount, - // ), - // reservePub, - // retryInfo: RetryInfo.reset(), - // secretSeed: backupWg.secret_seed, - // timestampStart: backupWg.timestamp_created, - // timestampFinish: backupWg.timestamp_finish, - // withdrawalGroupId: backupWg.withdrawal_group_id, - // denomSelUid: backupWg.selected_denoms_id, - // operationStatus: backupWg.timestamp_finish - // ? OperationStatus.Finished - // : OperationStatus.Pending, - // }); - // } - // } - // } } for (const backupProposal of backupBlob.proposals) { diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts b/packages/taler-wallet-core/src/operations/backup/index.ts index b69c0b7b7..fc84ce4ef 100644 --- a/packages/taler-wallet-core/src/operations/backup/index.ts +++ b/packages/taler-wallet-core/src/operations/backup/index.ts @@ -482,6 +482,8 @@ export async function processBackupForProvider( throw Error("unknown backup provider"); } + logger.info(`running backup for provider ${backupProviderBaseUrl}`); + return await runBackupCycleForProvider(ws, { backupProviderBaseUrl: provider.baseUrl, retryAfterPayment: true, diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts index 415100160..6757b79b4 100644 --- a/packages/taler-wallet-core/src/operations/pay.ts +++ b/packages/taler-wallet-core/src/operations/pay.ts @@ -78,6 +78,7 @@ import { makeErrorDetail, makePendingOperationFailedError, TalerError, + TalerProtocolViolationError, } from "../errors.js"; import { EXCHANGE_COINS_LOCK, @@ -752,7 +753,7 @@ async function handleInsufficientFunds( return; } - const brokenCoinPub = (err as any).coin_pub; + logger.trace(`got error details: ${j2s(err)}`); const exchangeReply = (err as any).exchange_reply; if ( @@ -766,7 +767,12 @@ async function handleInsufficientFunds( throw Error(`unable to handle /pay error response (${exchangeReply.code})`); } - logger.trace(`got error details: ${j2s(err)}`); + const brokenCoinPub = (exchangeReply as any).coin_pub; + logger.trace(`excluded broken coin pub=${brokenCoinPub}`); + + if (!brokenCoinPub) { + throw new TalerProtocolViolationError(); + } const { contractData } = proposal.download; @@ -1146,6 +1152,8 @@ export async function selectPayCoinsNew( req, ); + // logger.trace(`candidate denoms: ${j2s(candidateDenoms)}`); + const coinPubs: string[] = []; const coinContributions: AmountJson[] = []; const currency = contractTermsAmount.currency; @@ -1201,6 +1209,9 @@ export async function selectPayCoinsNew( const finalSel = selectedDenom; + logger.trace(`coin selection request ${j2s(req)}`); + logger.trace(`selected coins (via denoms) for payment: ${j2s(finalSel)}`); + await ws.db .mktx((x) => [x.coins, x.denominations]) .runReadOnly(async (tx) => { @@ -1301,7 +1312,7 @@ export async function checkPaymentByProposalId( }); if (!res) { - logger.info("not confirming payment, insufficient coins"); + logger.info("not allowing payment, insufficient coins"); return { status: PreparePayResultType.InsufficientBalance, contractTerms: d.contractTermsRaw, diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 2e362da6e..c91a96841 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -221,7 +221,7 @@ import { HttpRequestLibrary, readSuccessResponseJsonOrThrow, } from "./util/http.js"; -import { checkDbInvariant } from "./util/invariants.js"; +import { checkDbInvariant, checkLogicInvariant } from "./util/invariants.js"; import { AsyncCondition, OpenedPromise, @@ -812,6 +812,7 @@ export async function makeCoinAvailable( }>, coinRecord: CoinRecord, ): Promise { + checkLogicInvariant(coinRecord.status === CoinStatus.Fresh); const existingCoin = await tx.coins.get(coinRecord.coinPub); if (existingCoin) { return; -- cgit v1.2.3