summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/wallet.ts
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2020-08-03 13:00:48 +0530
committerFlorian Dold <florian.dold@gmail.com>2020-08-03 13:01:05 +0530
commitffd2a62c3f7df94365980302fef3bc3376b48182 (patch)
tree270af6f16b4cc7f5da2afdba55c8bc9dbea5eca5 /packages/taler-wallet-core/src/wallet.ts
parentaa481e42675fb7c4dcbbeec0ba1c61e1953b9596 (diff)
downloadwallet-core-ffd2a62c3f7df94365980302fef3bc3376b48182.tar.gz
wallet-core-ffd2a62c3f7df94365980302fef3bc3376b48182.tar.bz2
wallet-core-ffd2a62c3f7df94365980302fef3bc3376b48182.zip
modularize repo, use pnpm, improve typechecking
Diffstat (limited to 'packages/taler-wallet-core/src/wallet.ts')
-rw-r--r--packages/taler-wallet-core/src/wallet.ts882
1 files changed, 882 insertions, 0 deletions
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
new file mode 100644
index 000000000..4a409f58d
--- /dev/null
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -0,0 +1,882 @@
+/*
+ This file is part of GNU Taler
+ (C) 2015-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/>
+ */
+
+/**
+ * High-level wallet operations that should be indepentent from the underlying
+ * browser extension interface.
+ */
+
+/**
+ * Imports.
+ */
+import { CryptoWorkerFactory } from "./crypto/workers/cryptoApi";
+import { HttpRequestLibrary } from "./util/http";
+import { Database } from "./util/query";
+
+import { Amounts, AmountJson } from "./util/amounts";
+
+import {
+ getExchangeWithdrawalInfo,
+ getWithdrawalDetailsForUri,
+} from "./operations/withdraw";
+
+import {
+ preparePayForUri,
+ refuseProposal,
+ confirmPay,
+ processDownloadProposal,
+ processPurchasePay,
+} from "./operations/pay";
+
+import {
+ CoinRecord,
+ CurrencyRecord,
+ DenominationRecord,
+ ExchangeRecord,
+ PurchaseRecord,
+ ReserveRecord,
+ Stores,
+ ReserveRecordStatus,
+ CoinSourceType,
+ RefundState,
+} from "./types/dbTypes";
+import { CoinDumpJson, WithdrawUriInfoResponse } from "./types/talerTypes";
+import {
+ BenchmarkResult,
+ ConfirmPayResult,
+ ReturnCoinsRequest,
+ SenderWireInfos,
+ TipStatus,
+ PreparePayResult,
+ AcceptWithdrawalResponse,
+ PurchaseDetails,
+ RefreshReason,
+ ExchangeListItem,
+ ExchangesListRespose,
+ ManualWithdrawalDetails,
+ GetExchangeTosResult,
+ AcceptManualWithdrawalResult,
+ BalancesResponse,
+} from "./types/walletTypes";
+import { Logger } from "./util/logging";
+
+import { assertUnreachable } from "./util/assertUnreachable";
+
+import {
+ updateExchangeFromUrl,
+ getExchangeTrust,
+ getExchangePaytoUri,
+ acceptExchangeTermsOfService,
+} from "./operations/exchanges";
+import {
+ processReserve,
+ createTalerWithdrawReserve,
+ forceQueryReserve,
+ getFundingPaytoUris,
+} from "./operations/reserves";
+
+import { InternalWalletState } from "./operations/state";
+import { createReserve } from "./operations/reserves";
+import { processRefreshGroup, createRefreshGroup } from "./operations/refresh";
+import { processWithdrawGroup } from "./operations/withdraw";
+import { getPendingOperations } from "./operations/pending";
+import { getBalances } from "./operations/balance";
+import { acceptTip, getTipStatus, processTip } from "./operations/tip";
+import { TimerGroup } from "./util/timer";
+import { AsyncCondition } from "./util/promiseUtils";
+import { AsyncOpMemoSingle } from "./util/asyncMemo";
+import {
+ PendingOperationInfo,
+ PendingOperationsResponse,
+ PendingOperationType,
+} from "./types/pending";
+import { WalletNotification, NotificationType } from "./types/notifications";
+import { processPurchaseQueryRefund, applyRefund } from "./operations/refund";
+import { durationMin, Duration } from "./util/time";
+import { processRecoupGroup } from "./operations/recoup";
+import { OperationFailedAndReportedError } from "./operations/errors";
+import {
+ TransactionsRequest,
+ TransactionsResponse,
+} from "./types/transactions";
+import { getTransactions } from "./operations/transactions";
+import { withdrawTestBalance } from "./operations/testing";
+
+const builtinCurrencies: CurrencyRecord[] = [
+ {
+ auditors: [
+ {
+ auditorPub: "BW9DC48PHQY4NH011SHHX36DZZ3Q22Y6X7FZ1VD1CMZ2PTFZ6PN0",
+ baseUrl: "https://auditor.demo.taler.net/",
+ expirationStamp: new Date(2027, 1).getTime(),
+ },
+ ],
+ exchanges: [],
+ fractionalDigits: 2,
+ name: "KUDOS",
+ },
+];
+
+const logger = new Logger("wallet.ts");
+
+/**
+ * The platform-independent wallet implementation.
+ */
+export class Wallet {
+ private ws: InternalWalletState;
+ private timerGroup: TimerGroup = new TimerGroup();
+ private latch = new AsyncCondition();
+ private stopped = false;
+ private memoRunRetryLoop = new AsyncOpMemoSingle<void>();
+
+ get db(): Database {
+ return this.ws.db;
+ }
+
+ constructor(
+ db: Database,
+ http: HttpRequestLibrary,
+ cryptoWorkerFactory: CryptoWorkerFactory,
+ ) {
+ this.ws = new InternalWalletState(db, http, cryptoWorkerFactory);
+ }
+
+ getExchangePaytoUri(
+ exchangeBaseUrl: string,
+ supportedTargetTypes: string[],
+ ): Promise<string> {
+ return getExchangePaytoUri(this.ws, exchangeBaseUrl, supportedTargetTypes);
+ }
+
+ async getWithdrawalDetailsForAmount(
+ exchangeBaseUrl: string,
+ amount: AmountJson,
+ ): Promise<ManualWithdrawalDetails> {
+ const wi = await getExchangeWithdrawalInfo(
+ this.ws,
+ exchangeBaseUrl,
+ amount,
+ );
+ const paytoUris = wi.exchangeInfo.wireInfo?.accounts.map(
+ (x) => x.payto_uri,
+ );
+ if (!paytoUris) {
+ throw Error("exchange is in invalid state");
+ }
+ return {
+ amountRaw: Amounts.stringify(amount),
+ amountEffective: Amounts.stringify(wi.selectedDenoms.totalCoinValue),
+ paytoUris,
+ tosAccepted: wi.termsOfServiceAccepted,
+ };
+ }
+
+ addNotificationListener(f: (n: WalletNotification) => void): void {
+ this.ws.addNotificationListener(f);
+ }
+
+ /**
+ * Execute one operation based on the pending operation info record.
+ */
+ async processOnePendingOperation(
+ pending: PendingOperationInfo,
+ forceNow = false,
+ ): Promise<void> {
+ logger.trace(`running pending ${JSON.stringify(pending, undefined, 2)}`);
+ switch (pending.type) {
+ case PendingOperationType.Bug:
+ // Nothing to do, will just be displayed to the user
+ return;
+ case PendingOperationType.ExchangeUpdate:
+ await updateExchangeFromUrl(this.ws, pending.exchangeBaseUrl, forceNow);
+ break;
+ case PendingOperationType.Refresh:
+ await processRefreshGroup(this.ws, pending.refreshGroupId, forceNow);
+ break;
+ case PendingOperationType.Reserve:
+ await processReserve(this.ws, pending.reservePub, forceNow);
+ break;
+ case PendingOperationType.Withdraw:
+ await processWithdrawGroup(
+ this.ws,
+ pending.withdrawalGroupId,
+ forceNow,
+ );
+ break;
+ case PendingOperationType.ProposalChoice:
+ // Nothing to do, user needs to accept/reject
+ break;
+ case PendingOperationType.ProposalDownload:
+ await processDownloadProposal(this.ws, pending.proposalId, forceNow);
+ break;
+ case PendingOperationType.TipChoice:
+ // Nothing to do, user needs to accept/reject
+ break;
+ case PendingOperationType.TipPickup:
+ await processTip(this.ws, pending.tipId, forceNow);
+ break;
+ case PendingOperationType.Pay:
+ await processPurchasePay(this.ws, pending.proposalId, forceNow);
+ break;
+ case PendingOperationType.RefundQuery:
+ await processPurchaseQueryRefund(this.ws, pending.proposalId, forceNow);
+ break;
+ case PendingOperationType.Recoup:
+ await processRecoupGroup(this.ws, pending.recoupGroupId, forceNow);
+ break;
+ default:
+ assertUnreachable(pending);
+ }
+ }
+
+ /**
+ * Process pending operations.
+ */
+ public async runPending(forceNow = false): Promise<void> {
+ const onlyDue = !forceNow;
+ const pendingOpsResponse = await this.getPendingOperations({ onlyDue });
+ for (const p of pendingOpsResponse.pendingOperations) {
+ try {
+ await this.processOnePendingOperation(p, forceNow);
+ } catch (e) {
+ if (e instanceof OperationFailedAndReportedError) {
+ console.error(
+ "Operation failed:",
+ JSON.stringify(e.operationError, undefined, 2),
+ );
+ } else {
+ console.error(e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Run the wallet until there are no more pending operations that give
+ * liveness left. The wallet will be in a stopped state when this function
+ * returns without resolving to an exception.
+ */
+ public async runUntilDone(): Promise<void> {
+ let done = false;
+ const p = new Promise((resolve, reject) => {
+ // Run this asynchronously
+ this.addNotificationListener((n) => {
+ if (done) {
+ return;
+ }
+ if (
+ n.type === NotificationType.WaitingForRetry &&
+ n.numGivingLiveness == 0
+ ) {
+ done = true;
+ logger.trace("no liveness-giving operations left");
+ resolve();
+ }
+ });
+ this.runRetryLoop().catch((e) => {
+ console.log("exception in wallet retry loop");
+ reject(e);
+ });
+ });
+ await p;
+ }
+
+ /**
+ * Run the wallet until there are no more pending operations that give
+ * liveness left. The wallet will be in a stopped state when this function
+ * returns without resolving to an exception.
+ */
+ public async runUntilDoneAndStop(): Promise<void> {
+ await this.runUntilDone();
+ logger.trace("stopping after liveness-giving operations done");
+ this.stop();
+ }
+
+ /**
+ * Process pending operations and wait for scheduled operations in
+ * a loop until the wallet is stopped explicitly.
+ */
+ public async runRetryLoop(): Promise<void> {
+ // Make sure we only run one main loop at a time.
+ return this.memoRunRetryLoop.memo(async () => {
+ try {
+ await this.runRetryLoopImpl();
+ } catch (e) {
+ console.error("error during retry loop execution", e);
+ throw e;
+ }
+ });
+ }
+
+ private async runRetryLoopImpl(): Promise<void> {
+ while (!this.stopped) {
+ const pending = await this.getPendingOperations({ onlyDue: true });
+ if (pending.pendingOperations.length === 0) {
+ const allPending = await this.getPendingOperations({ onlyDue: false });
+ let numPending = 0;
+ let numGivingLiveness = 0;
+ for (const p of allPending.pendingOperations) {
+ numPending++;
+ if (p.givesLifeness) {
+ numGivingLiveness++;
+ }
+ }
+ let dt: Duration;
+ if (
+ allPending.pendingOperations.length === 0 ||
+ allPending.nextRetryDelay.d_ms === Number.MAX_SAFE_INTEGER
+ ) {
+ // Wait for 5 seconds
+ dt = { d_ms: 5000 };
+ } else {
+ dt = durationMin({ d_ms: 5000 }, allPending.nextRetryDelay);
+ }
+ const timeout = this.timerGroup.resolveAfter(dt);
+ this.ws.notify({
+ type: NotificationType.WaitingForRetry,
+ numGivingLiveness,
+ numPending,
+ });
+ await Promise.race([timeout, this.latch.wait()]);
+ console.log("timeout done");
+ } else {
+ // FIXME: maybe be a bit smarter about executing these
+ // operations in parallel?
+ for (const p of pending.pendingOperations) {
+ try {
+ await this.processOnePendingOperation(p);
+ } catch (e) {
+ if (e instanceof OperationFailedAndReportedError) {
+ logger.warn("operation processed resulted in reported error");
+ } else {
+ console.error("Uncaught exception", e);
+ this.ws.notify({
+ type: NotificationType.InternalError,
+ message: "uncaught exception",
+ exception: e,
+ });
+ }
+ }
+ this.ws.notify({
+ type: NotificationType.PendingOperationProcessed,
+ });
+ }
+ }
+ }
+ logger.trace("exiting wallet retry loop");
+ }
+
+ /**
+ * Insert the hard-coded defaults for exchanges, coins and
+ * auditors into the database, unless these defaults have
+ * already been applied.
+ */
+ async fillDefaults(): Promise<void> {
+ await this.db.runWithWriteTransaction(
+ [Stores.config, Stores.currencies],
+ async (tx) => {
+ let applied = false;
+ await tx.iter(Stores.config).forEach((x) => {
+ if (x.key == "currencyDefaultsApplied" && x.value == true) {
+ applied = true;
+ }
+ });
+ if (!applied) {
+ for (const c of builtinCurrencies) {
+ await tx.put(Stores.currencies, c);
+ }
+ }
+ },
+ );
+ }
+
+ /**
+ * Check if a payment for the given taler://pay/ URI is possible.
+ *
+ * If the payment is possible, the signature are already generated but not
+ * yet send to the merchant.
+ */
+ async preparePayForUri(talerPayUri: string): Promise<PreparePayResult> {
+ return preparePayForUri(this.ws, talerPayUri);
+ }
+
+ /**
+ * Add a contract to the wallet and sign coins, and send them.
+ */
+ async confirmPay(
+ proposalId: string,
+ sessionIdOverride: string | undefined,
+ ): Promise<ConfirmPayResult> {
+ try {
+ return await confirmPay(this.ws, proposalId, sessionIdOverride);
+ } finally {
+ this.latch.trigger();
+ }
+ }
+
+ /**
+ * First fetch information requred to withdraw from the reserve,
+ * then deplete the reserve, withdrawing coins until it is empty.
+ *
+ * The returned promise resolves once the reserve is set to the
+ * state DORMANT.
+ */
+ async processReserve(reservePub: string): Promise<void> {
+ try {
+ return await processReserve(this.ws, reservePub);
+ } finally {
+ this.latch.trigger();
+ }
+ }
+
+ /**
+ * Create a reserve, but do not flag it as confirmed yet.
+ *
+ * Adds the corresponding exchange as a trusted exchange if it is neither
+ * audited nor trusted already.
+ */
+ async acceptManualWithdrawal(
+ exchangeBaseUrl: string,
+ amount: AmountJson,
+ ): Promise<AcceptManualWithdrawalResult> {
+ try {
+ const resp = await createReserve(this.ws, {
+ amount,
+ exchange: exchangeBaseUrl,
+ });
+ const exchangePaytoUris = await this.db.runWithReadTransaction(
+ [Stores.exchanges, Stores.reserves],
+ (tx) => getFundingPaytoUris(tx, resp.reservePub),
+ );
+ return {
+ reservePub: resp.reservePub,
+ exchangePaytoUris,
+ };
+ } finally {
+ this.latch.trigger();
+ }
+ }
+
+ /**
+ * Check if and how an exchange is trusted and/or audited.
+ */
+ async getExchangeTrust(
+ exchangeInfo: ExchangeRecord,
+ ): Promise<{ isTrusted: boolean; isAudited: boolean }> {
+ return getExchangeTrust(this.ws, exchangeInfo);
+ }
+
+ async getWithdrawalDetailsForUri(
+ talerWithdrawUri: string,
+ ): Promise<WithdrawUriInfoResponse> {
+ return getWithdrawalDetailsForUri(this.ws, talerWithdrawUri);
+ }
+
+ /**
+ * Update or add exchange DB entry by fetching the /keys and /wire information.
+ * Optionally link the reserve entry to the new or existing
+ * exchange entry in then DB.
+ */
+ async updateExchangeFromUrl(
+ baseUrl: string,
+ force = false,
+ ): Promise<ExchangeRecord> {
+ try {
+ return updateExchangeFromUrl(this.ws, baseUrl, force);
+ } finally {
+ this.latch.trigger();
+ }
+ }
+
+ async getExchangeTos(exchangeBaseUrl: string): Promise<GetExchangeTosResult> {
+ const exchange = await this.updateExchangeFromUrl(exchangeBaseUrl);
+ const tos = exchange.termsOfServiceText;
+ const currentEtag = exchange.termsOfServiceLastEtag;
+ if (!tos || !currentEtag) {
+ throw Error("exchange is in invalid state");
+ }
+ return {
+ acceptedEtag: exchange.termsOfServiceAcceptedEtag,
+ currentEtag,
+ tos,
+ };
+ }
+
+ /**
+ * Get detailed balance information, sliced by exchange and by currency.
+ */
+ async getBalances(): Promise<BalancesResponse> {
+ return this.ws.memoGetBalance.memo(() => getBalances(this.ws));
+ }
+
+ async refresh(oldCoinPub: string): Promise<void> {
+ try {
+ const refreshGroupId = await this.db.runWithWriteTransaction(
+ [Stores.refreshGroups],
+ async (tx) => {
+ return await createRefreshGroup(
+ this.ws,
+ tx,
+ [{ coinPub: oldCoinPub }],
+ RefreshReason.Manual,
+ );
+ },
+ );
+ await processRefreshGroup(this.ws, refreshGroupId.refreshGroupId);
+ } catch (e) {
+ this.latch.trigger();
+ }
+ }
+
+ async findExchange(
+ exchangeBaseUrl: string,
+ ): Promise<ExchangeRecord | undefined> {
+ return await this.db.get(Stores.exchanges, exchangeBaseUrl);
+ }
+
+ async getPendingOperations({ onlyDue = false } = {}): Promise<
+ PendingOperationsResponse
+ > {
+ return this.ws.memoGetPending.memo(() =>
+ getPendingOperations(this.ws, { onlyDue }),
+ );
+ }
+
+ async acceptExchangeTermsOfService(
+ exchangeBaseUrl: string,
+ etag: string | undefined,
+ ): Promise<void> {
+ return acceptExchangeTermsOfService(this.ws, exchangeBaseUrl, etag);
+ }
+
+ async getDenoms(exchangeUrl: string): Promise<DenominationRecord[]> {
+ const denoms = await this.db
+ .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchangeUrl)
+ .toArray();
+ return denoms;
+ }
+
+ /**
+ * Get all exchanges known to the exchange.
+ *
+ * @deprecated Use getExchanges instead
+ */
+ async getExchangeRecords(): Promise<ExchangeRecord[]> {
+ return await this.db.iter(Stores.exchanges).toArray();
+ }
+
+ async getExchanges(): Promise<ExchangesListRespose> {
+ const exchanges: (ExchangeListItem | undefined)[] = await this.db
+ .iter(Stores.exchanges)
+ .map((x) => {
+ const details = x.details;
+ if (!details) {
+ return undefined;
+ }
+ if (!x.addComplete) {
+ return undefined;
+ }
+ if (!x.wireInfo) {
+ return undefined;
+ }
+ return {
+ exchangeBaseUrl: x.baseUrl,
+ currency: details.currency,
+ paytoUris: x.wireInfo.accounts.map((x) => x.payto_uri),
+ };
+ });
+ return {
+ exchanges: exchanges.filter((x) => !!x) as ExchangeListItem[],
+ };
+ }
+
+ async getCurrencies(): Promise<CurrencyRecord[]> {
+ return await this.db.iter(Stores.currencies).toArray();
+ }
+
+ async updateCurrency(currencyRecord: CurrencyRecord): Promise<void> {
+ logger.trace("updating currency to", currencyRecord);
+ await this.db.put(Stores.currencies, currencyRecord);
+ }
+
+ async getReserves(exchangeBaseUrl?: string): Promise<ReserveRecord[]> {
+ if (exchangeBaseUrl) {
+ return await this.db
+ .iter(Stores.reserves)
+ .filter((r) => r.exchangeBaseUrl === exchangeBaseUrl);
+ } else {
+ return await this.db.iter(Stores.reserves).toArray();
+ }
+ }
+
+ async getCoinsForExchange(exchangeBaseUrl: string): Promise<CoinRecord[]> {
+ return await this.db
+ .iter(Stores.coins)
+ .filter((c) => c.exchangeBaseUrl === exchangeBaseUrl);
+ }
+
+ async getCoins(): Promise<CoinRecord[]> {
+ return await this.db.iter(Stores.coins).toArray();
+ }
+
+ /**
+ * Stop ongoing processing.
+ */
+ stop(): void {
+ this.stopped = true;
+ this.timerGroup.stopCurrentAndFutureTimers();
+ this.ws.cryptoApi.stop();
+ }
+
+ async getSenderWireInfos(): Promise<SenderWireInfos> {
+ const m: { [url: string]: Set<string> } = {};
+
+ await this.db.iter(Stores.exchanges).forEach((x) => {
+ const wi = x.wireInfo;
+ if (!wi) {
+ return;
+ }
+ const s = (m[x.baseUrl] = m[x.baseUrl] || new Set());
+ Object.keys(wi.feesForType).map((k) => s.add(k));
+ });
+
+ const exchangeWireTypes: { [url: string]: string[] } = {};
+ Object.keys(m).map((e) => {
+ exchangeWireTypes[e] = Array.from(m[e]);
+ });
+
+ const senderWiresSet: Set<string> = new Set();
+ await this.db.iter(Stores.senderWires).forEach((x) => {
+ senderWiresSet.add(x.paytoUri);
+ });
+
+ const senderWires: string[] = Array.from(senderWiresSet);
+
+ return {
+ exchangeWireTypes,
+ senderWires,
+ };
+ }
+
+ /**
+ * Trigger paying coins back into the user's account.
+ */
+ async returnCoins(req: ReturnCoinsRequest): Promise<void> {
+ throw Error("not implemented");
+ }
+
+ /**
+ * Accept a refund, return the contract hash for the contract
+ * that was involved in the refund.
+ */
+ async applyRefund(
+ talerRefundUri: string,
+ ): Promise<{ contractTermsHash: string; proposalId: string }> {
+ return applyRefund(this.ws, talerRefundUri);
+ }
+
+ async getPurchase(
+ contractTermsHash: string,
+ ): Promise<PurchaseRecord | undefined> {
+ return this.db.get(Stores.purchases, contractTermsHash);
+ }
+
+ async acceptTip(talerTipUri: string): Promise<void> {
+ try {
+ return acceptTip(this.ws, talerTipUri);
+ } catch (e) {
+ this.latch.trigger();
+ }
+ }
+
+ async getTipStatus(talerTipUri: string): Promise<TipStatus> {
+ return getTipStatus(this.ws, talerTipUri);
+ }
+
+ async abortFailedPayment(contractTermsHash: string): Promise<void> {
+ throw Error("not implemented");
+ }
+
+ /**
+ * Inform the wallet that the status of a reserve has changed (e.g. due to a
+ * confirmation from the bank.).
+ */
+ public async handleNotifyReserve(): Promise<void> {
+ const reserves = await this.db.iter(Stores.reserves).toArray();
+ for (const r of reserves) {
+ if (r.reserveStatus === ReserveRecordStatus.WAIT_CONFIRM_BANK) {
+ try {
+ this.processReserve(r.reservePub);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Remove unreferenced / expired data from the wallet's database
+ * based on the current system time.
+ */
+ async collectGarbage(): Promise<void> {
+ // FIXME(#5845)
+ // We currently do not garbage-collect the wallet database. This might change
+ // after the feature has been properly re-designed, and we have come up with a
+ // strategy to test it.
+ }
+
+ async acceptWithdrawal(
+ talerWithdrawUri: string,
+ selectedExchange: string,
+ ): Promise<AcceptWithdrawalResponse> {
+ try {
+ return createTalerWithdrawReserve(
+ this.ws,
+ talerWithdrawUri,
+ selectedExchange,
+ );
+ } finally {
+ this.latch.trigger();
+ }
+ }
+
+ async updateReserve(reservePub: string): Promise<ReserveRecord | undefined> {
+ await forceQueryReserve(this.ws, reservePub);
+ return await this.ws.db.get(Stores.reserves, reservePub);
+ }
+
+ async getReserve(reservePub: string): Promise<ReserveRecord | undefined> {
+ return await this.ws.db.get(Stores.reserves, reservePub);
+ }
+
+ async refuseProposal(proposalId: string): Promise<void> {
+ return refuseProposal(this.ws, proposalId);
+ }
+
+ async getPurchaseDetails(proposalId: string): Promise<PurchaseDetails> {
+ const purchase = await this.db.get(Stores.purchases, proposalId);
+ if (!purchase) {
+ throw Error("unknown purchase");
+ }
+ const refundsDoneAmounts = Object.values(purchase.refunds)
+ .filter((x) => x.type === RefundState.Applied)
+ .map((x) => x.refundAmount);
+
+ const refundsPendingAmounts = Object.values(purchase.refunds)
+ .filter((x) => x.type === RefundState.Pending)
+ .map((x) => x.refundAmount);
+ const totalRefundAmount = Amounts.sum([
+ ...refundsDoneAmounts,
+ ...refundsPendingAmounts,
+ ]).amount;
+ const refundsDoneFees = Object.values(purchase.refunds)
+ .filter((x) => x.type === RefundState.Applied)
+ .map((x) => x.refundFee);
+ const refundsPendingFees = Object.values(purchase.refunds)
+ .filter((x) => x.type === RefundState.Pending)
+ .map((x) => x.refundFee);
+ const totalRefundFees = Amounts.sum([
+ ...refundsDoneFees,
+ ...refundsPendingFees,
+ ]).amount;
+ const totalFees = totalRefundFees;
+ return {
+ contractTerms: JSON.parse(purchase.contractTermsRaw),
+ hasRefund: purchase.timestampLastRefundStatus !== undefined,
+ totalRefundAmount: totalRefundAmount,
+ totalRefundAndRefreshFees: totalFees,
+ };
+ }
+
+ benchmarkCrypto(repetitions: number): Promise<BenchmarkResult> {
+ return this.ws.cryptoApi.benchmark(repetitions);
+ }
+
+ async setCoinSuspended(coinPub: string, suspended: boolean): Promise<void> {
+ await this.db.runWithWriteTransaction([Stores.coins], async (tx) => {
+ const c = await tx.get(Stores.coins, coinPub);
+ if (!c) {
+ logger.warn(`coin ${coinPub} not found, won't suspend`);
+ return;
+ }
+ c.suspended = suspended;
+ await tx.put(Stores.coins, c);
+ });
+ }
+
+ /**
+ * Dump the public information of coins we have in an easy-to-process format.
+ */
+ async dumpCoins(): Promise<CoinDumpJson> {
+ const coins = await this.db.iter(Stores.coins).toArray();
+ const coinsJson: CoinDumpJson = { coins: [] };
+ for (const c of coins) {
+ const denom = await this.db.get(Stores.denominations, [
+ c.exchangeBaseUrl,
+ c.denomPub,
+ ]);
+ if (!denom) {
+ console.error("no denom session found for coin");
+ continue;
+ }
+ const cs = c.coinSource;
+ let refreshParentCoinPub: string | undefined;
+ if (cs.type == CoinSourceType.Refresh) {
+ refreshParentCoinPub = cs.oldCoinPub;
+ }
+ let withdrawalReservePub: string | undefined;
+ if (cs.type == CoinSourceType.Withdraw) {
+ const ws = await this.db.get(
+ Stores.withdrawalGroups,
+ cs.withdrawalGroupId,
+ );
+ if (!ws) {
+ console.error("no withdrawal session found for coin");
+ continue;
+ }
+ if (ws.source.type == "reserve") {
+ withdrawalReservePub = ws.source.reservePub;
+ }
+ }
+ coinsJson.coins.push({
+ coin_pub: c.coinPub,
+ denom_pub: c.denomPub,
+ denom_pub_hash: c.denomPubHash,
+ denom_value: Amounts.stringify(denom.value),
+ exchange_base_url: c.exchangeBaseUrl,
+ refresh_parent_coin_pub: refreshParentCoinPub,
+ remaining_value: Amounts.stringify(c.currentAmount),
+ withdrawal_reserve_pub: withdrawalReservePub,
+ coin_suspended: c.suspended,
+ });
+ }
+ return coinsJson;
+ }
+
+ async getTransactions(
+ request: TransactionsRequest,
+ ): Promise<TransactionsResponse> {
+ return getTransactions(this.ws, request);
+ }
+
+ async withdrawTestBalance(
+ amount = "TESTKUDOS:10",
+ bankBaseUrl = "https://bank.test.taler.net/",
+ exchangeBaseUrl = "https://exchange.test.taler.net/",
+ ): Promise<void> {
+ await withdrawTestBalance(this.ws, amount, bankBaseUrl, exchangeBaseUrl);
+ }
+}