commit a70d37ef1675b53241f707c6730fab1537bd9d24 parent 453656b240c7e8771068ab877b6f5c9e3a26a4dc Author: Florian Dold <florian@dold.me> Date: Thu, 17 Jun 2021 15:49:05 +0200 towards factoring out cyclic dependencies Diffstat:
29 files changed, 364 insertions(+), 357 deletions(-)
diff --git a/packages/taler-wallet-core/src/common.ts b/packages/taler-wallet-core/src/common.ts @@ -0,0 +1,182 @@ +/* + 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 <http://www.gnu.org/licenses/> + */ + +/** + * Imports. + */ +import { + WalletNotification, + BalancesResponse, + Logger, +} from "@gnu-taler/taler-util"; +import { CryptoApi, CryptoWorkerFactory } from "./crypto/workers/cryptoApi.js"; +import { ExchangeDetailsRecord, ExchangeRecord, WalletStoresV1 } from "./db.js"; +import { PendingOperationsResponse } from "./pending-types.js"; +import { AsyncOpMemoMap, AsyncOpMemoSingle } from "./util/asyncMemo.js"; +import { HttpRequestLibrary } from "./util/http.js"; +import { + AsyncCondition, + OpenedPromise, + openPromise, +} from "./util/promiseUtils.js"; +import { DbAccess, GetReadOnlyAccess } from "./util/query.js"; +import { TimerGroup } from "./util/timer.js"; + +type NotificationListener = (n: WalletNotification) => void; + +const logger = new Logger("state.ts"); + +export const EXCHANGE_COINS_LOCK = "exchange-coins-lock"; +export const EXCHANGE_RESERVES_LOCK = "exchange-reserves-lock"; + +export interface TrustInfo { + isTrusted: boolean; + isAudited: boolean; +} + +/** + * Interface for exchange-related operations. + */ +export interface ExchangeOperations { + // FIXME: Should other operations maybe always use + // updateExchangeFromUrl? + getExchangeDetails( + tx: GetReadOnlyAccess<{ + exchanges: typeof WalletStoresV1.exchanges; + exchangeDetails: typeof WalletStoresV1.exchangeDetails; + }>, + exchangeBaseUrl: string, + ): Promise<ExchangeDetailsRecord | undefined>; + getExchangeTrust( + ws: InternalWalletState, + exchangeInfo: ExchangeRecord, + ): Promise<TrustInfo>; + updateExchangeFromUrl( + ws: InternalWalletState, + baseUrl: string, + forceNow?: boolean, + ): Promise<{ + exchange: ExchangeRecord; + exchangeDetails: ExchangeDetailsRecord; + }>; +} + +/** + * Internal state of the wallet. + */ +export class InternalWalletState { + memoProcessReserve: AsyncOpMemoMap<void> = new AsyncOpMemoMap(); + memoMakePlanchet: AsyncOpMemoMap<void> = new AsyncOpMemoMap(); + memoGetPending: AsyncOpMemoSingle<PendingOperationsResponse> = new AsyncOpMemoSingle(); + memoGetBalance: AsyncOpMemoSingle<BalancesResponse> = new AsyncOpMemoSingle(); + memoProcessRefresh: AsyncOpMemoMap<void> = new AsyncOpMemoMap(); + memoProcessRecoup: AsyncOpMemoMap<void> = new AsyncOpMemoMap(); + memoProcessDeposit: AsyncOpMemoMap<void> = new AsyncOpMemoMap(); + cryptoApi: CryptoApi; + + timerGroup: TimerGroup = new TimerGroup(); + latch = new AsyncCondition(); + stopped = false; + memoRunRetryLoop = new AsyncOpMemoSingle<void>(); + + listeners: NotificationListener[] = []; + + initCalled: boolean = false; + + exchangeOps: ExchangeOperations; + + /** + * Promises that are waiting for a particular resource. + */ + private resourceWaiters: Record<string, OpenedPromise<void>[]> = {}; + + /** + * Resources that are currently locked. + */ + private resourceLocks: Set<string> = new Set(); + + constructor( + // FIXME: Make this a getter and make + // the actual value nullable. + // Check if we are in a DB migration / garbage collection + // and throw an error in that case. + public db: DbAccess<typeof WalletStoresV1>, + public http: HttpRequestLibrary, + cryptoWorkerFactory: CryptoWorkerFactory, + ) { + this.cryptoApi = new CryptoApi(cryptoWorkerFactory); + } + + notify(n: WalletNotification): void { + logger.trace("Notification", n); + for (const l of this.listeners) { + const nc = JSON.parse(JSON.stringify(n)); + setTimeout(() => { + l(nc); + }, 0); + } + } + + addNotificationListener(f: (n: WalletNotification) => void): void { + this.listeners.push(f); + } + + /** + * Stop ongoing processing. + */ + stop(): void { + this.stopped = true; + this.timerGroup.stopCurrentAndFutureTimers(); + this.cryptoApi.stop(); + } + + /** + * Run an async function after acquiring a list of locks, identified + * by string tokens. + */ + async runSequentialized<T>(tokens: string[], f: () => Promise<T>) { + // Make sure locks are always acquired in the same order + tokens = [...tokens].sort(); + + for (const token of tokens) { + if (this.resourceLocks.has(token)) { + const p = openPromise<void>(); + let waitList = this.resourceWaiters[token]; + if (!waitList) { + waitList = this.resourceWaiters[token] = []; + } + waitList.push(p); + await p.promise; + } + this.resourceLocks.add(token); + } + + try { + logger.trace(`begin exclusive execution on ${JSON.stringify(tokens)}`); + const result = await f(); + logger.trace(`end exclusive execution on ${JSON.stringify(tokens)}`); + return result; + } finally { + for (const token of tokens) { + this.resourceLocks.delete(token); + let waiter = (this.resourceWaiters[token] ?? []).shift(); + if (waiter) { + waiter.resolve(); + } + } + } + } +} diff --git a/packages/taler-wallet-core/src/operations/errors.ts b/packages/taler-wallet-core/src/errors.ts diff --git a/packages/taler-wallet-core/src/headless/NodeHttpLib.ts b/packages/taler-wallet-core/src/headless/NodeHttpLib.ts @@ -27,7 +27,7 @@ import { } from "../util/http"; import { RequestThrottler } from "../util/RequestThrottler"; import Axios, { AxiosResponse } from "axios"; -import { OperationFailedError, makeErrorDetails } from "../operations/errors"; +import { OperationFailedError, makeErrorDetails } from "../errors"; import { URL } from "../util/url"; import { Logger } from "@gnu-taler/taler-util"; import { bytesToString } from "../crypto/talerCrypto"; diff --git a/packages/taler-wallet-core/src/headless/helpers.ts b/packages/taler-wallet-core/src/headless/helpers.ts @@ -35,7 +35,7 @@ import { Logger } from "@gnu-taler/taler-util"; import { SynchronousCryptoWorkerFactory } from "../crypto/workers/synchronousWorker"; import type { IDBFactory } from "@gnu-taler/idb-bridge"; import { WalletNotification } from "@gnu-taler/taler-util"; -import { InternalWalletState } from "../operations/state.js"; +import { InternalWalletState } from "../common.js"; const logger = new Logger("headless/helpers.ts"); diff --git a/packages/taler-wallet-core/src/index.ts b/packages/taler-wallet-core/src/index.ts @@ -19,7 +19,7 @@ */ // Errors -export * from "./operations/errors.js"; +export * from "./errors.js"; // Util functionality export { URL } from "./util/url.js"; @@ -34,7 +34,7 @@ export { DefaultNodeWalletArgs, } from "./headless/helpers.js"; -export * from "./operations/versions.js"; +export * from "./versions.js"; export * from "./db.js"; @@ -48,6 +48,6 @@ export * from "./crypto/talerCrypto.js"; export * from "./pending-types.js"; export * from "./util/debugFlags.js"; -export { InternalWalletState } from "./operations/state.js"; +export { InternalWalletState } from "./common.js"; export * from "./wallet-api-types.js"; export * from "./wallet.js"; diff --git a/packages/taler-wallet-core/src/operations/README.md b/packages/taler-wallet-core/src/operations/README.md @@ -0,0 +1,7 @@ +# Wallet Operations + +This folder contains the implementations for all wallet operations that operate on the wallet state. + +To avoid cyclic dependencies, these files must **not** reference each other. Instead, other operations should only be accessed via injected dependencies. + +Avoiding cyclic dependencies is important for module bundlers. +\ No newline at end of file diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts b/packages/taler-wallet-core/src/operations/backup/export.ts @@ -49,7 +49,7 @@ import { BackupRefreshSession, BackupExchangeDetails, } from "@gnu-taler/taler-util"; -import { InternalWalletState } from "../state.js"; +import { InternalWalletState } from "../../common.js"; import { provideBackupState, getWalletBackupState } from "./state"; import { Amounts, getTimestampNow } from "@gnu-taler/taler-util"; import { diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts @@ -51,7 +51,7 @@ import { j2s } from "@gnu-taler/taler-util"; import { checkDbInvariant, checkLogicInvariant } from "../../util/invariants"; import { Logger } from "@gnu-taler/taler-util"; import { initRetryInfo } from "../../util/retries.js"; -import { InternalWalletState } from "../state.js"; +import { InternalWalletState } from "../../common.js"; import { provideBackupState } from "./state.js"; import { makeEventId, TombstoneTag } from "../transactions.js"; import { getExchangeDetails } from "../exchanges.js"; diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts b/packages/taler-wallet-core/src/operations/backup/index.ts @@ -24,7 +24,7 @@ /** * Imports. */ -import { InternalWalletState } from "../state.js"; +import { InternalWalletState } from "../../common.js"; import { AmountString, BackupRecovery, diff --git a/packages/taler-wallet-core/src/operations/backup/state.ts b/packages/taler-wallet-core/src/operations/backup/state.ts @@ -23,7 +23,7 @@ import { } from "../../db.js"; import { checkDbInvariant } from "../../util/invariants.js"; import { GetReadOnlyAccess } from "../../util/query.js"; -import { InternalWalletState } from "../state.js"; +import { InternalWalletState } from "../../common.js"; export async function provideBackupState( ws: InternalWalletState, diff --git a/packages/taler-wallet-core/src/operations/balance.ts b/packages/taler-wallet-core/src/operations/balance.ts @@ -23,12 +23,11 @@ import { Amounts, Logger, } from "@gnu-taler/taler-util"; - import { CoinStatus, WalletStoresV1 } from "../db.js"; import { GetReadOnlyAccess } from "../util/query.js"; -import { InternalWalletState } from "./state.js"; +import { InternalWalletState } from "../common.js"; -const logger = new Logger("withdraw.ts"); +const logger = new Logger("operations/balance.ts"); interface WalletBalance { available: AmountJson; diff --git a/packages/taler-wallet-core/src/operations/currencies.ts b/packages/taler-wallet-core/src/operations/currencies.ts @@ -1,81 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 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 <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { ExchangeRecord } from "../db.js"; -import { Logger } from "@gnu-taler/taler-util"; -import { getExchangeDetails } from "./exchanges.js"; -import { InternalWalletState } from "./state.js"; - -const logger = new Logger("currencies.ts"); - -export interface TrustInfo { - isTrusted: boolean; - isAudited: boolean; -} - -/** - * Check if and how an exchange is trusted and/or audited. - */ -export async function getExchangeTrust( - ws: InternalWalletState, - exchangeInfo: ExchangeRecord, -): Promise<TrustInfo> { - let isTrusted = false; - let isAudited = false; - - return await ws.db - .mktx((x) => ({ - exchanges: x.exchanges, - exchangeDetails: x.exchangeDetails, - exchangesTrustStore: x.exchangeTrust, - auditorTrust: x.auditorTrust, - })) - .runReadOnly(async (tx) => { - const exchangeDetails = await getExchangeDetails( - tx, - exchangeInfo.baseUrl, - ); - - if (!exchangeDetails) { - throw Error(`exchange ${exchangeInfo.baseUrl} details not available`); - } - const exchangeTrustRecord = await tx.exchangesTrustStore.indexes.byExchangeMasterPub.get( - exchangeDetails.masterPublicKey, - ); - if ( - exchangeTrustRecord && - exchangeTrustRecord.uids.length > 0 && - exchangeTrustRecord.currency === exchangeDetails.currency - ) { - isTrusted = true; - } - - for (const auditor of exchangeDetails.auditors) { - const auditorTrustRecord = await tx.auditorTrust.indexes.byAuditorPub.get( - auditor.auditor_pub, - ); - if (auditorTrustRecord && auditorTrustRecord.uids.length > 0) { - isAudited = true; - break; - } - } - - return { isTrusted, isAudited }; - }); -} diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts @@ -14,19 +14,10 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { kdf } from "../crypto/primitives/kdf.js"; -import { - encodeCrock, - getRandomBytes, - stringToBytes, -} from "../crypto/talerCrypto.js"; -import { selectPayCoins } from "../util/coinSelection.js"; -import { canonicalJson } from "@gnu-taler/taler-util"; -import { readSuccessResponseJsonOrThrow } from "../util/http.js"; -import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js"; import { Amounts, buildCodecForObject, + canonicalJson, Codec, codecForString, codecForTimestamp, @@ -36,6 +27,7 @@ import { CreateDepositGroupResponse, durationFromSpec, getTimestampNow, + Logger, NotificationType, parsePaytoUri, TalerErrorDetails, @@ -45,7 +37,20 @@ import { TrackDepositGroupRequest, TrackDepositGroupResponse, } from "@gnu-taler/taler-util"; -import { URL } from "../util/url"; +import { InternalWalletState } from "../common.js"; +import { kdf } from "../crypto/primitives/kdf.js"; +import { + encodeCrock, + getRandomBytes, + stringToBytes, +} from "../crypto/talerCrypto.js"; +import { DepositGroupRecord } from "../db.js"; +import { guardOperationException } from "../errors.js"; +import { selectPayCoins } from "../util/coinSelection.js"; +import { readSuccessResponseJsonOrThrow } from "../util/http.js"; +import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js"; +import { URL } from "../util/url.js"; +import { getExchangeDetails } from "./exchanges.js"; import { applyCoinSpend, extractContractData, @@ -54,12 +59,6 @@ import { getEffectiveDepositAmount, getTotalPaymentCost, } from "./pay.js"; -import { InternalWalletState } from "./state.js"; -import { Logger } from "@gnu-taler/taler-util"; -import { DepositGroupRecord } from "../db.js"; - -import { guardOperationException } from "./errors.js"; -import { getExchangeDetails } from "./exchanges.js"; /** * Logger. diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts @@ -20,6 +20,7 @@ import { Amounts, Auditor, + canonicalizeBaseUrl, codecForExchangeKeysJson, codecForExchangeWireJson, compare, @@ -30,6 +31,7 @@ import { ExchangeWireJson, getTimestampNow, isTimestampExpired, + j2s, Logger, NotificationType, parsePaytoUri, @@ -38,38 +40,37 @@ import { TalerErrorDetails, Timestamp, } from "@gnu-taler/taler-util"; +import { decodeCrock, encodeCrock, hash } from "../crypto/talerCrypto.js"; +import { CryptoApi } from "../crypto/workers/cryptoApi.js"; import { DenominationRecord, DenominationStatus, + ExchangeDetailsRecord, ExchangeRecord, + WalletStoresV1, WireFee, - ExchangeDetailsRecord, WireInfo, - WalletStoresV1, } from "../db.js"; -import { j2s, canonicalizeBaseUrl } from "@gnu-taler/taler-util"; -import { updateRetryInfoTimeout, initRetryInfo } from "../util/retries.js"; -import { - makeErrorDetails, - guardOperationException, - OperationFailedError, -} from "./errors.js"; -import { createRecoupGroup, processRecoupGroup } from "./recoup.js"; -import { InternalWalletState } from "./state.js"; -import { - WALLET_CACHE_BREAKER_CLIENT_VERSION, - WALLET_EXCHANGE_PROTOCOL_VERSION, -} from "./versions.js"; import { getExpiryTimestamp, HttpRequestLibrary, readSuccessResponseJsonOrThrow, readSuccessResponseTextOrThrow, } from "../util/http.js"; -import { CryptoApi } from "../crypto/workers/cryptoApi.js"; import { DbAccess, GetReadOnlyAccess } from "../util/query.js"; -import { decodeCrock, encodeCrock, hash } from "../crypto/talerCrypto.js"; +import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js"; import { URL } from "../util/url.js"; +import { + guardOperationException, + makeErrorDetails, + OperationFailedError, +} from "../errors.js"; +import { createRecoupGroup, processRecoupGroup } from "./recoup.js"; +import { InternalWalletState, TrustInfo } from "../common.js"; +import { + WALLET_CACHE_BREAKER_CLIENT_VERSION, + WALLET_EXCHANGE_PROTOCOL_VERSION, +} from "../versions.js"; const logger = new Logger("exchanges.ts"); @@ -605,3 +606,54 @@ export async function getExchangePaytoUri( } throw Error("no matching exchange account found"); } + +/** + * Check if and how an exchange is trusted and/or audited. + */ +export async function getExchangeTrust( + ws: InternalWalletState, + exchangeInfo: ExchangeRecord, +): Promise<TrustInfo> { + let isTrusted = false; + let isAudited = false; + + return await ws.db + .mktx((x) => ({ + exchanges: x.exchanges, + exchangeDetails: x.exchangeDetails, + exchangesTrustStore: x.exchangeTrust, + auditorTrust: x.auditorTrust, + })) + .runReadOnly(async (tx) => { + const exchangeDetails = await getExchangeDetails( + tx, + exchangeInfo.baseUrl, + ); + + if (!exchangeDetails) { + throw Error(`exchange ${exchangeInfo.baseUrl} details not available`); + } + const exchangeTrustRecord = await tx.exchangesTrustStore.indexes.byExchangeMasterPub.get( + exchangeDetails.masterPublicKey, + ); + if ( + exchangeTrustRecord && + exchangeTrustRecord.uids.length > 0 && + exchangeTrustRecord.currency === exchangeDetails.currency + ) { + isTrusted = true; + } + + for (const auditor of exchangeDetails.auditors) { + const auditorTrustRecord = await tx.auditorTrust.indexes.byAuditorPub.get( + auditor.auditor_pub, + ); + if (auditorTrustRecord && auditorTrustRecord.uids.length > 0) { + isAudited = true; + break; + } + } + + return { isTrusted, isAudited }; + }); +} diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts @@ -68,7 +68,7 @@ import { getRetryDuration, } from "../util/retries.js"; import { getTotalRefreshCost, createRefreshGroup } from "./refresh.js"; -import { InternalWalletState, EXCHANGE_COINS_LOCK } from "./state.js"; +import { InternalWalletState, EXCHANGE_COINS_LOCK } from "../common.js"; import { ContractTermsUtil } from "../util/contractTerms.js"; import { getExchangeDetails } from "./exchanges.js"; import { GetReadWriteAccess } from "../util/query.js"; @@ -98,7 +98,7 @@ import { makeErrorDetails, OperationFailedAndReportedError, OperationFailedError, -} from "./errors.js"; +} from "../errors.js"; import { URL } from "../util/url.js"; /** diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts @@ -29,7 +29,7 @@ import { ReserveType, } from "../pending-types.js"; import { getTimestampNow, Timestamp } from "@gnu-taler/taler-util"; -import { InternalWalletState } from "./state.js"; +import { InternalWalletState } from "../common.js"; import { getBalancesInsideTransaction } from "./balance.js"; import { GetReadOnlyAccess } from "../util/query.js"; diff --git a/packages/taler-wallet-core/src/operations/recoup.ts b/packages/taler-wallet-core/src/operations/recoup.ts @@ -48,10 +48,10 @@ import { readSuccessResponseJsonOrThrow } from "../util/http.js"; import { Logger } from "@gnu-taler/taler-util"; import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js"; import { URL } from "../util/url.js"; -import { guardOperationException } from "./errors.js"; +import { guardOperationException } from "../errors.js"; import { createRefreshGroup, processRefreshGroup } from "./refresh.js"; import { getReserveRequestTimeout, processReserve } from "./reserves.js"; -import { InternalWalletState } from "./state.js"; +import { InternalWalletState } from "../common.js"; import { GetReadWriteAccess } from "../util/query.js"; const logger = new Logger("operations/recoup.ts"); diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts @@ -52,9 +52,9 @@ import { timestampMin, } from "@gnu-taler/taler-util"; import { URL } from "../util/url.js"; -import { guardOperationException } from "./errors.js"; +import { guardOperationException } from "../errors.js"; import { updateExchangeFromUrl } from "./exchanges.js"; -import { EXCHANGE_COINS_LOCK, InternalWalletState } from "./state.js"; +import { EXCHANGE_COINS_LOCK, InternalWalletState } from "../common.js"; import { isWithdrawableDenom, selectWithdrawalDenominations, diff --git a/packages/taler-wallet-core/src/operations/refund.ts b/packages/taler-wallet-core/src/operations/refund.ts @@ -23,12 +23,7 @@ /** * Imports. */ -import { InternalWalletState } from "./state.js"; -import { guardOperationException } from "./errors.js"; import { - getTimestampNow, - timestampAddDuration, - TalerErrorDetails, AbortingCoin, AbortRequest, AmountJson, @@ -37,29 +32,34 @@ import { codecForAbortResponse, codecForMerchantOrderRefundPickupResponse, CoinPublicKey, + getTimestampNow, + Logger, MerchantCoinRefundFailureStatus, MerchantCoinRefundStatus, MerchantCoinRefundSuccessStatus, NotificationType, parseRefundUri, RefreshReason, + TalerErrorCode, + TalerErrorDetails, + timestampAddDuration, } from "@gnu-taler/taler-util"; -import { Logger } from "@gnu-taler/taler-util"; -import { readSuccessResponseJsonOrThrow } from "../util/http.js"; -import { URL } from "../util/url.js"; -import { updateRetryInfoTimeout, initRetryInfo } from "../util/retries.js"; -import { checkDbInvariant } from "../util/invariants.js"; -import { TalerErrorCode } from "@gnu-taler/taler-util"; import { - PurchaseRecord, - CoinStatus, - RefundState, AbortStatus, + CoinStatus, + PurchaseRecord, RefundReason, + RefundState, WalletStoresV1, } from "../db.js"; -import { getTotalRefreshCost, createRefreshGroup } from "./refresh.js"; +import { readSuccessResponseJsonOrThrow } from "../util/http.js"; +import { checkDbInvariant } from "../util/invariants.js"; import { GetReadWriteAccess } from "../util/query.js"; +import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js"; +import { URL } from "../util/url.js"; +import { guardOperationException } from "../errors.js"; +import { createRefreshGroup, getTotalRefreshCost } from "./refresh.js"; +import { InternalWalletState } from "../common.js"; const logger = new Logger("refund.ts"); diff --git a/packages/taler-wallet-core/src/operations/reserves.ts b/packages/taler-wallet-core/src/operations/reserves.ts @@ -47,13 +47,14 @@ import { getRetryDuration, updateRetryInfoTimeout, } from "../util/retries.js"; -import { guardOperationException, OperationFailedError } from "./errors.js"; +import { guardOperationException, OperationFailedError } from "../errors.js"; import { updateExchangeFromUrl, getExchangePaytoUri, getExchangeDetails, + getExchangeTrust, } from "./exchanges.js"; -import { InternalWalletState } from "./state.js"; +import { InternalWalletState } from "../common.js"; import { updateWithdrawalDenoms, getCandidateWithdrawalDenoms, @@ -62,7 +63,6 @@ import { processWithdrawGroup, getBankWithdrawalInfo, } from "./withdraw.js"; -import { getExchangeTrust } from "./currencies.js"; import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto.js"; import { Logger } from "@gnu-taler/taler-util"; import { diff --git a/packages/taler-wallet-core/src/operations/state.ts b/packages/taler-wallet-core/src/operations/state.ts @@ -1,148 +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 <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { - WalletNotification, - BalancesResponse, - Logger, -} from "@gnu-taler/taler-util"; -import { CryptoApi, CryptoWorkerFactory } from "../crypto/workers/cryptoApi.js"; -import { WalletStoresV1 } from "../db.js"; -import { PendingOperationsResponse } from "../pending-types.js"; -import { AsyncOpMemoMap, AsyncOpMemoSingle } from "../util/asyncMemo.js"; -import { HttpRequestLibrary } from "../util/http"; -import { - AsyncCondition, - OpenedPromise, - openPromise, -} from "../util/promiseUtils.js"; -import { DbAccess } from "../util/query.js"; -import { TimerGroup } from "../util/timer.js"; - -type NotificationListener = (n: WalletNotification) => void; - -const logger = new Logger("state.ts"); - -export const EXCHANGE_COINS_LOCK = "exchange-coins-lock"; -export const EXCHANGE_RESERVES_LOCK = "exchange-reserves-lock"; - -/** - * Internal state of the wallet. - */ -export class InternalWalletState { - memoProcessReserve: AsyncOpMemoMap<void> = new AsyncOpMemoMap(); - memoMakePlanchet: AsyncOpMemoMap<void> = new AsyncOpMemoMap(); - memoGetPending: AsyncOpMemoSingle<PendingOperationsResponse> = new AsyncOpMemoSingle(); - memoGetBalance: AsyncOpMemoSingle<BalancesResponse> = new AsyncOpMemoSingle(); - memoProcessRefresh: AsyncOpMemoMap<void> = new AsyncOpMemoMap(); - memoProcessRecoup: AsyncOpMemoMap<void> = new AsyncOpMemoMap(); - memoProcessDeposit: AsyncOpMemoMap<void> = new AsyncOpMemoMap(); - cryptoApi: CryptoApi; - - timerGroup: TimerGroup = new TimerGroup(); - latch = new AsyncCondition(); - stopped = false; - memoRunRetryLoop = new AsyncOpMemoSingle<void>(); - - listeners: NotificationListener[] = []; - - initCalled: boolean = false; - - /** - * Promises that are waiting for a particular resource. - */ - private resourceWaiters: Record<string, OpenedPromise<void>[]> = {}; - - /** - * Resources that are currently locked. - */ - private resourceLocks: Set<string> = new Set(); - - constructor( - // FIXME: Make this a getter and make - // the actual value nullable. - // Check if we are in a DB migration / garbage collection - // and throw an error in that case. - public db: DbAccess<typeof WalletStoresV1>, - public http: HttpRequestLibrary, - cryptoWorkerFactory: CryptoWorkerFactory, - ) { - this.cryptoApi = new CryptoApi(cryptoWorkerFactory); - } - - notify(n: WalletNotification): void { - logger.trace("Notification", n); - for (const l of this.listeners) { - const nc = JSON.parse(JSON.stringify(n)); - setTimeout(() => { - l(nc); - }, 0); - } - } - - addNotificationListener(f: (n: WalletNotification) => void): void { - this.listeners.push(f); - } - - /** - * Stop ongoing processing. - */ - stop(): void { - this.stopped = true; - this.timerGroup.stopCurrentAndFutureTimers(); - this.cryptoApi.stop(); - } - - /** - * Run an async function after acquiring a list of locks, identified - * by string tokens. - */ - async runSequentialized<T>(tokens: string[], f: () => Promise<T>) { - // Make sure locks are always acquired in the same order - tokens = [...tokens].sort(); - - for (const token of tokens) { - if (this.resourceLocks.has(token)) { - const p = openPromise<void>(); - let waitList = this.resourceWaiters[token]; - if (!waitList) { - waitList = this.resourceWaiters[token] = []; - } - waitList.push(p); - await p.promise; - } - this.resourceLocks.add(token); - } - - try { - logger.trace(`begin exclusive execution on ${JSON.stringify(tokens)}`); - const result = await f(); - logger.trace(`end exclusive execution on ${JSON.stringify(tokens)}`); - return result; - } finally { - for (const token of tokens) { - this.resourceLocks.delete(token); - let waiter = (this.resourceWaiters[token] ?? []).shift(); - if (waiter) { - waiter.resolve(); - } - } - } - } -} diff --git a/packages/taler-wallet-core/src/operations/testing.ts b/packages/taler-wallet-core/src/operations/testing.ts @@ -34,7 +34,7 @@ import { PreparePayResultType, } from "@gnu-taler/taler-util"; import { createTalerWithdrawReserve } from "./reserves.js"; -import { InternalWalletState } from "./state.js"; +import { InternalWalletState } from "../common.js"; import { URL } from "../util/url.js"; import { confirmPay, preparePayForUri } from "./pay.js"; import { getBalances } from "./balance.js"; diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts @@ -40,9 +40,9 @@ import { import { j2s } from "@gnu-taler/taler-util"; import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js"; -import { guardOperationException, makeErrorDetails } from "./errors.js"; +import { guardOperationException, makeErrorDetails } from "../errors.js"; import { updateExchangeFromUrl } from "./exchanges.js"; -import { InternalWalletState } from "./state.js"; +import { InternalWalletState } from "../common.js"; import { getExchangeWithdrawalInfo, updateWithdrawalDenoms, diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts @@ -17,7 +17,7 @@ /** * Imports. */ -import { InternalWalletState } from "./state.js"; +import { InternalWalletState } from "../common.js"; import { WalletRefundItem, RefundState, diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -17,65 +17,56 @@ /** * Imports. */ +import * as LibtoolVersion from "@gnu-taler/taler-util"; import { AmountJson, Amounts, + BankWithdrawDetails, + codecForTalerConfigResponse, + codecForWithdrawOperationStatusResponse, + codecForWithdrawResponse, + compare, durationFromSpec, + ExchangeListItem, + getDurationRemaining, + getTimestampNow, + Logger, + NotificationType, parseWithdrawUri, + TalerErrorCode, + TalerErrorDetails, Timestamp, + timestampCmp, + timestampSubtractDuraction, + WithdrawResponse, + WithdrawUriInfoResponse, } from "@gnu-taler/taler-util"; import { - DenominationRecord, - DenominationStatus, - CoinStatus, CoinRecord, CoinSourceType, + CoinStatus, + DenominationRecord, DenominationSelectionInfo, - PlanchetRecord, + DenominationStatus, DenomSelectionState, - ExchangeRecord, ExchangeDetailsRecord, + ExchangeRecord, + PlanchetRecord, } from "../db.js"; -import { - BankWithdrawDetails, - TalerErrorDetails, - ExchangeListItem, - WithdrawUriInfoResponse, -} from "@gnu-taler/taler-util"; -import { - codecForWithdrawOperationStatusResponse, - codecForWithdrawResponse, - WithdrawResponse, - codecForTalerConfigResponse, -} from "@gnu-taler/taler-util"; -import { InternalWalletState } from "./state.js"; -import { Logger } from "@gnu-taler/taler-util"; -import { getExchangeDetails, updateExchangeFromUrl } from "./exchanges.js"; -import { - WALLET_EXCHANGE_PROTOCOL_VERSION, - WALLET_BANK_INTEGRATION_PROTOCOL_VERSION, -} from "./versions.js"; - -import * as LibtoolVersion from "@gnu-taler/taler-util"; +import { walletCoreDebugFlags } from "../util/debugFlags.js"; +import { readSuccessResponseJsonOrThrow } from "../util/http.js"; +import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js"; +import { URL } from "../util/url.js"; import { guardOperationException, makeErrorDetails, OperationFailedError, -} from "./errors.js"; -import { NotificationType } from "@gnu-taler/taler-util"; +} from "../errors.js"; +import { InternalWalletState } from "../common.js"; import { - getTimestampNow, - getDurationRemaining, - timestampCmp, - timestampSubtractDuraction, -} from "@gnu-taler/taler-util"; -import { readSuccessResponseJsonOrThrow } from "../util/http.js"; -import { URL } from "../util/url.js"; -import { TalerErrorCode } from "@gnu-taler/taler-util"; -import { updateRetryInfoTimeout, initRetryInfo } from "../util/retries.js"; -import { compare } from "@gnu-taler/taler-util"; -import { walletCoreDebugFlags } from "../util/debugFlags.js"; -import { getExchangeTrust } from "./currencies.js"; + WALLET_BANK_INTEGRATION_PROTOCOL_VERSION, + WALLET_EXCHANGE_PROTOCOL_VERSION, +} from "../versions.js"; /** * Logger for this file. @@ -690,7 +681,7 @@ export async function updateWithdrawalDenoms( exchangeDetails: x.exchangeDetails, })) .runReadOnly(async (tx) => { - return getExchangeDetails(tx, exchangeBaseUrl); + return ws.exchangeOps.getExchangeDetails(tx, exchangeBaseUrl); }); if (!exchangeDetails) { logger.error("exchange details not available"); @@ -816,7 +807,10 @@ async function processWithdrawGroupImpl( return; } - await updateExchangeFromUrl(ws, withdrawalGroup.exchangeBaseUrl); + await ws.exchangeOps.updateExchangeFromUrl( + ws, + withdrawalGroup.exchangeBaseUrl, + ); const numTotalCoins = withdrawalGroup.denomsSel.selectedDenoms .map((x) => x.count) @@ -910,10 +904,10 @@ export async function getExchangeWithdrawalInfo( baseUrl: string, amount: AmountJson, ): Promise<ExchangeWithdrawDetails> { - const { exchange, exchangeDetails } = await updateExchangeFromUrl( - ws, - baseUrl, - ); + const { + exchange, + exchangeDetails, + } = await ws.exchangeOps.updateExchangeFromUrl(ws, baseUrl); await updateWithdrawalDenoms(ws, baseUrl); const denoms = await getCandidateWithdrawalDenoms(ws, baseUrl); const selectedDenoms = selectWithdrawalDenominations(amount, denoms); @@ -922,7 +916,10 @@ export async function getExchangeWithdrawalInfo( exchangeWireAccounts.push(account.payto_uri); } - const { isTrusted, isAudited } = await getExchangeTrust(ws, exchange); + const { isTrusted, isAudited } = await ws.exchangeOps.getExchangeTrust( + ws, + exchange, + ); let earliestDepositExpiration = selectedDenoms.selectedDenoms[0].denom.stampExpireDeposit; @@ -1009,7 +1006,7 @@ export async function getWithdrawalDetailsForUri( // FIXME: right now the exchange gets permanently added, // we might want to only temporarily add it. try { - await updateExchangeFromUrl(ws, info.suggestedExchange); + await ws.exchangeOps.updateExchangeFromUrl(ws, info.suggestedExchange); } catch (e) { // We still continued if it failed, as other exchanges might be available. // We don't want to fail if the bank-suggested exchange is broken/offline. @@ -1029,7 +1026,7 @@ export async function getWithdrawalDetailsForUri( .runReadOnly(async (tx) => { const exchangeRecords = await tx.exchanges.iter().toArray(); for (const r of exchangeRecords) { - const details = await getExchangeDetails(tx, r.baseUrl); + const details = await ws.exchangeOps.getExchangeDetails(tx, r.baseUrl); if (details) { exchanges.push({ exchangeBaseUrl: details.exchangeBaseUrl, diff --git a/packages/taler-wallet-core/src/pending-types.ts b/packages/taler-wallet-core/src/pending-types.ts @@ -27,7 +27,6 @@ import { TalerErrorDetails, BalancesResponse, - Duration, Timestamp, } from "@gnu-taler/taler-util"; import { ReserveRecordStatus } from "./db.js"; diff --git a/packages/taler-wallet-core/src/util/http.ts b/packages/taler-wallet-core/src/util/http.ts @@ -27,7 +27,7 @@ import { OperationFailedError, makeErrorDetails, -} from "../operations/errors.js"; +} from "../errors.js"; import { Logger, Duration, diff --git a/packages/taler-wallet-core/src/operations/versions.ts b/packages/taler-wallet-core/src/versions.ts diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts @@ -55,7 +55,7 @@ import { makeErrorDetails, OperationFailedAndReportedError, OperationFailedError, -} from "./operations/errors"; +} from "./errors"; import { acceptExchangeTermsOfService, getExchangeDetails, @@ -85,7 +85,7 @@ import { getFundingPaytoUris, processReserve, } from "./operations/reserves"; -import { InternalWalletState } from "./operations/state"; +import { InternalWalletState } from "./common"; import { runIntegrationTest, testPay,