From bafb52edff4d56bcb9e3c3d0a260f507c517b08c Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 16 Dec 2020 17:59:04 +0100 Subject: don't store reserve history anymore, adjust withdrawal implementation accordingly --- .../src/crypto/workers/cryptoImplementation.ts | 2 + packages/taler-wallet-core/src/index.ts | 4 +- .../taler-wallet-core/src/operations/backup.ts | 41 ++- .../taler-wallet-core/src/operations/exchanges.ts | 1 - packages/taler-wallet-core/src/operations/pay.ts | 2 + .../taler-wallet-core/src/operations/pending.ts | 3 +- .../taler-wallet-core/src/operations/refresh.ts | 7 +- .../taler-wallet-core/src/operations/reserves.ts | 367 +++++++-------------- packages/taler-wallet-core/src/operations/state.ts | 2 +- packages/taler-wallet-core/src/operations/tip.ts | 16 +- .../src/operations/transactions.ts | 2 +- .../src/operations/withdraw-test.ts | 4 +- .../taler-wallet-core/src/operations/withdraw.ts | 67 ++-- .../taler-wallet-core/src/types/backupTypes.ts | 82 ----- .../taler-wallet-core/src/types/cryptoTypes.ts | 6 + packages/taler-wallet-core/src/types/dbTypes.ts | 114 ++----- .../taler-wallet-core/src/types/notifications.ts | 6 - packages/taler-wallet-core/src/types/pending.ts | 276 ---------------- .../taler-wallet-core/src/types/transactions.ts | 337 ------------------- .../taler-wallet-core/src/types/walletTypes.ts | 2 +- packages/taler-wallet-core/src/util/query.ts | 2 +- .../src/util/reserveHistoryUtil-test.ts | 285 ---------------- .../src/util/reserveHistoryUtil.ts | 363 -------------------- packages/taler-wallet-core/src/wallet.ts | 6 +- 24 files changed, 248 insertions(+), 1749 deletions(-) delete mode 100644 packages/taler-wallet-core/src/types/pending.ts delete mode 100644 packages/taler-wallet-core/src/types/transactions.ts delete mode 100644 packages/taler-wallet-core/src/util/reserveHistoryUtil-test.ts delete mode 100644 packages/taler-wallet-core/src/util/reserveHistoryUtil.ts diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts index deaad42bb..4f553c502 100644 --- a/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts +++ b/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts @@ -206,6 +206,7 @@ export class CryptoImplementation { const tipPlanchet: DerivedTipPlanchet = { blindingKey: encodeCrock(blindingFactor), coinEv: encodeCrock(ev), + coinEvHash: encodeCrock(hash(ev)), coinPriv: encodeCrock(fc.coinPriv), coinPub: encodeCrock(fc.coinPub), }; @@ -463,6 +464,7 @@ export class CryptoImplementation { coinEv: encodeCrock(ev), privateKey: encodeCrock(coinPriv), publicKey: encodeCrock(coinPub), + coinEvHash: encodeCrock(hash(ev)), }; planchets.push(planchet); diff --git a/packages/taler-wallet-core/src/index.ts b/packages/taler-wallet-core/src/index.ts index a94155b14..3d52ed762 100644 --- a/packages/taler-wallet-core/src/index.ts +++ b/packages/taler-wallet-core/src/index.ts @@ -63,5 +63,5 @@ export * from "./util/time"; export * from "./types/talerTypes"; export * from "./types/walletTypes"; export * from "./types/notifications"; -export * from "./types/transactions"; -export * from "./types/pending"; +export * from "./types/transactionsTypes"; +export * from "./types/pendingTypes"; diff --git a/packages/taler-wallet-core/src/operations/backup.ts b/packages/taler-wallet-core/src/operations/backup.ts index 7ab908c46..1e5aa542d 100644 --- a/packages/taler-wallet-core/src/operations/backup.ts +++ b/packages/taler-wallet-core/src/operations/backup.ts @@ -44,6 +44,7 @@ import { BackupRefundState, BackupReserve, BackupTip, + BackupWithdrawalGroup, WalletBackupContentV1, } from "../types/backupTypes"; import { TransactionHandle } from "../util/query"; @@ -172,6 +173,7 @@ export async function exportBackup( Stores.tips, Stores.recoupGroups, Stores.reserves, + Stores.withdrawalGroups, ], async (tx) => { const bs = await getWalletBackupState(ws, tx); @@ -188,9 +190,46 @@ export async function exportBackup( const backupBackupProviders: BackupBackupProvider[] = []; const backupTips: BackupTip[] = []; const backupRecoupGroups: BackupRecoupGroup[] = []; + const withdrawalGroupsByReserve: { + [reservePub: string]: BackupWithdrawalGroup[]; + } = {}; + + await tx.iter(Stores.withdrawalGroups).forEachAsync(async (wg) => { + const withdrawalGroups = (withdrawalGroupsByReserve[ + wg.reservePub + ] ??= []); + // FIXME: finish! + // withdrawalGroups.push({ + // raw_withdrawal_amount: Amounts.stringify(wg.rawWithdrawalAmount), + // selected_denoms: wg.denomsSel.selectedDenoms.map((x) => ({ + // count: x.count, + // denom_pub_hash: x.denomPubHash, + // })), + // timestamp_start: wg.timestampStart, + // timestamp_finish: wg.timestampFinish, + // withdrawal_group_id: wg.withdrawalGroupId, + // }); + }); await tx.iter(Stores.reserves).forEach((reserve) => { - // FIXME: implement + const backupReserve: BackupReserve = { + initial_selected_denoms: reserve.initialDenomSel.selectedDenoms.map( + (x) => ({ + count: x.count, + denom_pub_hash: x.denomPubHash, + }), + ), + initial_withdrawal_group_id: reserve.initialWithdrawalGroupId, + instructed_amount: Amounts.stringify(reserve.instructedAmount), + reserve_priv: reserve.reservePriv, + timestamp_created: reserve.timestampCreated, + withdrawal_groups: + withdrawalGroupsByReserve[reserve.reservePub] ?? [], + }; + const backupReserves = (backupReservesByExchange[ + reserve.exchangeBaseUrl + ] ??= []); + backupReserves.push(backupReserve); }); await tx.iter(Stores.tips).forEach((tip) => { diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts index b6865cccc..3e71634cd 100644 --- a/packages/taler-wallet-core/src/operations/exchanges.ts +++ b/packages/taler-wallet-core/src/operations/exchanges.ts @@ -58,7 +58,6 @@ import { } from "../util/http"; import { Logger } from "../util/logging"; import { URL } from "../util/url"; -import { reconcileReserveHistory } from "../util/reserveHistoryUtil"; import { checkDbInvariant } from "../util/invariants"; import { NotificationType } from "../types/notifications"; import { updateRetryInfoTimeout, initRetryInfo } from "../util/retries"; diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts index 52f0c4510..c374cfe4a 100644 --- a/packages/taler-wallet-core/src/operations/pay.ts +++ b/packages/taler-wallet-core/src/operations/pay.ts @@ -460,6 +460,8 @@ async function recordConfirmPay( paymentSubmitPending: true, refunds: {}, merchantPaySig: undefined, + noncePriv: proposal.noncePriv, + noncePub: proposal.noncePub, }; await ws.db.runWithWriteTransaction( diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts index a42d89c9a..cc693a49d 100644 --- a/packages/taler-wallet-core/src/operations/pending.ts +++ b/packages/taler-wallet-core/src/operations/pending.ts @@ -29,7 +29,7 @@ import { PendingOperationType, ExchangeUpdateOperationStage, ReserveType, -} from "../types/pending"; +} from "../types/pendingTypes"; import { Duration, getTimestampNow, @@ -189,7 +189,6 @@ async function gatherReservePending( // nothing to report as pending break; case ReserveRecordStatus.WAIT_CONFIRM_BANK: - case ReserveRecordStatus.WITHDRAWING: case ReserveRecordStatus.QUERYING_STATUS: case ReserveRecordStatus.REGISTERING_BANK: resp.nextRetryDelay = updateRetryDelay( diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts index 71cc78fa9..2d80f0a50 100644 --- a/packages/taler-wallet-core/src/operations/refresh.ts +++ b/packages/taler-wallet-core/src/operations/refresh.ts @@ -29,7 +29,7 @@ import { amountToPretty } from "../util/helpers"; import { TransactionHandle } from "../util/query"; import { InternalWalletState, EXCHANGE_COINS_LOCK } from "./state"; import { Logger } from "../util/logging"; -import { getWithdrawDenomList, isWithdrawableDenom } from "./withdraw"; +import { selectWithdrawalDenominations, isWithdrawableDenom } from "./withdraw"; import { updateExchangeFromUrl } from "./exchanges"; import { TalerErrorDetails, @@ -83,7 +83,7 @@ export function getTotalRefreshCost( ): AmountJson { const withdrawAmount = Amounts.sub(amountLeft, refreshedDenom.feeRefresh) .amount; - const withdrawDenoms = getWithdrawDenomList(withdrawAmount, denoms); + const withdrawDenoms = selectWithdrawalDenominations(withdrawAmount, denoms); const resultingAmount = Amounts.add( Amounts.getZero(withdrawAmount.currency), ...withdrawDenoms.selectedDenoms.map( @@ -150,7 +150,7 @@ async function refreshCreateSession( oldDenom.feeRefresh, ).amount; - const newCoinDenoms = getWithdrawDenomList(availableAmount, availableDenoms); + const newCoinDenoms = selectWithdrawalDenominations(availableAmount, availableDenoms); if (newCoinDenoms.selectedDenoms.length === 0) { logger.trace( @@ -478,6 +478,7 @@ async function refreshReveal( oldCoinPub: refreshGroup.oldCoinPubs[coinIndex], }, suspended: false, + coinEvHash: pc.coinEv, }; coins.push(coin); diff --git a/packages/taler-wallet-core/src/operations/reserves.ts b/packages/taler-wallet-core/src/operations/reserves.ts index a2a1b3018..95c38120c 100644 --- a/packages/taler-wallet-core/src/operations/reserves.ts +++ b/packages/taler-wallet-core/src/operations/reserves.ts @@ -28,8 +28,6 @@ import { CurrencyRecord, Stores, WithdrawalGroupRecord, - WalletReserveHistoryItemType, - ReserveHistoryRecord, ReserveBankInfo, } from "../types/dbTypes"; import { Logger } from "../util/logging"; @@ -47,10 +45,12 @@ import { assertUnreachable } from "../util/assertUnreachable"; import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto"; import { randomBytes } from "../crypto/primitives/nacl-fast"; import { - selectWithdrawalDenoms, processWithdrawGroup, getBankWithdrawalInfo, denomSelectionInfoToState, + updateWithdrawalDenoms, + selectWithdrawalDenominations, + getPossibleWithdrawalDenoms, } from "./withdraw"; import { guardOperationException, @@ -66,11 +66,6 @@ import { durationMin, durationMax, } from "../util/time"; -import { - reconcileReserveHistory, - summarizeReserveHistory, - ReserveHistorySummary, -} from "../util/reserveHistoryUtil"; import { TransactionHandle } from "../util/query"; import { addPaytoQueryParams } from "../util/payto"; import { TalerErrorCode } from "../TalerErrorCode"; @@ -86,6 +81,7 @@ import { getRetryDuration, updateRetryInfoTimeout, } from "../util/retries"; +import { ReserveTransactionType } from "../types/ReserveTransaction"; const logger = new Logger("reserves.ts"); @@ -138,11 +134,9 @@ export async function createReserve( const initialWithdrawalGroupId = encodeCrock(getRandomBytes(32)); - const denomSelInfo = await selectWithdrawalDenoms( - ws, - canonExchange, - req.amount, - ); + await updateWithdrawalDenoms(ws, canonExchange); + const denoms = await getPossibleWithdrawalDenoms(ws, canonExchange); + const denomSelInfo = selectWithdrawalDenominations(req.amount, denoms); const initialDenomSel = denomSelectionInfoToState(denomSelInfo); const reserveRecord: ReserveRecord = { @@ -166,16 +160,6 @@ export async function createReserve( requestedQuery: false, }; - const reserveHistoryRecord: ReserveHistoryRecord = { - reservePub: keypair.pub, - reserveTransactions: [], - }; - - reserveHistoryRecord.reserveTransactions.push({ - type: WalletReserveHistoryItemType.Credit, - expectedAmount: req.amount, - }); - const exchangeInfo = await updateExchangeFromUrl(ws, req.exchange); const exchangeDetails = exchangeInfo.details; if (!exchangeDetails) { @@ -206,12 +190,7 @@ export async function createReserve( const cr: CurrencyRecord = currencyRecord; const resp = await ws.db.runWithWriteTransaction( - [ - Stores.currencies, - Stores.reserves, - Stores.reserveHistory, - Stores.bankWithdrawUris, - ], + [Stores.currencies, Stores.reserves, Stores.bankWithdrawUris], async (tx) => { // Check if we have already created a reserve for that bankWithdrawStatusUrl if (reserveRecord.bankInfo?.statusUrl) { @@ -238,7 +217,6 @@ export async function createReserve( } await tx.put(Stores.currencies, cr); await tx.put(Stores.reserves, reserveRecord); - await tx.put(Stores.reserveHistory, reserveHistoryRecord); const r: CreateReserveResponse = { exchange: canonExchange, reservePub: keypair.pub, @@ -499,6 +477,10 @@ async function incrementReserveRetry( /** * Update the information about a reserve that is stored in the wallet * by quering the reserve's exchange. + * + * If the reserve have funds that are not allocated in a withdrawal group yet + * and are big enough to withdraw with available denominations, + * create a new withdrawal group for the remaining amount. */ async function updateReserve( ws: InternalWalletState, @@ -542,78 +524,130 @@ async function updateReserve( } const reserveInfo = result.response; - const balance = Amounts.parseOrThrow(reserveInfo.balance); const currency = balance.currency; - let updateSummary: ReserveHistorySummary | undefined; - await ws.db.runWithWriteTransaction( - [Stores.reserves, Stores.reserveHistory], + + await updateWithdrawalDenoms(ws, reserve.exchangeBaseUrl); + const denoms = await getPossibleWithdrawalDenoms(ws, reserve.exchangeBaseUrl); + + const newWithdrawalGroup = await ws.db.runWithWriteTransaction( + [Stores.coins, Stores.planchets, Stores.withdrawalGroups, Stores.reserves], async (tx) => { - const r = await tx.get(Stores.reserves, reservePub); - if (!r) { - return; - } - if (r.reserveStatus !== ReserveRecordStatus.QUERYING_STATUS) { + const newReserve = await tx.get(Stores.reserves, reserve.reservePub); + if (!newReserve) { return; } + let amountReservePlus = Amounts.getZero(currency); + let amountReserveMinus = Amounts.getZero(currency); + + // Subtract withdrawal groups for this reserve from the available amount. + await tx + .iterIndexed(Stores.withdrawalGroups.byReservePub, reservePub) + .forEach((wg) => { + const cost = wg.denomsSel.totalWithdrawCost; + amountReserveMinus = Amounts.add(amountReserveMinus, cost).amount; + }); - const hist = await tx.get(Stores.reserveHistory, reservePub); - if (!hist) { - throw Error("inconsistent database"); + for (const entry of reserveInfo.history) { + switch (entry.type) { + case ReserveTransactionType.Credit: + amountReservePlus = Amounts.add( + amountReservePlus, + Amounts.parseOrThrow(entry.amount), + ).amount; + break; + case ReserveTransactionType.Recoup: + amountReservePlus = Amounts.add( + amountReservePlus, + Amounts.parseOrThrow(entry.amount), + ).amount; + break; + case ReserveTransactionType.Closing: + amountReserveMinus = Amounts.add( + amountReserveMinus, + Amounts.parseOrThrow(entry.amount), + ).amount; + break; + case ReserveTransactionType.Withdraw: { + // Now we check if the withdrawal transaction + // is part of any withdrawal known to this wallet. + const planchet = await tx.getIndexed( + Stores.planchets.coinEvHashIndex, + entry.h_coin_envelope, + ); + if (planchet) { + // Amount is already accounted in some withdrawal session + break; + } + const coin = await tx.getIndexed( + Stores.coins.coinEvHashIndex, + entry.h_coin_envelope, + ); + if (coin) { + // Amount is already accounted in some withdrawal session + break; + } + // Amount has been claimed by some withdrawal we don't know about + amountReserveMinus = Amounts.add( + amountReserveMinus, + Amounts.parseOrThrow(entry.amount), + ).amount; + break; + } + } } - const newHistoryTransactions = reserveInfo.history.slice( - hist.reserveTransactions.length, + const remainingAmount = Amounts.sub(amountReservePlus, amountReserveMinus) + .amount; + const denomSelInfo = selectWithdrawalDenominations( + remainingAmount, + denoms, ); - const reserveUpdateId = encodeCrock(getRandomBytes(32)); + if (denomSelInfo.selectedDenoms.length > 0) { + let withdrawalGroupId: string; - const reconciled = reconcileReserveHistory( - hist.reserveTransactions, - reserveInfo.history, - ); - - updateSummary = summarizeReserveHistory( - reconciled.updatedLocalHistory, - currency, - ); - - if ( - reconciled.newAddedItems.length + reconciled.newMatchedItems.length != - 0 - ) { - logger.trace("setting reserve status to 'withdrawing' after query"); - r.reserveStatus = ReserveRecordStatus.WITHDRAWING; - r.retryInfo = initRetryInfo(); - r.requestedQuery = false; - } else { - if (r.requestedQuery) { - logger.trace( - "setting reserve status to 'querying-status' (requested query) after query", - ); - r.reserveStatus = ReserveRecordStatus.QUERYING_STATUS; - r.requestedQuery = false; - r.retryInfo = initRetryInfo(); + if (!newReserve.initialWithdrawalStarted) { + withdrawalGroupId = newReserve.initialWithdrawalGroupId; + newReserve.initialWithdrawalStarted = true; } else { - logger.trace("setting reserve status to 'dormant' after query"); - r.reserveStatus = ReserveRecordStatus.DORMANT; - r.retryInfo = initRetryInfo(false); + withdrawalGroupId = encodeCrock(randomBytes(32)); } + + const withdrawalRecord: WithdrawalGroupRecord = { + withdrawalGroupId: withdrawalGroupId, + exchangeBaseUrl: reserve.exchangeBaseUrl, + reservePub: reserve.reservePub, + rawWithdrawalAmount: remainingAmount, + timestampStart: getTimestampNow(), + retryInfo: initRetryInfo(), + lastError: undefined, + denomsSel: denomSelectionInfoToState(denomSelInfo), + }; + + newReserve.lastError = undefined; + newReserve.retryInfo = initRetryInfo(false); + newReserve.reserveStatus = ReserveRecordStatus.DORMANT; + + await tx.put(Stores.reserves, newReserve); + await tx.put(Stores.withdrawalGroups, withdrawalRecord); + return withdrawalRecord; } - r.lastSuccessfulStatusQuery = getTimestampNow(); - hist.reserveTransactions = reconciled.updatedLocalHistory; - r.lastError = undefined; - await tx.put(Stores.reserves, r); - await tx.put(Stores.reserveHistory, hist); + return; }, ); - ws.notify({ type: NotificationType.ReserveUpdated, updateSummary }); - const reserve2 = await ws.db.get(Stores.reserves, reservePub); - if (reserve2) { - logger.trace( - `after db transaction, reserve status is ${reserve2.reserveStatus}`, - ); + + if (newWithdrawalGroup) { + logger.trace("processing new withdraw group"); + ws.notify({ + type: NotificationType.WithdrawGroupCreated, + withdrawalGroupId: newWithdrawalGroup.withdrawalGroupId, + }); + await processWithdrawGroup(ws, newWithdrawalGroup.withdrawalGroupId); + } else { + console.trace("withdraw session already existed"); } + return { ready: true }; } @@ -651,9 +685,6 @@ async function processReserveImpl( break; } } - case ReserveRecordStatus.WITHDRAWING: - await depleteReserve(ws, reservePub); - break; case ReserveRecordStatus.DORMANT: // nothing to do break; @@ -669,166 +700,6 @@ async function processReserveImpl( } } -/** - * Withdraw coins from a reserve until it is empty. - * - * When finished, marks the reserve as depleted by setting - * the depleted timestamp. - */ -async function depleteReserve( - ws: InternalWalletState, - reservePub: string, -): Promise { - let reserve: ReserveRecord | undefined; - let hist: ReserveHistoryRecord | undefined; - await ws.db.runWithReadTransaction( - [Stores.reserves, Stores.reserveHistory], - async (tx) => { - reserve = await tx.get(Stores.reserves, reservePub); - hist = await tx.get(Stores.reserveHistory, reservePub); - }, - ); - - if (!reserve) { - return; - } - if (!hist) { - throw Error("inconsistent database"); - } - if (reserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) { - return; - } - logger.trace(`depleting reserve ${reservePub}`); - - const summary = summarizeReserveHistory( - hist.reserveTransactions, - reserve.currency, - ); - - const withdrawAmount = summary.unclaimedReserveAmount; - - const denomsForWithdraw = await selectWithdrawalDenoms( - ws, - reserve.exchangeBaseUrl, - withdrawAmount, - ); - if (!denomsForWithdraw) { - // Only complain about inability to withdraw if we - // didn't withdraw before. - if (Amounts.isZero(summary.withdrawnAmount)) { - const opErr = makeErrorDetails( - TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT, - `Unable to withdraw from reserve, no denominations are available to withdraw.`, - {}, - ); - await incrementReserveRetry(ws, reserve.reservePub, opErr); - throw new OperationFailedAndReportedError(opErr); - } - return; - } - - logger.trace( - `Selected coins total cost ${Amounts.stringify( - denomsForWithdraw.totalWithdrawCost, - )} for withdrawal of ${Amounts.stringify(withdrawAmount)}`, - ); - - logger.trace("selected denominations"); - - const newWithdrawalGroup = await ws.db.runWithWriteTransaction( - [ - Stores.withdrawalGroups, - Stores.reserves, - Stores.reserveHistory, - Stores.planchets, - ], - async (tx) => { - const newReserve = await tx.get(Stores.reserves, reservePub); - if (!newReserve) { - return false; - } - if (newReserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) { - return false; - } - const newHist = await tx.get(Stores.reserveHistory, reservePub); - if (!newHist) { - throw Error("inconsistent database"); - } - const newSummary = summarizeReserveHistory( - newHist.reserveTransactions, - newReserve.currency, - ); - if ( - Amounts.cmp( - newSummary.unclaimedReserveAmount, - denomsForWithdraw.totalWithdrawCost, - ) < 0 - ) { - // Something must have happened concurrently! - logger.error( - "aborting withdrawal session, likely concurrent withdrawal happened", - ); - logger.error( - `unclaimed reserve amount is ${newSummary.unclaimedReserveAmount}`, - ); - logger.error( - `withdrawal cost is ${denomsForWithdraw.totalWithdrawCost}`, - ); - return false; - } - for (let i = 0; i < denomsForWithdraw.selectedDenoms.length; i++) { - const sd = denomsForWithdraw.selectedDenoms[i]; - for (let j = 0; j < sd.count; j++) { - const amt = Amounts.add(sd.denom.value, sd.denom.feeWithdraw).amount; - newHist.reserveTransactions.push({ - type: WalletReserveHistoryItemType.Withdraw, - expectedAmount: amt, - }); - } - } - logger.trace("setting reserve status to dormant after depletion"); - newReserve.reserveStatus = ReserveRecordStatus.DORMANT; - newReserve.retryInfo = initRetryInfo(false); - - let withdrawalGroupId: string; - - if (!newReserve.initialWithdrawalStarted) { - withdrawalGroupId = newReserve.initialWithdrawalGroupId; - newReserve.initialWithdrawalStarted = true; - } else { - withdrawalGroupId = encodeCrock(randomBytes(32)); - } - - const withdrawalRecord: WithdrawalGroupRecord = { - withdrawalGroupId: withdrawalGroupId, - exchangeBaseUrl: newReserve.exchangeBaseUrl, - reservePub: newReserve.reservePub, - rawWithdrawalAmount: withdrawAmount, - timestampStart: getTimestampNow(), - retryInfo: initRetryInfo(), - lastError: undefined, - denomsSel: denomSelectionInfoToState(denomsForWithdraw), - }; - - await tx.put(Stores.reserves, newReserve); - await tx.put(Stores.reserveHistory, newHist); - await tx.put(Stores.withdrawalGroups, withdrawalRecord); - return withdrawalRecord; - }, - ); - - if (newWithdrawalGroup) { - logger.trace("processing new withdraw group"); - ws.notify({ - type: NotificationType.WithdrawGroupCreated, - withdrawalGroupId: newWithdrawalGroup.withdrawalGroupId, - }); - await processWithdrawGroup(ws, newWithdrawalGroup.withdrawalGroupId); - } else { - console.trace("withdraw session already existed"); - } -} - export async function createTalerWithdrawReserve( ws: InternalWalletState, talerWithdrawUri: string, diff --git a/packages/taler-wallet-core/src/operations/state.ts b/packages/taler-wallet-core/src/operations/state.ts index c4d5b38f1..11695f6d0 100644 --- a/packages/taler-wallet-core/src/operations/state.ts +++ b/packages/taler-wallet-core/src/operations/state.ts @@ -19,7 +19,7 @@ import { BalancesResponse } from "../types/walletTypes"; import { CryptoApi, CryptoWorkerFactory } from "../crypto/workers/cryptoApi"; import { AsyncOpMemoMap, AsyncOpMemoSingle } from "../util/asyncMemo"; import { Logger } from "../util/logging"; -import { PendingOperationsResponse } from "../types/pending"; +import { PendingOperationsResponse } from "../types/pendingTypes"; import { WalletNotification } from "../types/notifications"; import { Database } from "../util/query"; import { openPromise, OpenedPromise } from "../util/promiseUtils"; diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts index bc10e346d..f47f76623 100644 --- a/packages/taler-wallet-core/src/operations/tip.ts +++ b/packages/taler-wallet-core/src/operations/tip.ts @@ -32,8 +32,10 @@ import { } from "../types/dbTypes"; import { getExchangeWithdrawalInfo, - selectWithdrawalDenoms, denomSelectionInfoToState, + updateWithdrawalDenoms, + getPossibleWithdrawalDenoms, + selectWithdrawalDenominations, } from "./withdraw"; import { updateExchangeFromUrl } from "./exchanges"; import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto"; @@ -92,12 +94,15 @@ export async function prepareTip( ); const walletTipId = encodeCrock(getRandomBytes(32)); - const selectedDenoms = await selectWithdrawalDenoms( - ws, - tipPickupStatus.exchange_url, + await updateWithdrawalDenoms(ws, tipPickupStatus.exchange_url); + const denoms = await getPossibleWithdrawalDenoms(ws, tipPickupStatus.exchange_url); + const selectedDenoms = await selectWithdrawalDenominations( amount, + denoms ); + const secretSeed = encodeCrock(getRandomBytes(64)); + tipRecord = { walletTipId: walletTipId, acceptedTimestamp: undefined, @@ -105,7 +110,6 @@ export async function prepareTip( tipExpiration: tipPickupStatus.expiration, exchangeBaseUrl: tipPickupStatus.exchange_url, merchantBaseUrl: res.merchantBaseUrl, - planchets: undefined, createdTimestamp: getTimestampNow(), merchantTipId: res.merchantTipId, tipAmountEffective: Amounts.sub( @@ -117,6 +121,7 @@ export async function prepareTip( lastError: undefined, denomsSel: denomSelectionInfoToState(selectedDenoms), pickedUpTimestamp: undefined, + secretSeed, }; await ws.db.put(Stores.tips, tipRecord); } @@ -316,6 +321,7 @@ async function processTipImpl( exchangeBaseUrl: tipRecord.exchangeBaseUrl, status: CoinStatus.Fresh, suspended: false, + coinEvHash: planchet.coinEvHash, }); } diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index 56e07a426..cf524db4e 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -36,7 +36,7 @@ import { WithdrawalType, WithdrawalDetails, OrderShortInfo, -} from "../types/transactions"; +} from "../types/transactionsTypes"; import { getFundingPaytoUris } from "./reserves"; import { TipResponse } from "../types/talerTypes"; diff --git a/packages/taler-wallet-core/src/operations/withdraw-test.ts b/packages/taler-wallet-core/src/operations/withdraw-test.ts index 24cb6f4b1..d21119c8c 100644 --- a/packages/taler-wallet-core/src/operations/withdraw-test.ts +++ b/packages/taler-wallet-core/src/operations/withdraw-test.ts @@ -15,7 +15,7 @@ */ import test from "ava"; -import { getWithdrawDenomList } from "./withdraw"; +import { selectWithdrawalDenominations } from "./withdraw"; import { Amounts } from "../util/amounts"; test("withdrawal selection bug repro", (t) => { @@ -322,7 +322,7 @@ test("withdrawal selection bug repro", (t) => { }, ]; - const res = getWithdrawDenomList(amount, denoms); + const res = selectWithdrawalDenominations(amount, denoms); console.error("cost", Amounts.stringify(res.totalWithdrawCost)); console.error("withdraw amount", Amounts.stringify(amount)); diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index a3bb9724c..758b80787 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -86,12 +86,13 @@ export function isWithdrawableDenom(d: DenominationRecord): boolean { return started && stillOkay && !d.isRevoked; } + /** * Get a list of denominations (with repetitions possible) * whose total value is as close as possible to the available * amount, but never larger. */ -export function getWithdrawDenomList( +export function selectWithdrawalDenominations( amountAvailable: AmountJson, denoms: DenominationRecord[], ): DenominationSelectionInfo { @@ -207,7 +208,7 @@ export async function getBankWithdrawalInfo( /** * Return denominations that can potentially used for a withdrawal. */ -async function getPossibleDenoms( +export async function getPossibleWithdrawalDenoms( ws: InternalWalletState, exchangeBaseUrl: string, ): Promise { @@ -470,6 +471,7 @@ async function processPlanchetVerifyAndStoreCoin( denomPub: planchet.denomPub, denomPubHash: planchet.denomPubHash, denomSig, + coinEvHash: planchet.coinEvHash, exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl, status: CoinStatus.Fresh, coinSource: { @@ -524,17 +526,13 @@ export function denomSelectionInfoToState( } /** - * Get a list of denominations to withdraw from the given exchange for the - * given amount, making sure that all denominations' signatures are verified. - * - * Writes to the DB in order to record the result from verifying - * denominations. + * Make sure that denominations that currently can be used for withdrawal + * are validated, and the result of validation is stored in the database. */ -export async function selectWithdrawalDenoms( +export async function updateWithdrawalDenoms( ws: InternalWalletState, exchangeBaseUrl: string, - amount: AmountJson, -): Promise { +): Promise { const exchange = await ws.db.get(Stores.exchanges, exchangeBaseUrl); if (!exchange) { logger.error("exchange not found"); @@ -545,43 +543,24 @@ export async function selectWithdrawalDenoms( logger.error("exchange details not available"); throw Error(`exchange ${exchangeBaseUrl} details not available`); } - - let allValid = false; - let selectedDenoms: DenominationSelectionInfo; - - // Find a denomination selection for the requested amount. - // If a selected denomination has not been validated yet - // and turns our to be invalid, we try again with the - // reduced set of denominations. - do { - allValid = true; - const nextPossibleDenoms = await getPossibleDenoms(ws, exchange.baseUrl); - selectedDenoms = getWithdrawDenomList(amount, nextPossibleDenoms); - for (const denomSel of selectedDenoms.selectedDenoms) { - const denom = denomSel.denom; - if (denom.status === DenominationStatus.Unverified) { - const valid = await ws.cryptoApi.isValidDenom( - denom, - exchangeDetails.masterPublicKey, - ); - if (!valid) { - denom.status = DenominationStatus.VerifiedBad; - allValid = false; - } else { - denom.status = DenominationStatus.VerifiedGood; - } - await ws.db.put(Stores.denominations, denom); + const denominations = await getPossibleWithdrawalDenoms(ws, exchangeBaseUrl); + for (const denom of denominations) { + if (denom.status === DenominationStatus.Unverified) { + const valid = await ws.cryptoApi.isValidDenom( + denom, + exchangeDetails.masterPublicKey, + ); + if (!valid) { + denom.status = DenominationStatus.VerifiedBad; + } else { + denom.status = DenominationStatus.VerifiedGood; } + await ws.db.put(Stores.denominations, denom); } - } while (selectedDenoms.selectedDenoms.length > 0 && !allValid); - - if (Amounts.cmp(selectedDenoms.totalWithdrawCost, amount) > 0) { - throw Error("Bug: withdrawal coin selection is wrong"); } - - return selectedDenoms; } + async function incrementWithdrawalRetry( ws: InternalWalletState, withdrawalGroupId: string, @@ -745,7 +724,9 @@ export async function getExchangeWithdrawalInfo( throw Error(`exchange ${exchangeInfo.baseUrl} wire details not available`); } - const selectedDenoms = await selectWithdrawalDenoms(ws, baseUrl, amount); + await updateWithdrawalDenoms(ws, baseUrl); + const denoms = await getPossibleWithdrawalDenoms(ws, baseUrl); + const selectedDenoms = selectWithdrawalDenominations(amount, denoms); const exchangeWireAccounts: string[] = []; for (const account of exchangeWireInfo.accounts) { exchangeWireAccounts.push(account.payto_uri); diff --git a/packages/taler-wallet-core/src/types/backupTypes.ts b/packages/taler-wallet-core/src/types/backupTypes.ts index 09bc4670c..a3261ae35 100644 --- a/packages/taler-wallet-core/src/types/backupTypes.ts +++ b/packages/taler-wallet-core/src/types/backupTypes.ts @@ -53,12 +53,6 @@ * Imports. */ import { Timestamp } from "../util/time"; -import { - ReserveClosingTransaction, - ReserveCreditTransaction, - ReserveRecoupTransaction, - ReserveWithdrawTransaction, -} from "./ReserveTransaction"; /** * Type alias for strings that are to be treated like amounts. @@ -1128,82 +1122,6 @@ export interface BackupExchange { tos_etag_accepted: string | undefined; } -export enum WalletReserveHistoryItemType { - Credit = "credit", - Withdraw = "withdraw", - Closing = "closing", - Recoup = "recoup", -} - -export interface BackupReserveHistoryCreditItem { - type: WalletReserveHistoryItemType.Credit; - - /** - * Amount we expect to see credited. - */ - expected_amount?: BackupAmountString; - - /** - * Item from the reserve transaction history that this - * wallet reserve history item matches up with. - */ - matched_exchange_transaction?: ReserveCreditTransaction; -} - -/** - * Reserve history item for a withdrawal - */ -export interface BackupReserveHistoryWithdrawItem { - type: WalletReserveHistoryItemType.Withdraw; - - expected_amount?: BackupAmountString; - - /** - * Hash of the blinded coin. - * - * When this value is set, it indicates that a withdrawal is active - * in the wallet for the reserve. - */ - expected_coin_ev_hash?: string; - - /** - * Item from the reserve transaction history that this - * wallet reserve history item matches up with. - */ - matched_exchange_transaction?: ReserveWithdrawTransaction; -} - -export interface BackupReserveHistoryClosingItem { - type: WalletReserveHistoryItemType.Closing; - - /** - * Item from the reserve transaction history that this - * wallet reserve history item matches up with. - */ - matched_exchange_transaction?: ReserveClosingTransaction; -} - -export interface BackupReserveHistoryRecoupItem { - type: WalletReserveHistoryItemType.Recoup; - - /** - * Amount we expect to see recouped. - */ - expected_amount?: BackupAmountString; - - /** - * Item from the reserve transaction history that this - * wallet reserve history item matches up with. - */ - matched_exchange_transaction?: ReserveRecoupTransaction; -} - -export type BackupReserveHistoryItem = - | BackupReserveHistoryCreditItem - | BackupReserveHistoryWithdrawItem - | BackupReserveHistoryRecoupItem - | BackupReserveHistoryClosingItem; - export enum BackupProposalStatus { /** * Proposed (and either downloaded or not, diff --git a/packages/taler-wallet-core/src/types/cryptoTypes.ts b/packages/taler-wallet-core/src/types/cryptoTypes.ts index 98bdf92ec..eb18d83fc 100644 --- a/packages/taler-wallet-core/src/types/cryptoTypes.ts +++ b/packages/taler-wallet-core/src/types/cryptoTypes.ts @@ -83,6 +83,11 @@ export interface DerivedRefreshSession { */ coinEv: string; + /** + * Hash of the blinded public key. + */ + coinEvHash: string; + /** * Blinding key used. */ @@ -122,6 +127,7 @@ export interface DeriveTipRequest { export interface DerivedTipPlanchet { blindingKey: string; coinEv: string; + coinEvHash: string; coinPriv: string; coinPub: string; } diff --git a/packages/taler-wallet-core/src/types/dbTypes.ts b/packages/taler-wallet-core/src/types/dbTypes.ts index 3a42b8dbc..71a591310 100644 --- a/packages/taler-wallet-core/src/types/dbTypes.ts +++ b/packages/taler-wallet-core/src/types/dbTypes.ts @@ -65,12 +65,6 @@ export enum ReserveRecordStatus { */ QUERYING_STATUS = "querying-status", - /** - * Status is queried, the wallet must now select coins - * and start withdrawing. - */ - WITHDRAWING = "withdrawing", - /** * The corresponding withdraw record has been created. * No further processing is done, unless explicitly requested @@ -84,76 +78,6 @@ export enum ReserveRecordStatus { BANK_ABORTED = "bank-aborted", } -export enum WalletReserveHistoryItemType { - Credit = "credit", - Withdraw = "withdraw", - Closing = "closing", - Recoup = "recoup", -} - -export interface WalletReserveHistoryCreditItem { - type: WalletReserveHistoryItemType.Credit; - - /** - * Amount we expect to see credited. - */ - expectedAmount?: AmountJson; - - /** - * Item from the reserve transaction history that this - * wallet reserve history item matches up with. - */ - matchedExchangeTransaction?: ReserveCreditTransaction; -} - -export interface WalletReserveHistoryWithdrawItem { - expectedAmount?: AmountJson; - - type: WalletReserveHistoryItemType.Withdraw; - - /** - * Item from the reserve transaction history that this - * wallet reserve history item matches up with. - */ - matchedExchangeTransaction?: ReserveWithdrawTransaction; -} - -export interface WalletReserveHistoryClosingItem { - type: WalletReserveHistoryItemType.Closing; - - /** - * Item from the reserve transaction history that this - * wallet reserve history item matches up with. - */ - matchedExchangeTransaction?: ReserveClosingTransaction; -} - -export interface WalletReserveHistoryRecoupItem { - type: WalletReserveHistoryItemType.Recoup; - - /** - * Amount we expect to see recouped. - */ - expectedAmount?: AmountJson; - - /** - * Item from the reserve transaction history that this - * wallet reserve history item matches up with. - */ - matchedExchangeTransaction?: ReserveRecoupTransaction; -} - -export type WalletReserveHistoryItem = - | WalletReserveHistoryCreditItem - | WalletReserveHistoryWithdrawItem - | WalletReserveHistoryRecoupItem - | WalletReserveHistoryClosingItem; - -export interface ReserveHistoryRecord { - reservePub: string; - reserveTransactions: WalletReserveHistoryItem[]; -} - export interface ReserveBankInfo { /** * Status URL that the wallet will use to query the status @@ -667,6 +591,8 @@ export interface RefreshPlanchet { */ coinEv: string; + coinEvHash: string; + /** * Blinding key used. */ @@ -782,6 +708,14 @@ export interface CoinRecord { */ blindingKey: string; + /** + * Hash of the coin envelope. + * + * Stored here for indexing purposes, so that when looking at a + * reserve history, we can quickly find the coin for a withdrawal transaction. + */ + coinEvHash: string; + /** * Status of the coin. */ @@ -1536,6 +1470,12 @@ class CoinsStore extends Store<"coins", CoinRecord> { string, CoinRecord >(this, "denomPubHashIndex", "denomPubHash"); + + coinEvHashIndex = new Index<"coins", "coinEvHashIndex", string, CoinRecord>( + this, + "coinEvHashIndex", + "coinEvHash", + ); } class ProposalsStore extends Store<"proposals", ProposalRecord> { @@ -1602,15 +1542,6 @@ class ReservesStore extends Store<"reserves", ReserveRecord> { } } -class ReserveHistoryStore extends Store< - "reserveHistory", - ReserveHistoryRecord -> { - constructor() { - super("reserveHistory", { keyPath: "reservePub" }); - } -} - class TipsStore extends Store<"tips", TipRecord> { constructor() { super("tips", { keyPath: "walletTipId" }); @@ -1638,6 +1569,12 @@ class WithdrawalGroupsStore extends Store< constructor() { super("withdrawals", { keyPath: "withdrawalGroupId" }); } + byReservePub = new Index< + "withdrawals", + "withdrawalsByReserveIndex", + string, + WithdrawalGroupRecord + >(this, "withdrawalsByReserveIndex", "reservePub"); } class PlanchetsStore extends Store<"planchets", PlanchetRecord> { @@ -1656,6 +1593,12 @@ class PlanchetsStore extends Store<"planchets", PlanchetRecord> { string, PlanchetRecord >(this, "withdrawalGroupIndex", "withdrawalGroupId"); + + coinEvHashIndex = new Index<"planchets", "coinEvHashIndex", string, PlanchetRecord>( + this, + "coinEvHashIndex", + "coinEvHash", + ); } /** @@ -1702,7 +1645,6 @@ export const Stores = { keyPath: "recoupGroupId", }), reserves: new ReservesStore(), - reserveHistory: new ReserveHistoryStore(), purchases: new PurchasesStore(), tips: new TipsStore(), withdrawalGroups: new WithdrawalGroupsStore(), diff --git a/packages/taler-wallet-core/src/types/notifications.ts b/packages/taler-wallet-core/src/types/notifications.ts index 533223cc0..9ddcf4fa2 100644 --- a/packages/taler-wallet-core/src/types/notifications.ts +++ b/packages/taler-wallet-core/src/types/notifications.ts @@ -23,7 +23,6 @@ * Imports. */ import { TalerErrorDetails } from "./walletTypes"; -import { ReserveHistorySummary } from "../util/reserveHistoryUtil"; export enum NotificationType { CoinWithdrawn = "coin-withdrawn", @@ -125,10 +124,6 @@ export interface RefreshRefusedNotification { type: NotificationType.RefreshUnwarranted; } -export interface ReserveUpdatedNotification { - type: NotificationType.ReserveUpdated; - updateSummary?: ReserveHistorySummary; -} export interface ReserveConfirmedNotification { type: NotificationType.ReserveConfirmed; @@ -252,7 +247,6 @@ export type WalletNotification = | RefreshRevealedNotification | RefreshStartedNotification | RefreshRefusedNotification - | ReserveUpdatedNotification | ReserveCreatedNotification | ReserveConfirmedNotification | WithdrawalGroupFinishedNotification diff --git a/packages/taler-wallet-core/src/types/pending.ts b/packages/taler-wallet-core/src/types/pending.ts deleted file mode 100644 index 18d9a2fa4..000000000 --- a/packages/taler-wallet-core/src/types/pending.ts +++ /dev/null @@ -1,276 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2019 GNUnet e.V. - - 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 - Foundation; either version 3, or (at your option) any later version. - - GNU 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 - GNU Taler; see the file COPYING. If not, see - */ - -/** - * Type and schema definitions for pending operations in the wallet. - */ - -/** - * Imports. - */ -import { TalerErrorDetails, BalancesResponse } from "./walletTypes"; -import { ReserveRecordStatus } from "./dbTypes"; -import { Timestamp, Duration } from "../util/time"; -import { RetryInfo } from "../util/retries"; - -export enum PendingOperationType { - Bug = "bug", - ExchangeUpdate = "exchange-update", - ExchangeCheckRefresh = "exchange-check-refresh", - Pay = "pay", - ProposalChoice = "proposal-choice", - ProposalDownload = "proposal-download", - Refresh = "refresh", - Reserve = "reserve", - Recoup = "recoup", - RefundQuery = "refund-query", - TipChoice = "tip-choice", - TipPickup = "tip-pickup", - Withdraw = "withdraw", -} - -/** - * Information about a pending operation. - */ -export type PendingOperationInfo = PendingOperationInfoCommon & - ( - | PendingBugOperation - | PendingExchangeUpdateOperation - | PendingExchangeCheckRefreshOperation - | PendingPayOperation - | PendingProposalChoiceOperation - | PendingProposalDownloadOperation - | PendingRefreshOperation - | PendingRefundQueryOperation - | PendingReserveOperation - | PendingTipChoiceOperation - | PendingTipPickupOperation - | PendingWithdrawOperation - | PendingRecoupOperation - ); - -/** - * The wallet is currently updating information about an exchange. - */ -export interface PendingExchangeUpdateOperation { - type: PendingOperationType.ExchangeUpdate; - stage: ExchangeUpdateOperationStage; - reason: string; - exchangeBaseUrl: string; - lastError: TalerErrorDetails | undefined; -} - -/** - * The wallet should check whether coins from this exchange - * need to be auto-refreshed. - */ -export interface PendingExchangeCheckRefreshOperation { - type: PendingOperationType.ExchangeCheckRefresh; - exchangeBaseUrl: string; -} - -/** - * Some interal error happened in the wallet. This pending operation - * should *only* be reported for problems in the wallet, not when - * a problem with a merchant/exchange/etc. occurs. - */ -export interface PendingBugOperation { - type: PendingOperationType.Bug; - message: string; - details: any; -} - -/** - * Current state of an exchange update operation. - */ -export enum ExchangeUpdateOperationStage { - FetchKeys = "fetch-keys", - FetchWire = "fetch-wire", - FinalizeUpdate = "finalize-update", -} - -export enum ReserveType { - /** - * Manually created. - */ - Manual = "manual", - /** - * Withdrawn from a bank that has "tight" Taler integration - */ - TalerBankWithdraw = "taler-bank-withdraw", -} - -/** - * Status of processing a reserve. - * - * Does *not* include the withdrawal operation that might result - * from this. - */ -export interface PendingReserveOperation { - type: PendingOperationType.Reserve; - retryInfo: RetryInfo | undefined; - stage: ReserveRecordStatus; - timestampCreated: Timestamp; - reserveType: ReserveType; - reservePub: string; - bankWithdrawConfirmUrl?: string; -} - -/** - * Status of an ongoing withdrawal operation. - */ -export interface PendingRefreshOperation { - type: PendingOperationType.Refresh; - lastError?: TalerErrorDetails; - refreshGroupId: string; - finishedPerCoin: boolean[]; - retryInfo: RetryInfo; -} - -/** - * Status of downloading signed contract terms from a merchant. - */ -export interface PendingProposalDownloadOperation { - type: PendingOperationType.ProposalDownload; - merchantBaseUrl: string; - proposalTimestamp: Timestamp; - proposalId: string; - orderId: string; - lastError?: TalerErrorDetails; - retryInfo: RetryInfo; -} - -/** - * User must choose whether to accept or reject the merchant's - * proposed contract terms. - */ -export interface PendingProposalChoiceOperation { - type: PendingOperationType.ProposalChoice; - merchantBaseUrl: string; - proposalTimestamp: Timestamp; - proposalId: string; -} - -/** - * The wallet is picking up a tip that the user has accepted. - */ -export interface PendingTipPickupOperation { - type: PendingOperationType.TipPickup; - tipId: string; - merchantBaseUrl: string; - merchantTipId: string; -} - -/** - * The wallet has been offered a tip, and the user now needs to - * decide whether to accept or reject the tip. - */ -export interface PendingTipChoiceOperation { - type: PendingOperationType.TipChoice; - tipId: string; - merchantBaseUrl: string; - merchantTipId: string; -} - -/** - * The wallet is signing coins and then sending them to - * the merchant. - */ -export interface PendingPayOperation { - type: PendingOperationType.Pay; - proposalId: string; - isReplay: boolean; - retryInfo: RetryInfo; - lastError: TalerErrorDetails | undefined; -} - -/** - * The wallet is querying the merchant about whether any refund - * permissions are available for a purchase. - */ -export interface PendingRefundQueryOperation { - type: PendingOperationType.RefundQuery; - proposalId: string; - retryInfo: RetryInfo; - lastError: TalerErrorDetails | undefined; -} - -export interface PendingRecoupOperation { - type: PendingOperationType.Recoup; - recoupGroupId: string; - retryInfo: RetryInfo; - lastError: TalerErrorDetails | undefined; -} - -/** - * Status of an ongoing withdrawal operation. - */ -export interface PendingWithdrawOperation { - type: PendingOperationType.Withdraw; - lastError: TalerErrorDetails | undefined; - retryInfo: RetryInfo; - withdrawalGroupId: string; - numCoinsWithdrawn: number; - numCoinsTotal: number; -} - -/** - * Fields that are present in every pending operation. - */ -export interface PendingOperationInfoCommon { - /** - * Type of the pending operation. - */ - type: PendingOperationType; - - /** - * Set to true if the operation indicates that something is really in progress, - * as opposed to some regular scheduled operation or a permanent failure. - */ - givesLifeness: boolean; - - /** - * Retry info, not available on all pending operations. - * If it is available, it must have the same name. - */ - retryInfo?: RetryInfo; -} - -/** - * Response returned from the pending operations API. - */ -export interface PendingOperationsResponse { - /** - * List of pending operations. - */ - pendingOperations: PendingOperationInfo[]; - - /** - * Current wallet balance, including pending balances. - */ - walletBalance: BalancesResponse; - - /** - * When is the next pending operation due to be re-tried? - */ - nextRetryDelay: Duration; - - /** - * Does this response only include pending operations that - * are due to be executed right now? - */ - onlyDue: boolean; -} diff --git a/packages/taler-wallet-core/src/types/transactions.ts b/packages/taler-wallet-core/src/types/transactions.ts deleted file mode 100644 index 0a683f298..000000000 --- a/packages/taler-wallet-core/src/types/transactions.ts +++ /dev/null @@ -1,337 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2019 Taler Systems S.A. - - 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 - Foundation; either version 3, or (at your option) any later version. - - GNU 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 - GNU Taler; see the file COPYING. If not, see - */ - -/** - * Type and schema definitions for the wallet's transaction list. - * - * @author Florian Dold - * @author Torsten Grote - */ - -/** - * Imports. - */ -import { Timestamp } from "../util/time"; -import { - AmountString, - Product, - InternationalizedString, - MerchantInfo, - codecForInternationalizedString, - codecForMerchantInfo, - codecForProduct, -} from "./talerTypes"; -import { - Codec, - buildCodecForObject, - codecOptional, - codecForString, - codecForList, - codecForAny, -} from "../util/codec"; -import { TalerErrorDetails } from "./walletTypes"; - -export interface TransactionsRequest { - /** - * return only transactions in the given currency - */ - currency?: string; - - /** - * if present, results will be limited to transactions related to the given search string - */ - search?: string; -} - -export interface TransactionsResponse { - // a list of past and pending transactions sorted by pending, timestamp and transactionId. - // In case two events are both pending and have the same timestamp, - // they are sorted by the transactionId - // (lexically ascending and locale-independent comparison). - transactions: Transaction[]; -} - -export interface TransactionCommon { - // opaque unique ID for the transaction, used as a starting point for paginating queries - // and for invoking actions on the transaction (e.g. deleting/hiding it from the history) - transactionId: string; - - // the type of the transaction; different types might provide additional information - type: TransactionType; - - // main timestamp of the transaction - timestamp: Timestamp; - - // true if the transaction is still pending, false otherwise - // If a transaction is not longer pending, its timestamp will be updated, - // but its transactionId will remain unchanged - pending: boolean; - - // Raw amount of the transaction (exclusive of fees or other extra costs) - amountRaw: AmountString; - - // Amount added or removed from the wallet's balance (including all fees and other costs) - amountEffective: AmountString; - - error?: TalerErrorDetails; -} - -export type Transaction = - | TransactionWithdrawal - | TransactionPayment - | TransactionRefund - | TransactionTip - | TransactionRefresh; - -export enum TransactionType { - Withdrawal = "withdrawal", - Payment = "payment", - Refund = "refund", - Refresh = "refresh", - Tip = "tip", -} - -export enum WithdrawalType { - TalerBankIntegrationApi = "taler-bank-integration-api", - ManualTransfer = "manual-transfer", -} - -export type WithdrawalDetails = - | WithdrawalDetailsForManualTransfer - | WithdrawalDetailsForTalerBankIntegrationApi; - -interface WithdrawalDetailsForManualTransfer { - type: WithdrawalType.ManualTransfer; - - /** - * Payto URIs that the exchange supports. - * - * Already contains the amount and message. - */ - exchangePaytoUris: string[]; -} - -interface WithdrawalDetailsForTalerBankIntegrationApi { - type: WithdrawalType.TalerBankIntegrationApi; - - /** - * Set to true if the bank has confirmed the withdrawal, false if not. - * An unconfirmed withdrawal usually requires user-input and should be highlighted in the UI. - * See also bankConfirmationUrl below. - */ - confirmed: boolean; - - /** - * If the withdrawal is unconfirmed, this can include a URL for user - * initiated confirmation. - */ - bankConfirmationUrl?: string; -} - -// This should only be used for actual withdrawals -// and not for tips that have their own transactions type. -interface TransactionWithdrawal extends TransactionCommon { - type: TransactionType.Withdrawal; - - /** - * Exchange of the withdrawal. - */ - exchangeBaseUrl: string; - - /** - * Amount that got subtracted from the reserve balance. - */ - amountRaw: AmountString; - - /** - * Amount that actually was (or will be) added to the wallet's balance. - */ - amountEffective: AmountString; - - withdrawalDetails: WithdrawalDetails; -} - -export enum PaymentStatus { - /** - * Explicitly aborted after timeout / failure - */ - Aborted = "aborted", - - /** - * Payment failed, wallet will auto-retry. - * User should be given the option to retry now / abort. - */ - Failed = "failed", - - /** - * Paid successfully - */ - Paid = "paid", - - /** - * User accepted, payment is processing. - */ - Accepted = "accepted", -} - -export interface TransactionPayment extends TransactionCommon { - type: TransactionType.Payment; - - /** - * Additional information about the payment. - */ - info: OrderShortInfo; - - /** - * Wallet-internal end-to-end identifier for the payment. - */ - proposalId: string; - - /** - * How far did the wallet get with processing the payment? - */ - status: PaymentStatus; - - /** - * Amount that must be paid for the contract - */ - amountRaw: AmountString; - - /** - * Amount that was paid, including deposit, wire and refresh fees. - */ - amountEffective: AmountString; -} - -export interface OrderShortInfo { - /** - * Order ID, uniquely identifies the order within a merchant instance - */ - orderId: string; - - /** - * Hash of the contract terms. - */ - contractTermsHash: string; - - /** - * More information about the merchant - */ - merchant: MerchantInfo; - - /** - * Summary of the order, given by the merchant - */ - summary: string; - - /** - * Map from IETF BCP 47 language tags to localized summaries - */ - summary_i18n?: InternationalizedString; - - /** - * List of products that are part of the order - */ - products: Product[] | undefined; - - /** - * URL of the fulfillment, given by the merchant - */ - fulfillmentUrl?: string; - - /** - * Plain text message that should be shown to the user - * when the payment is complete. - */ - fulfillmentMessage?: string; - - /** - * Translations of fulfillmentMessage. - */ - fulfillmentMessage_i18n?: InternationalizedString; -} - -interface TransactionRefund extends TransactionCommon { - type: TransactionType.Refund; - - // ID for the transaction that is refunded - refundedTransactionId: string; - - // Additional information about the refunded payment - info: OrderShortInfo; - - // Amount that has been refunded by the merchant - amountRaw: AmountString; - - // Amount will be added to the wallet's balance after fees and refreshing - amountEffective: AmountString; -} - -interface TransactionTip extends TransactionCommon { - type: TransactionType.Tip; - - // Raw amount of the tip, without extra fees that apply - amountRaw: AmountString; - - // Amount will be (or was) added to the wallet's balance after fees and refreshing - amountEffective: AmountString; - - merchantBaseUrl: string; -} - -// A transaction shown for refreshes that are not associated to other transactions -// such as a refresh necessary before coin expiration. -// It should only be returned by the API if the effective amount is different from zero. -interface TransactionRefresh extends TransactionCommon { - type: TransactionType.Refresh; - - // Exchange that the coins are refreshed with - exchangeBaseUrl: string; - - // Raw amount that is refreshed - amountRaw: AmountString; - - // Amount that will be paid as fees for the refresh - amountEffective: AmountString; -} - -export const codecForTransactionsRequest = (): Codec => - buildCodecForObject() - .property("currency", codecOptional(codecForString())) - .property("search", codecOptional(codecForString())) - .build("TransactionsRequest"); - -// FIXME: do full validation here! -export const codecForTransactionsResponse = (): Codec => - buildCodecForObject() - .property("transactions", codecForList(codecForAny())) - .build("TransactionsResponse"); - -export const codecForOrderShortInfo = (): Codec => - buildCodecForObject() - .property("contractTermsHash", codecForString()) - .property("fulfillmentMessage", codecOptional(codecForString())) - .property( - "fulfillmentMessage_i18n", - codecOptional(codecForInternationalizedString()), - ) - .property("fulfillmentUrl", codecOptional(codecForString())) - .property("merchant", codecForMerchantInfo()) - .property("orderId", codecForString()) - .property("products", codecOptional(codecForList(codecForProduct()))) - .property("summary", codecForString()) - .property("summary_i18n", codecOptional(codecForInternationalizedString())) - .build("OrderShortInfo"); diff --git a/packages/taler-wallet-core/src/types/walletTypes.ts b/packages/taler-wallet-core/src/types/walletTypes.ts index d0e72b289..7dc675b38 100644 --- a/packages/taler-wallet-core/src/types/walletTypes.ts +++ b/packages/taler-wallet-core/src/types/walletTypes.ts @@ -55,7 +55,7 @@ import { codecForContractTerms, ContractTerms, } from "./talerTypes"; -import { OrderShortInfo, codecForOrderShortInfo } from "./transactions"; +import { OrderShortInfo, codecForOrderShortInfo } from "./transactionsTypes"; /** * Response for the create reserve request to the wallet. diff --git a/packages/taler-wallet-core/src/util/query.ts b/packages/taler-wallet-core/src/util/query.ts index e1a23b168..35aab81e9 100644 --- a/packages/taler-wallet-core/src/util/query.ts +++ b/packages/taler-wallet-core/src/util/query.ts @@ -583,7 +583,7 @@ export class Database { } async getIndexed>( - index: InferIndex, + index: Ind, key: IDBValidKey, ): Promise | undefined> { const tx = this.db.transaction([index.storeName], "readonly"); diff --git a/packages/taler-wallet-core/src/util/reserveHistoryUtil-test.ts b/packages/taler-wallet-core/src/util/reserveHistoryUtil-test.ts deleted file mode 100644 index 79022de77..000000000 --- a/packages/taler-wallet-core/src/util/reserveHistoryUtil-test.ts +++ /dev/null @@ -1,285 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - 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 - Foundation; either version 3, or (at your option) any later version. - - GNU 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 - GNU Taler; see the file COPYING. If not, see - */ - -/** - * Imports. - */ -import test from "ava"; -import { - reconcileReserveHistory, - summarizeReserveHistory, -} from "./reserveHistoryUtil"; -import { - WalletReserveHistoryItem, - WalletReserveHistoryItemType, -} from "../types/dbTypes"; -import { - ReserveTransaction, - ReserveTransactionType, -} from "../types/ReserveTransaction"; -import { Amounts } from "./amounts"; - -test("basics", (t) => { - const r = reconcileReserveHistory([], []); - t.deepEqual(r.updatedLocalHistory, []); -}); - -test("unmatched credit", (t) => { - const localHistory: WalletReserveHistoryItem[] = []; - const remoteHistory: ReserveTransaction[] = [ - { - type: ReserveTransactionType.Credit, - amount: "TESTKUDOS:100", - sender_account_url: "payto://void/", - timestamp: { t_ms: 42 }, - wire_reference: "ABC01", - }, - ]; - const r = reconcileReserveHistory(localHistory, remoteHistory); - const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS"); - t.deepEqual(r.updatedLocalHistory.length, 1); - t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:100"); - t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0"); - t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:100"); -}); - -test("unmatched credit #2", (t) => { - const localHistory: WalletReserveHistoryItem[] = []; - const remoteHistory: ReserveTransaction[] = [ - { - type: ReserveTransactionType.Credit, - amount: "TESTKUDOS:100", - sender_account_url: "payto://void/", - timestamp: { t_ms: 42 }, - wire_reference: "ABC01", - }, - { - type: ReserveTransactionType.Credit, - amount: "TESTKUDOS:50", - sender_account_url: "payto://void/", - timestamp: { t_ms: 42 }, - wire_reference: "ABC02", - }, - ]; - const r = reconcileReserveHistory(localHistory, remoteHistory); - const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS"); - t.deepEqual(r.updatedLocalHistory.length, 2); - t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:150"); - t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0"); - t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:150"); -}); - -test("matched credit", (t) => { - const localHistory: WalletReserveHistoryItem[] = [ - { - type: WalletReserveHistoryItemType.Credit, - expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"), - matchedExchangeTransaction: { - type: ReserveTransactionType.Credit, - amount: "TESTKUDOS:100", - sender_account_url: "payto://void/", - timestamp: { t_ms: 42 }, - wire_reference: "ABC01", - }, - }, - ]; - const remoteHistory: ReserveTransaction[] = [ - { - type: ReserveTransactionType.Credit, - amount: "TESTKUDOS:100", - sender_account_url: "payto://void/", - timestamp: { t_ms: 42 }, - wire_reference: "ABC01", - }, - { - type: ReserveTransactionType.Credit, - amount: "TESTKUDOS:50", - sender_account_url: "payto://void/", - timestamp: { t_ms: 42 }, - wire_reference: "ABC02", - }, - ]; - const r = reconcileReserveHistory(localHistory, remoteHistory); - const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS"); - t.deepEqual(r.updatedLocalHistory.length, 2); - t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:150"); - t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0"); - t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:150"); -}); - -test("fulfilling credit", (t) => { - const localHistory: WalletReserveHistoryItem[] = [ - { - type: WalletReserveHistoryItemType.Credit, - expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"), - }, - ]; - const remoteHistory: ReserveTransaction[] = [ - { - type: ReserveTransactionType.Credit, - amount: "TESTKUDOS:100", - sender_account_url: "payto://void/", - timestamp: { t_ms: 42 }, - wire_reference: "ABC01", - }, - { - type: ReserveTransactionType.Credit, - amount: "TESTKUDOS:50", - sender_account_url: "payto://void/", - timestamp: { t_ms: 42 }, - wire_reference: "ABC02", - }, - ]; - const r = reconcileReserveHistory(localHistory, remoteHistory); - const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS"); - t.deepEqual(r.updatedLocalHistory.length, 2); - t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:150"); -}); - -test("unfulfilled credit", (t) => { - const localHistory: WalletReserveHistoryItem[] = [ - { - type: WalletReserveHistoryItemType.Credit, - expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"), - }, - ]; - const remoteHistory: ReserveTransaction[] = [ - { - type: ReserveTransactionType.Credit, - amount: "TESTKUDOS:100", - sender_account_url: "payto://void/", - timestamp: { t_ms: 42 }, - wire_reference: "ABC01", - }, - { - type: ReserveTransactionType.Credit, - amount: "TESTKUDOS:50", - sender_account_url: "payto://void/", - timestamp: { t_ms: 42 }, - wire_reference: "ABC02", - }, - ]; - const r = reconcileReserveHistory(localHistory, remoteHistory); - const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS"); - t.deepEqual(r.updatedLocalHistory.length, 2); - t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:150"); -}); - -test("awaited credit", (t) => { - const localHistory: WalletReserveHistoryItem[] = [ - { - type: WalletReserveHistoryItemType.Credit, - expectedAmount: Amounts.parseOrThrow("TESTKUDOS:50"), - }, - { - type: WalletReserveHistoryItemType.Credit, - expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"), - }, - ]; - const remoteHistory: ReserveTransaction[] = [ - { - type: ReserveTransactionType.Credit, - amount: "TESTKUDOS:100", - sender_account_url: "payto://void/", - timestamp: { t_ms: 42 }, - wire_reference: "ABC01", - }, - ]; - const r = reconcileReserveHistory(localHistory, remoteHistory); - const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS"); - t.deepEqual(r.updatedLocalHistory.length, 2); - t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:100"); - t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:50"); - t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:100"); -}); - -test("withdrawal new match", (t) => { - const localHistory: WalletReserveHistoryItem[] = [ - { - type: WalletReserveHistoryItemType.Credit, - expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"), - matchedExchangeTransaction: { - type: ReserveTransactionType.Credit, - amount: "TESTKUDOS:100", - sender_account_url: "payto://void/", - timestamp: { t_ms: 42 }, - wire_reference: "ABC01", - }, - }, - { - type: WalletReserveHistoryItemType.Withdraw, - expectedAmount: Amounts.parseOrThrow("TESTKUDOS:5"), - }, - ]; - const remoteHistory: ReserveTransaction[] = [ - { - type: ReserveTransactionType.Credit, - amount: "TESTKUDOS:100", - sender_account_url: "payto://void/", - timestamp: { t_ms: 42 }, - wire_reference: "ABC01", - }, - { - type: ReserveTransactionType.Withdraw, - amount: "TESTKUDOS:5", - h_coin_envelope: "foobar", - h_denom_pub: "foobar", - reserve_sig: "foobar", - withdraw_fee: "TESTKUDOS:0.1", - }, - ]; - const r = reconcileReserveHistory(localHistory, remoteHistory); - const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS"); - t.deepEqual(r.updatedLocalHistory.length, 2); - t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:95"); - t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0"); - t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:95"); -}); - -test("claimed but now arrived", (t) => { - const localHistory: WalletReserveHistoryItem[] = [ - { - type: WalletReserveHistoryItemType.Credit, - expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"), - matchedExchangeTransaction: { - type: ReserveTransactionType.Credit, - amount: "TESTKUDOS:100", - sender_account_url: "payto://void/", - timestamp: { t_ms: 42 }, - wire_reference: "ABC01", - }, - }, - { - type: WalletReserveHistoryItemType.Withdraw, - expectedAmount: Amounts.parseOrThrow("TESTKUDOS:5"), - }, - ]; - const remoteHistory: ReserveTransaction[] = [ - { - type: ReserveTransactionType.Credit, - amount: "TESTKUDOS:100", - sender_account_url: "payto://void/", - timestamp: { t_ms: 42 }, - wire_reference: "ABC01", - }, - ]; - const r = reconcileReserveHistory(localHistory, remoteHistory); - const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS"); - t.deepEqual(r.updatedLocalHistory.length, 2); - t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:100"); - t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0"); - t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:95"); -}); diff --git a/packages/taler-wallet-core/src/util/reserveHistoryUtil.ts b/packages/taler-wallet-core/src/util/reserveHistoryUtil.ts deleted file mode 100644 index 60823e1e0..000000000 --- a/packages/taler-wallet-core/src/util/reserveHistoryUtil.ts +++ /dev/null @@ -1,363 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - 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 - Foundation; either version 3, or (at your option) any later version. - - GNU 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 - GNU Taler; see the file COPYING. If not, see - */ - -/** - * Helpers for dealing with reserve histories. - * - * @author Florian Dold - */ - -/** - * Imports. - */ -import { - WalletReserveHistoryItem, - WalletReserveHistoryItemType, -} from "../types/dbTypes"; -import { - ReserveTransaction, - ReserveTransactionType, -} from "../types/ReserveTransaction"; -import * as Amounts from "../util/amounts"; -import { timestampCmp } from "./time"; -import { deepCopy } from "./helpers"; -import { AmountJson } from "../util/amounts"; - -/** - * Result of a reserve reconciliation. - */ -export interface ReserveReconciliationResult { - /** - * The wallet's local history reconciled with the exchange's reserve history. - */ - updatedLocalHistory: WalletReserveHistoryItem[]; - - /** - * History items that were newly created, subset of the - * updatedLocalHistory items. - */ - newAddedItems: WalletReserveHistoryItem[]; - - /** - * History items that were newly matched, subset of the - * updatedLocalHistory items. - */ - newMatchedItems: WalletReserveHistoryItem[]; -} - -/** - * Various totals computed from the wallet's view - * on the reserve history. - */ -export interface ReserveHistorySummary { - /** - * Balance computed by the wallet, should match the balance - * computed by the reserve. - */ - computedReserveBalance: Amounts.AmountJson; - - /** - * Reserve balance that is still available for withdrawal. - */ - unclaimedReserveAmount: Amounts.AmountJson; - - /** - * Amount that we're still expecting to come into the reserve. - */ - awaitedReserveAmount: Amounts.AmountJson; - - /** - * Amount withdrawn from the reserve so far. Only counts - * finished withdrawals, not withdrawals in progress. - */ - withdrawnAmount: Amounts.AmountJson; -} - -/** - * Check if two reserve history items (exchange's version) match. - */ -function isRemoteHistoryMatch( - t1: ReserveTransaction, - t2: ReserveTransaction, -): boolean { - switch (t1.type) { - case ReserveTransactionType.Closing: { - return t1.type === t2.type && t1.wtid == t2.wtid; - } - case ReserveTransactionType.Credit: { - return t1.type === t2.type && t1.wire_reference === t2.wire_reference; - } - case ReserveTransactionType.Recoup: { - return ( - t1.type === t2.type && - t1.coin_pub === t2.coin_pub && - timestampCmp(t1.timestamp, t2.timestamp) === 0 - ); - } - case ReserveTransactionType.Withdraw: { - return t1.type === t2.type && t1.h_coin_envelope === t2.h_coin_envelope; - } - } -} - -/** - * Check a local reserve history item and a remote history item are a match. - */ -export function isLocalRemoteHistoryMatch( - t1: WalletReserveHistoryItem, - t2: ReserveTransaction, -): boolean { - switch (t1.type) { - case WalletReserveHistoryItemType.Credit: { - return ( - t2.type === ReserveTransactionType.Credit && - !!t1.expectedAmount && - Amounts.cmp(t1.expectedAmount, Amounts.parseOrThrow(t2.amount)) === 0 - ); - } - case WalletReserveHistoryItemType.Withdraw: - return ( - t2.type === ReserveTransactionType.Withdraw && - !!t1.expectedAmount && - Amounts.cmp(t1.expectedAmount, Amounts.parseOrThrow(t2.amount)) === 0 - ); - case WalletReserveHistoryItemType.Recoup: { - return ( - t2.type === ReserveTransactionType.Recoup && - !!t1.expectedAmount && - Amounts.cmp(t1.expectedAmount, Amounts.parseOrThrow(t2.amount)) === 0 - ); - } - } - return false; -} - -/** - * Compute totals for the wallet's view of the reserve history. - */ -export function summarizeReserveHistory( - localHistory: WalletReserveHistoryItem[], - currency: string, -): ReserveHistorySummary { - const posAmounts: AmountJson[] = []; - const negAmounts: AmountJson[] = []; - const expectedPosAmounts: AmountJson[] = []; - const expectedNegAmounts: AmountJson[] = []; - const withdrawnAmounts: AmountJson[] = []; - - for (const item of localHistory) { - switch (item.type) { - case WalletReserveHistoryItemType.Credit: - if (item.matchedExchangeTransaction) { - posAmounts.push( - Amounts.parseOrThrow(item.matchedExchangeTransaction.amount), - ); - } else if (item.expectedAmount) { - expectedPosAmounts.push(item.expectedAmount); - } - break; - case WalletReserveHistoryItemType.Recoup: - if (item.matchedExchangeTransaction) { - if (item.matchedExchangeTransaction) { - posAmounts.push( - Amounts.parseOrThrow(item.matchedExchangeTransaction.amount), - ); - } else if (item.expectedAmount) { - expectedPosAmounts.push(item.expectedAmount); - } else { - throw Error("invariant failed"); - } - } - break; - case WalletReserveHistoryItemType.Closing: - if (item.matchedExchangeTransaction) { - negAmounts.push( - Amounts.parseOrThrow(item.matchedExchangeTransaction.amount), - ); - } else { - throw Error("invariant failed"); - } - break; - case WalletReserveHistoryItemType.Withdraw: - if (item.matchedExchangeTransaction) { - negAmounts.push( - Amounts.parseOrThrow(item.matchedExchangeTransaction.amount), - ); - withdrawnAmounts.push( - Amounts.parseOrThrow(item.matchedExchangeTransaction.amount), - ); - } else if (item.expectedAmount) { - expectedNegAmounts.push(item.expectedAmount); - } else { - throw Error("invariant failed"); - } - break; - } - } - - const z = Amounts.getZero(currency); - - const computedBalance = Amounts.sub( - Amounts.add(z, ...posAmounts).amount, - ...negAmounts, - ).amount; - - const unclaimedReserveAmount = Amounts.sub( - Amounts.add(z, ...posAmounts).amount, - ...negAmounts, - ...expectedNegAmounts, - ).amount; - - const awaitedReserveAmount = Amounts.sub( - Amounts.add(z, ...expectedPosAmounts).amount, - ...expectedNegAmounts, - ).amount; - - const withdrawnAmount = Amounts.add(z, ...withdrawnAmounts).amount; - - return { - computedReserveBalance: computedBalance, - unclaimedReserveAmount: unclaimedReserveAmount, - awaitedReserveAmount: awaitedReserveAmount, - withdrawnAmount, - }; -} - -/** - * Reconcile the wallet's local model of the reserve history - * with the reserve history of the exchange. - */ -export function reconcileReserveHistory( - localHistory: WalletReserveHistoryItem[], - remoteHistory: ReserveTransaction[], -): ReserveReconciliationResult { - const updatedLocalHistory: WalletReserveHistoryItem[] = deepCopy( - localHistory, - ); - const newMatchedItems: WalletReserveHistoryItem[] = []; - const newAddedItems: WalletReserveHistoryItem[] = []; - - const remoteMatched = remoteHistory.map(() => false); - const localMatched = localHistory.map(() => false); - - // Take care of deposits - - // First, see which pairs are already a definite match. - for (let remoteIndex = 0; remoteIndex < remoteHistory.length; remoteIndex++) { - const rhi = remoteHistory[remoteIndex]; - for (let localIndex = 0; localIndex < localHistory.length; localIndex++) { - if (localMatched[localIndex]) { - continue; - } - const lhi = localHistory[localIndex]; - if (!lhi.matchedExchangeTransaction) { - continue; - } - if (isRemoteHistoryMatch(rhi, lhi.matchedExchangeTransaction)) { - localMatched[localIndex] = true; - remoteMatched[remoteIndex] = true; - break; - } - } - } - - // Check that all previously matched items are still matched - for (let localIndex = 0; localIndex < localHistory.length; localIndex++) { - if (localMatched[localIndex]) { - continue; - } - const lhi = localHistory[localIndex]; - if (lhi.matchedExchangeTransaction) { - // Don't use for further matching - localMatched[localIndex] = true; - // FIXME: emit some error here! - throw Error("previously matched reserve history item now unmatched"); - } - } - - // Next, find out if there are any exact new matches between local and remote - // history items - for (let localIndex = 0; localIndex < localHistory.length; localIndex++) { - if (localMatched[localIndex]) { - continue; - } - const lhi = localHistory[localIndex]; - for ( - let remoteIndex = 0; - remoteIndex < remoteHistory.length; - remoteIndex++ - ) { - const rhi = remoteHistory[remoteIndex]; - if (remoteMatched[remoteIndex]) { - continue; - } - if (isLocalRemoteHistoryMatch(lhi, rhi)) { - localMatched[localIndex] = true; - remoteMatched[remoteIndex] = true; - updatedLocalHistory[localIndex].matchedExchangeTransaction = rhi as any; - newMatchedItems.push(lhi); - break; - } - } - } - - // Finally we add new history items - for (let remoteIndex = 0; remoteIndex < remoteHistory.length; remoteIndex++) { - if (remoteMatched[remoteIndex]) { - continue; - } - const rhi = remoteHistory[remoteIndex]; - let newItem: WalletReserveHistoryItem; - switch (rhi.type) { - case ReserveTransactionType.Closing: { - newItem = { - type: WalletReserveHistoryItemType.Closing, - matchedExchangeTransaction: rhi, - }; - break; - } - case ReserveTransactionType.Credit: { - newItem = { - type: WalletReserveHistoryItemType.Credit, - matchedExchangeTransaction: rhi, - }; - break; - } - case ReserveTransactionType.Recoup: { - newItem = { - type: WalletReserveHistoryItemType.Recoup, - matchedExchangeTransaction: rhi, - }; - break; - } - case ReserveTransactionType.Withdraw: { - newItem = { - type: WalletReserveHistoryItemType.Withdraw, - matchedExchangeTransaction: rhi, - }; - break; - } - } - updatedLocalHistory.push(newItem); - newAddedItems.push(newItem); - } - - return { - updatedLocalHistory, - newAddedItems, - newMatchedItems, - }; -} diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 07af32bb8..baafc63dd 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -130,7 +130,7 @@ import { PendingOperationInfo, PendingOperationsResponse, PendingOperationType, -} from "./types/pending"; +} from "./types/pendingTypes"; import { WalletNotification, NotificationType } from "./types/notifications"; import { processPurchaseQueryRefund, @@ -148,7 +148,7 @@ import { TransactionsRequest, TransactionsResponse, codecForTransactionsRequest, -} from "./types/transactions"; +} from "./types/transactionsTypes"; import { getTransactions } from "./operations/transactions"; import { withdrawTestBalance, @@ -326,7 +326,7 @@ export class Wallet { } = {}, ): Promise { let done = false; - const p = new Promise((resolve, reject) => { + const p = new Promise((resolve, reject) => { // Monitor for conditions that means we're done or we // should quit with an error (due to exceeded retries). this.addNotificationListener((n) => { -- cgit v1.2.3