wallet-core/packages/taler-wallet-core/src/wallet.ts

1244 lines
36 KiB
TypeScript
Raw Normal View History

2015-12-25 22:42:14 +01:00
/*
This file is part of GNU Taler
2019-11-30 00:36:20 +01:00
(C) 2015-2019 GNUnet e.V.
2015-12-25 22:42:14 +01:00
GNU Taler is free software; you can redistribute it and/or modify it under the
2015-12-25 22:42:14 +01:00
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
2015-12-25 22:42:14 +01:00
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/>
2015-12-25 22:42:14 +01:00
*/
2016-01-05 15:42:46 +01:00
/**
* High-level wallet operations that should be indepentent from the underlying
* browser extension interface.
*/
2017-05-24 16:52:00 +02:00
/**
* Imports.
*/
2021-05-12 16:18:32 +02:00
import {
BackupRecovery,
codecForAny,
TalerErrorCode,
WalletCurrencyInfo,
2021-05-12 16:18:32 +02:00
} from "@gnu-taler/taler-util";
2019-12-09 13:29:11 +01:00
import { CryptoWorkerFactory } from "./crypto/workers/cryptoApi";
import {
2021-02-08 15:38:34 +01:00
addBackupProvider,
AddBackupProviderRequest,
BackupInfo,
codecForAddBackupProviderRequest,
exportBackupEncrypted,
getBackupInfo,
getBackupRecovery,
importBackupEncrypted,
importBackupPlain,
loadBackupRecovery,
runBackupCycle,
} from "./operations/backup";
2021-03-10 12:00:30 +01:00
import { exportBackup } from "./operations/backup/export";
2021-02-08 15:38:34 +01:00
import { getBalances } from "./operations/balance";
import {
createDepositGroup,
processDepositGroup,
trackDepositGroup,
} from "./operations/deposits";
import {
makeErrorDetails,
OperationFailedAndReportedError,
OperationFailedError,
} from "./operations/errors";
import {
acceptExchangeTermsOfService,
getExchangePaytoUri,
updateExchangeFromUrl,
} from "./operations/exchanges";
import {
confirmPay,
2021-02-08 15:38:34 +01:00
preparePayForUri,
2019-12-03 00:52:15 +01:00
processDownloadProposal,
processPurchasePay,
2021-02-08 15:38:34 +01:00
refuseProposal,
} from "./operations/pay";
2021-02-08 15:38:34 +01:00
import { getPendingOperations } from "./operations/pending";
import { processRecoupGroup } from "./operations/recoup";
import {
autoRefresh,
createRefreshGroup,
processRefreshGroup,
} from "./operations/refresh";
import {
abortFailedPayWithRefund,
applyRefund,
processPurchaseQueryRefund,
} from "./operations/refund";
import {
createReserve,
createTalerWithdrawReserve,
forceQueryReserve,
getFundingPaytoUris,
processReserve,
} from "./operations/reserves";
import { InternalWalletState } from "./operations/state";
import {
runIntegrationTest,
testPay,
withdrawTestBalance,
} from "./operations/testing";
import { acceptTip, prepareTip, processTip } from "./operations/tip";
import { getTransactions } from "./operations/transactions";
import {
getExchangeWithdrawalInfo,
getWithdrawalDetailsForUri,
processWithdrawGroup,
} from "./operations/withdraw";
2016-05-24 01:53:56 +02:00
import {
AuditorTrustRecord,
2017-05-28 01:10:54 +02:00
CoinRecord,
2021-02-08 15:38:34 +01:00
CoinSourceType,
2017-05-28 01:10:54 +02:00
DenominationRecord,
2016-11-15 15:07:17 +01:00
ExchangeRecord,
PurchaseRecord,
2021-02-08 15:38:34 +01:00
RefundState,
ReserveRecord,
2019-11-21 23:09:43 +01:00
ReserveRecordStatus,
2021-02-08 15:38:34 +01:00
Stores,
2021-03-17 17:56:37 +01:00
} from "./db.js";
import { NotificationType, WalletNotification } from "@gnu-taler/taler-util";
2021-02-08 15:38:34 +01:00
import {
PendingOperationInfo,
PendingOperationsResponse,
PendingOperationType,
2021-03-17 17:56:37 +01:00
} from "./pending-types.js";
import { CoinDumpJson } from "@gnu-taler/taler-util";
2021-02-08 15:38:34 +01:00
import {
codecForTransactionsRequest,
TransactionsRequest,
TransactionsResponse,
2021-03-17 17:56:37 +01:00
} from "@gnu-taler/taler-util";
import {
2020-07-16 19:22:56 +02:00
AcceptManualWithdrawalResult,
2021-02-08 15:38:34 +01:00
AcceptWithdrawalResponse,
ApplyRefundResponse,
BalancesResponse,
2021-02-08 15:38:34 +01:00
BenchmarkResult,
codecForAbortPayWithRefundRequest,
codecForAcceptBankIntegratedWithdrawalRequest,
codecForAcceptExchangeTosRequest,
2021-02-08 15:38:34 +01:00
codecForAcceptManualWithdrawalRequet,
codecForAcceptTipRequest,
codecForAddExchangeRequest,
codecForApplyRefundRequest,
codecForConfirmPayRequest,
2021-02-08 15:38:34 +01:00
codecForCreateDepositGroupRequest,
codecForForceExchangeUpdateRequest,
codecForForceRefreshRequest,
2021-02-08 15:38:34 +01:00
codecForGetExchangeTosRequest,
codecForGetWithdrawalDetailsForAmountRequest,
codecForGetWithdrawalDetailsForUri,
codecForIntegrationTestArgs,
codecForPreparePayRequest,
2020-09-08 14:10:47 +02:00
codecForPrepareTipRequest,
2021-02-08 15:38:34 +01:00
codecForSetCoinSuspendedRequest,
codecForTestPayArgs,
codecForTrackDepositGroupRequest,
codecForWithdrawTestBalance,
ConfirmPayResult,
CoreApiResponse,
2021-01-18 23:35:41 +01:00
CreateDepositGroupRequest,
CreateDepositGroupResponse,
2021-02-08 15:38:34 +01:00
ExchangeListItem,
ExchangesListRespose,
GetExchangeTosResult,
IntegrationTestArgs,
ManualWithdrawalDetails,
PreparePayResult,
PrepareTipResult,
PurchaseDetails,
RecoveryLoadRequest,
RefreshReason,
ReturnCoinsRequest,
TestPayArgs,
2021-01-18 23:35:41 +01:00
TrackDepositGroupRequest,
TrackDepositGroupResponse,
2021-02-08 15:38:34 +01:00
WithdrawTestBalanceRequest,
WithdrawUriInfoResponse,
2021-03-17 17:56:37 +01:00
} from "@gnu-taler/taler-util";
import { AmountJson, Amounts } from "@gnu-taler/taler-util";
import { assertUnreachable } from "./util/assertUnreachable";
2019-12-05 19:38:19 +01:00
import { AsyncOpMemoSingle } from "./util/asyncMemo";
2021-02-08 15:38:34 +01:00
import { HttpRequestLibrary } from "./util/http";
import { Logger } from "./util/logging";
import { AsyncCondition } from "./util/promiseUtils";
import { Database } from "./util/query";
2021-03-17 17:56:37 +01:00
import { Duration, durationMin } from "@gnu-taler/taler-util";
2021-02-08 15:38:34 +01:00
import { TimerGroup } from "./util/timer";
import { getExchangeTrust } from "./operations/currencies.js";
const builtinAuditors: AuditorTrustRecord[] = [
{
currency: "KUDOS",
auditorPub: "BW9DC48PHQY4NH011SHHX36DZZ3Q22Y6X7FZ1VD1CMZ2PTFZ6PN0",
auditorBaseUrl: "https://auditor.demo.taler.net/",
uids: ["5P25XF8TVQP9AW6VYGY2KV47WT5Y3ZXFSJAA570GJPX5SVJXKBVG"],
},
];
2019-11-21 23:09:43 +01:00
const logger = new Logger("wallet.ts");
/**
* The platform-independent wallet implementation.
*/
export class Wallet {
private ws: InternalWalletState;
2019-12-02 17:35:47 +01:00
private timerGroup: TimerGroup = new TimerGroup();
private latch = new AsyncCondition();
2020-04-06 17:45:41 +02:00
private stopped = false;
2019-12-05 19:38:19 +01:00
private memoRunRetryLoop = new AsyncOpMemoSingle<void>();
get db(): Database<typeof Stores> {
return this.ws.db;
}
2019-06-26 15:30:32 +02:00
constructor(
db: Database<typeof Stores>,
2019-06-26 15:30:32 +02:00
http: HttpRequestLibrary,
2019-08-15 19:10:23 +02:00
cryptoWorkerFactory: CryptoWorkerFactory,
2019-06-26 15:30:32 +02:00
) {
2019-12-05 19:38:19 +01:00
this.ws = new InternalWalletState(db, http, cryptoWorkerFactory);
}
2020-04-07 10:07:32 +02:00
getExchangePaytoUri(
exchangeBaseUrl: string,
supportedTargetTypes: string[],
): Promise<string> {
return getExchangePaytoUri(this.ws, exchangeBaseUrl, supportedTargetTypes);
}
2020-07-11 09:56:07 +02:00
async getWithdrawalDetailsForAmount(
2019-12-09 19:59:08 +01:00
exchangeBaseUrl: string,
amount: AmountJson,
2020-07-11 09:56:07 +02:00
): Promise<ManualWithdrawalDetails> {
2020-07-16 19:22:56 +02:00
const wi = await getExchangeWithdrawalInfo(
this.ws,
exchangeBaseUrl,
amount,
);
const paytoUris = wi.exchangeInfo.wireInfo?.accounts.map(
(x) => x.payto_uri,
);
2020-07-11 09:56:07 +02:00
if (!paytoUris) {
throw Error("exchange is in invalid state");
}
return {
2020-07-28 20:08:50 +02:00
amountRaw: Amounts.stringify(amount),
amountEffective: Amounts.stringify(wi.selectedDenoms.totalCoinValue),
2020-07-11 09:56:07 +02:00
paytoUris,
tosAccepted: wi.termsOfServiceAccepted,
};
}
2017-06-05 02:00:03 +02:00
2019-12-05 19:38:19 +01:00
addNotificationListener(f: (n: WalletNotification) => void): void {
this.ws.addNotificationListener(f);
}
2019-11-21 23:09:43 +01:00
/**
2019-11-30 00:36:20 +01:00
* Execute one operation based on the pending operation info record.
2019-11-21 23:09:43 +01:00
*/
2019-11-30 00:36:20 +01:00
async processOnePendingOperation(
pending: PendingOperationInfo,
2020-04-06 17:45:41 +02:00
forceNow = false,
2019-11-30 00:36:20 +01:00
): Promise<void> {
logger.trace(`running pending ${JSON.stringify(pending, undefined, 2)}`);
2019-11-30 00:36:20 +01:00
switch (pending.type) {
case PendingOperationType.Bug:
2019-12-05 19:38:19 +01:00
// Nothing to do, will just be displayed to the user
2019-11-30 00:36:20 +01:00
return;
case PendingOperationType.ExchangeUpdate:
await updateExchangeFromUrl(this.ws, pending.exchangeBaseUrl, forceNow);
2019-11-30 00:36:20 +01:00
break;
case PendingOperationType.Refresh:
2019-12-16 12:53:22 +01:00
await processRefreshGroup(this.ws, pending.refreshGroupId, forceNow);
2019-11-30 00:36:20 +01:00
break;
case PendingOperationType.Reserve:
2019-12-05 19:38:19 +01:00
await processReserve(this.ws, pending.reservePub, forceNow);
2019-11-30 00:36:20 +01:00
break;
case PendingOperationType.Withdraw:
await processWithdrawGroup(
2019-12-09 19:59:08 +01:00
this.ws,
pending.withdrawalGroupId,
2019-12-09 19:59:08 +01:00
forceNow,
);
2019-11-30 00:36:20 +01:00
break;
case PendingOperationType.ProposalChoice:
2019-11-30 00:36:20 +01:00
// Nothing to do, user needs to accept/reject
break;
2019-12-16 12:53:22 +01:00
case PendingOperationType.ProposalDownload:
await processDownloadProposal(this.ws, pending.proposalId, forceNow);
2019-12-03 00:52:15 +01:00
break;
2019-12-16 12:53:22 +01:00
case PendingOperationType.TipChoice:
// Nothing to do, user needs to accept/reject
break;
case PendingOperationType.TipPickup:
await processTip(this.ws, pending.tipId, forceNow);
2019-12-02 17:35:47 +01:00
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;
2020-09-03 13:59:09 +02:00
case PendingOperationType.ExchangeCheckRefresh:
await autoRefresh(this.ws, pending.exchangeBaseUrl);
2020-09-03 13:59:09 +02:00
break;
2021-01-18 23:35:41 +01:00
case PendingOperationType.Deposit:
await processDepositGroup(this.ws, pending.depositGroupId);
break;
2019-11-30 00:36:20 +01:00
default:
assertUnreachable(pending);
2019-11-21 23:09:43 +01:00
}
2019-11-30 00:36:20 +01:00
}
2019-11-21 23:09:43 +01:00
2019-11-30 00:36:20 +01:00
/**
* Process pending operations.
*/
2020-04-06 17:45:41 +02:00
public async runPending(forceNow = false): Promise<void> {
2019-12-05 19:38:19 +01:00
const onlyDue = !forceNow;
const pendingOpsResponse = await this.getPendingOperations({ onlyDue });
2019-11-30 00:36:20 +01:00
for (const p of pendingOpsResponse.pendingOperations) {
try {
2019-12-03 14:40:05 +01:00
await this.processOnePendingOperation(p, forceNow);
2019-11-30 00:36:20 +01:00
} catch (e) {
if (e instanceof OperationFailedAndReportedError) {
2020-03-24 10:55:04 +01:00
console.error(
"Operation failed:",
JSON.stringify(e.operationError, undefined, 2),
);
} else {
console.error(e);
}
2019-11-30 00:36:20 +01:00
}
2019-11-21 23:09:43 +01:00
}
}
/**
2019-12-05 19:38:19 +01:00
* 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.
*/
2020-09-01 14:30:46 +02:00
public async runUntilDone(
req: {
maxRetries?: number;
} = {},
): Promise<void> {
2020-05-11 14:49:43 +02:00
let done = false;
const p = new Promise<void>((resolve, reject) => {
2020-09-01 14:30:46 +02:00
// Monitor for conditions that means we're done or we
// should quit with an error (due to exceeded retries).
2020-03-24 10:55:04 +01:00
this.addNotificationListener((n) => {
2020-05-11 14:49:43 +02:00
if (done) {
return;
}
if (
n.type === NotificationType.WaitingForRetry &&
n.numGivingLiveness == 0
) {
2020-05-11 14:49:43 +02:00
done = true;
logger.trace("no liveness-giving operations left");
resolve();
}
2020-09-01 14:30:46 +02:00
const maxRetries = req.maxRetries;
if (!maxRetries) {
return;
}
this.getPendingOperations({ onlyDue: false })
.then((pending) => {
for (const p of pending.pendingOperations) {
if (p.retryInfo && p.retryInfo.retryCounter > maxRetries) {
console.warn(
`stopping, as ${maxRetries} retries are exceeded in an operation of type ${p.type}`,
);
this.stop();
done = true;
resolve();
}
}
})
.catch((e) => {
logger.error(e);
reject(e);
});
});
2020-09-01 14:30:46 +02:00
// Run this asynchronously
2020-03-24 10:55:04 +01:00
this.runRetryLoop().catch((e) => {
logger.error("exception in wallet retry loop");
reject(e);
});
});
await p;
}
2019-11-21 23:09:43 +01:00
/**
2019-12-05 19:38:19 +01:00
* Process pending operations and wait for scheduled operations in
* a loop until the wallet is stopped explicitly.
2019-11-21 23:09:43 +01:00
*/
2019-12-05 19:38:19 +01:00
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;
2019-11-30 00:36:20 +01:00
}
2019-12-05 19:38:19 +01:00
});
}
private async runRetryLoopImpl(): Promise<void> {
let iteration = 0;
for (; !this.stopped; iteration++) {
2020-04-06 17:45:41 +02:00
const pending = await this.getPendingOperations({ onlyDue: true });
2020-09-03 17:08:26 +02:00
let numDueAndLive = 0;
for (const p of pending.pendingOperations) {
if (p.givesLifeness) {
numDueAndLive++;
}
}
// Make sure that we run tasks that don't give lifeness at least
// one time.
if (iteration !== 0 && numDueAndLive === 0) {
const allPending = await this.getPendingOperations({ onlyDue: false });
2019-12-05 19:38:19 +01:00
let numPending = 0;
let numGivingLiveness = 0;
for (const p of allPending.pendingOperations) {
numPending++;
if (p.givesLifeness) {
numGivingLiveness++;
}
}
let dt: Duration;
2019-12-05 19:38:19 +01:00
if (
allPending.pendingOperations.length === 0 ||
allPending.nextRetryDelay.d_ms === Number.MAX_SAFE_INTEGER
) {
// Wait for 5 seconds
dt = { d_ms: 5000 };
2019-12-05 19:38:19 +01:00
} else {
dt = durationMin({ d_ms: 5000 }, allPending.nextRetryDelay);
2019-12-05 19:38:19 +01:00
}
const timeout = this.timerGroup.resolveAfter(dt);
2019-12-05 19:38:19 +01:00
this.ws.notify({
type: NotificationType.WaitingForRetry,
numGivingLiveness,
numPending,
});
await Promise.race([timeout, this.latch.wait()]);
} else {
// FIXME: maybe be a bit smarter about executing these
// operations in parallel?
logger.trace(
`running ${pending.pendingOperations.length} pending operations`,
);
2019-12-05 19:38:19 +01:00
for (const p of pending.pendingOperations) {
try {
await this.processOnePendingOperation(p);
} catch (e) {
2020-07-20 12:50:32 +02:00
if (e instanceof OperationFailedAndReportedError) {
logger.warn("operation processed resulted in reported error");
} else {
2020-09-01 14:30:46 +02:00
logger.error("Uncaught exception", e);
2020-07-20 12:50:32 +02:00
this.ws.notify({
type: NotificationType.InternalError,
message: "uncaught exception",
exception: e,
});
2020-07-20 12:50:32 +02:00
}
2019-12-05 19:38:19 +01:00
}
2020-07-20 12:50:32 +02:00
this.ws.notify({
type: NotificationType.PendingOperationProcessed,
});
2019-12-05 19:38:19 +01:00
}
2019-11-21 23:09:43 +01:00
}
}
2019-12-05 19:38:19 +01:00
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.
*/
2020-04-06 20:02:01 +02:00
async fillDefaults(): Promise<void> {
2019-12-12 22:39:45 +01:00
await this.db.runWithWriteTransaction(
[Stores.config, Stores.auditorTrustStore],
2020-03-24 10:55:04 +01:00
async (tx) => {
let applied = false;
2020-03-24 10:55:04 +01:00
await tx.iter(Stores.config).forEach((x) => {
if (x.key == "currencyDefaultsApplied" && x.value == true) {
applied = true;
}
});
if (!applied) {
for (const c of builtinAuditors) {
await tx.put(Stores.auditorTrustStore, c);
}
}
2019-11-20 20:02:48 +01:00
},
);
}
2019-09-06 09:48:00 +02:00
/**
* Check if a payment for the given taler://pay/ URI is possible.
*
2019-09-06 09:48:00 +02:00
* If the payment is possible, the signature are already generated but not
* yet send to the merchant.
*/
2019-12-20 01:25:22 +01:00
async preparePayForUri(talerPayUri: string): Promise<PreparePayResult> {
return preparePayForUri(this.ws, talerPayUri);
}
2016-02-10 02:03:31 +01:00
/**
* Add a contract to the wallet and sign coins, and send them.
2016-02-10 02:03:31 +01:00
*/
2019-06-26 15:30:32 +02:00
async confirmPay(
2019-11-30 00:36:20 +01:00
proposalId: string,
sessionIdOverride: string | undefined,
2019-06-26 15:30:32 +02:00
): Promise<ConfirmPayResult> {
2019-12-02 17:35:47 +01:00
try {
return await confirmPay(this.ws, proposalId, sessionIdOverride);
} finally {
this.latch.trigger();
}
}
2019-11-30 00:36:20 +01:00
2016-02-09 21:56:06 +01:00
/**
2021-04-27 23:42:25 +02:00
* First fetch information required to withdraw from the reserve,
2016-02-09 21:56:06 +01:00
* then deplete the reserve, withdrawing coins until it is empty.
2019-11-21 23:09:43 +01:00
*
* The returned promise resolves once the reserve is set to the
* state DORMANT.
2016-02-09 21:56:06 +01:00
*/
async processReserve(reservePub: string): Promise<void> {
2019-12-02 17:35:47 +01:00
try {
return await processReserve(this.ws, reservePub);
} finally {
this.latch.trigger();
}
}
2016-02-09 21:56:06 +01:00
/**
* 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.
2016-02-09 21:56:06 +01:00
*/
2020-07-16 19:22:56 +02:00
async acceptManualWithdrawal(
exchangeBaseUrl: string,
amount: AmountJson,
): Promise<AcceptManualWithdrawalResult> {
2019-12-02 17:35:47 +01:00
try {
2020-07-16 19:22:56 +02:00
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,
};
2019-12-02 17:35:47 +01:00
} finally {
this.latch.trigger();
}
}
2016-09-28 18:54:48 +02:00
/**
* 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);
2019-11-30 00:36:20 +01:00
}
async getWithdrawalDetailsForUri(
talerWithdrawUri: string,
): Promise<WithdrawUriInfoResponse> {
return getWithdrawalDetailsForUri(this.ws, talerWithdrawUri);
}
2016-02-11 18:17:02 +01:00
/**
* 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.
2016-02-11 18:17:02 +01:00
*/
async updateExchangeFromUrl(
baseUrl: string,
2020-04-06 17:45:41 +02:00
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,
2020-07-16 19:22:56 +02:00
};
}
/**
* Get detailed balance information, sliced by exchange and by currency.
*/
async getBalances(): Promise<BalancesResponse> {
2019-12-05 19:38:19 +01:00
return this.ws.memoGetBalance.memo(() => getBalances(this.ws));
}
async refresh(oldCoinPub: string): Promise<void> {
try {
2019-12-16 12:53:22 +01:00
const refreshGroupId = await this.db.runWithWriteTransaction(
[Stores.refreshGroups, Stores.denominations, Stores.coins],
2020-03-24 10:55:04 +01:00
async (tx) => {
2019-12-16 12:53:22 +01:00
return await createRefreshGroup(
2020-07-23 14:05:17 +02:00
this.ws,
2019-12-16 12:53:22 +01:00
tx,
[{ coinPub: oldCoinPub }],
RefreshReason.Manual,
);
},
);
await processRefreshGroup(this.ws, refreshGroupId.refreshGroupId);
} catch (e) {
this.latch.trigger();
}
}
2019-11-20 20:02:48 +01:00
async findExchange(
exchangeBaseUrl: string,
): Promise<ExchangeRecord | undefined> {
2019-12-12 22:39:45 +01:00
return await this.db.get(Stores.exchanges, exchangeBaseUrl);
}
2021-02-08 15:38:34 +01:00
async getPendingOperations({
onlyDue = false,
} = {}): Promise<PendingOperationsResponse> {
2019-12-05 19:38:19 +01:00
return this.ws.memoGetPending.memo(() =>
getPendingOperations(this.ws, { onlyDue }),
2019-12-05 19:38:19 +01:00
);
2019-11-19 16:16:12 +01:00
}
2019-12-09 19:59:08 +01:00
async acceptExchangeTermsOfService(
exchangeBaseUrl: string,
etag: string | undefined,
2020-04-06 20:02:01 +02:00
): Promise<void> {
2019-12-09 19:59:08 +01:00
return acceptExchangeTermsOfService(this.ws, exchangeBaseUrl, etag);
}
async getDenoms(exchangeUrl: string): Promise<DenominationRecord[]> {
2019-12-16 12:53:22 +01:00
const denoms = await this.db
.iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchangeUrl)
.toArray();
return denoms;
}
2016-11-13 10:17:39 +01:00
/**
* Get all exchanges known to the exchange.
*
* @deprecated Use getExchanges instead
*/
async getExchangeRecords(): Promise<ExchangeRecord[]> {
2019-12-12 22:39:45 +01:00
return await this.db.iter(Stores.exchanges).toArray();
}
2016-02-23 14:07:53 +01:00
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,
2020-07-16 19:22:56 +02:00
paytoUris: x.wireInfo.accounts.map((x) => x.payto_uri),
};
});
return {
exchanges: exchanges.filter((x) => !!x) as ExchangeListItem[],
};
}
async getCurrencies(): Promise<WalletCurrencyInfo> {
const trustedAuditors = await this.db
.iter(Stores.auditorTrustStore)
.toArray();
const trustedExchanges = await this.db
.iter(Stores.exchangeTrustStore)
.toArray();
return {
trustedAuditors: trustedAuditors.map((x) => ({
currency: x.currency,
auditorBaseUrl: x.auditorBaseUrl,
auditorPub: x.auditorPub,
})),
trustedExchanges: trustedExchanges.map((x) => ({
currency: x.currency,
exchangeBaseUrl: x.exchangeBaseUrl,
exchangeMasterPub: x.exchangeMasterPub,
})),
};
2017-03-24 17:54:22 +01:00
}
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();
}
2016-10-12 02:55:53 +02:00
}
2019-11-21 23:09:43 +01:00
async getCoinsForExchange(exchangeBaseUrl: string): Promise<CoinRecord[]> {
2019-12-16 12:53:22 +01:00
return await this.db
.iter(Stores.coins)
2020-03-24 10:55:04 +01:00
.filter((c) => c.exchangeBaseUrl === exchangeBaseUrl);
2016-10-12 02:55:53 +02:00
}
2019-11-21 23:09:43 +01:00
async getCoins(): Promise<CoinRecord[]> {
2019-12-12 22:39:45 +01:00
return await this.db.iter(Stores.coins).toArray();
2019-11-21 23:09:43 +01:00
}
2017-06-05 02:00:03 +02:00
/**
* Stop ongoing processing.
*/
2020-04-06 20:02:01 +02:00
stop(): void {
2019-12-02 17:35:47 +01:00
this.stopped = true;
this.timerGroup.stopCurrentAndFutureTimers();
2019-12-05 19:38:19 +01:00
this.ws.cryptoApi.stop();
2017-06-05 02:00:03 +02:00
}
/**
* 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.
*/
2020-12-14 16:45:15 +01:00
async applyRefund(talerRefundUri: string): Promise<ApplyRefundResponse> {
return applyRefund(this.ws, talerRefundUri);
2017-08-27 03:56:19 +02:00
}
2019-06-26 15:30:32 +02:00
async getPurchase(
contractTermsHash: string,
): Promise<PurchaseRecord | undefined> {
2019-12-12 22:39:45 +01:00
return this.db.get(Stores.purchases, contractTermsHash);
2017-08-27 03:56:19 +02:00
}
2019-11-30 00:36:20 +01:00
async acceptTip(talerTipUri: string): Promise<void> {
try {
return acceptTip(this.ws, talerTipUri);
} catch (e) {
this.latch.trigger();
}
2017-11-30 04:07:36 +01:00
}
2020-09-08 14:10:47 +02:00
async prepareTip(talerTipUri: string): Promise<PrepareTipResult> {
return prepareTip(this.ws, talerTipUri);
2017-11-30 04:07:36 +01:00
}
async abortFailedPayWithRefund(proposalId: string): Promise<void> {
return abortFailedPayWithRefund(this.ws, proposalId);
}
2019-12-09 19:59:08 +01:00
/**
* Inform the wallet that the status of a reserve has changed (e.g. due to a
* confirmation from the bank.).
*/
2020-04-06 20:02:01 +02:00
public async handleNotifyReserve(): Promise<void> {
2019-12-12 22:39:45 +01:00
const reserves = await this.db.iter(Stores.reserves).toArray();
2019-11-30 00:36:20 +01:00
for (const r of reserves) {
if (r.reserveStatus === ReserveRecordStatus.WAIT_CONFIRM_BANK) {
try {
this.processReserve(r.reservePub);
2019-11-30 00:36:20 +01:00
} catch (e) {
console.error(e);
}
}
}
}
/**
* Remove unreferenced / expired data from the wallet's database
* based on the current system time.
*/
2020-04-06 20:02:01 +02:00
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(
2019-08-28 02:49:27 +02:00
talerWithdrawUri: string,
selectedExchange: string,
): Promise<AcceptWithdrawalResponse> {
try {
return createTalerWithdrawReserve(
this.ws,
talerWithdrawUri,
selectedExchange,
);
} finally {
this.latch.trigger();
}
2019-08-28 02:49:27 +02:00
}
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);
}
2019-12-20 01:25:22 +01:00
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);
2019-08-31 13:27:12 +02:00
if (!purchase) {
throw Error("unknown purchase");
}
2020-07-23 14:05:17 +02:00
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);
2019-08-31 13:27:12 +02:00
const totalRefundAmount = Amounts.sum([
...refundsDoneAmounts,
...refundsPendingAmounts,
]).amount;
2020-07-23 14:05:17 +02:00
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);
2019-08-31 13:27:12 +02:00
const totalRefundFees = Amounts.sum([
...refundsDoneFees,
...refundsPendingFees,
]).amount;
const totalFees = totalRefundFees;
return {
2021-01-04 13:30:38 +01:00
contractTerms: JSON.parse(purchase.download.contractTermsRaw),
2019-12-16 16:20:45 +01:00
hasRefund: purchase.timestampLastRefundStatus !== undefined,
2019-08-31 13:27:12 +02:00
totalRefundAmount: totalRefundAmount,
totalRefundAndRefreshFees: totalFees,
};
}
2018-09-20 02:56:13 +02:00
benchmarkCrypto(repetitions: number): Promise<BenchmarkResult> {
2019-12-05 19:38:19 +01:00
return this.ws.cryptoApi.benchmark(repetitions);
2018-09-20 02:56:13 +02:00
}
2020-03-24 10:55:04 +01:00
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,
2020-09-08 17:33:10 +02:00
c.denomPubHash,
2020-03-24 10:55:04 +01:00
]);
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,
2020-03-24 10:55:04 +01:00
);
if (!ws) {
console.error("no withdrawal session found for coin");
continue;
}
2020-09-08 15:57:08 +02:00
withdrawalReservePub = ws.reservePub;
2020-03-24 10:55:04 +01:00
}
coinsJson.coins.push({
coin_pub: c.coinPub,
denom_pub: c.denomPub,
denom_pub_hash: c.denomPubHash,
denom_value: Amounts.stringify(denom.value),
2020-03-24 10:55:04 +01:00
exchange_base_url: c.exchangeBaseUrl,
refresh_parent_coin_pub: refreshParentCoinPub,
remaining_value: Amounts.stringify(c.currentAmount),
2020-03-24 10:55:04 +01:00
withdrawal_reserve_pub: withdrawalReservePub,
coin_suspended: c.suspended,
});
}
return coinsJson;
}
2020-05-12 10:38:58 +02:00
async getTransactions(
request: TransactionsRequest,
): Promise<TransactionsResponse> {
2020-05-12 10:38:58 +02:00
return getTransactions(this.ws, request);
}
2020-09-01 14:30:46 +02:00
async withdrawTestBalance(req: WithdrawTestBalanceRequest): Promise<void> {
await withdrawTestBalance(
this.ws,
req.amount,
req.bankBaseUrl,
req.exchangeBaseUrl,
);
}
async runIntegrationtest(args: IntegrationTestArgs): Promise<void> {
return runIntegrationTest(this.ws.http, this, args);
}
async testPay(args: TestPayArgs) {
return testPay(this.ws.http, this, args);
}
2021-01-07 15:01:23 +01:00
async exportBackupPlain() {
return exportBackup(this.ws);
}
async importBackupPlain(backup: any) {
return importBackupPlain(this.ws, backup);
}
async exportBackupEncrypted() {
return exportBackupEncrypted(this.ws);
}
async importBackupEncrypted(backup: Uint8Array) {
return importBackupEncrypted(this.ws, backup);
}
async getBackupRecovery(): Promise<BackupRecovery> {
return getBackupRecovery(this.ws);
}
async loadBackupRecovery(req: RecoveryLoadRequest): Promise<void> {
return loadBackupRecovery(this.ws, req);
}
async addBackupProvider(req: AddBackupProviderRequest): Promise<void> {
return addBackupProvider(this.ws, req);
}
2021-01-18 23:35:41 +01:00
async createDepositGroup(
req: CreateDepositGroupRequest,
): Promise<CreateDepositGroupResponse> {
return createDepositGroup(this.ws, req);
}
async runBackupCycle(): Promise<void> {
return runBackupCycle(this.ws);
}
async getBackupStatus(): Promise<BackupInfo> {
return getBackupInfo(this.ws);
}
2021-01-13 00:51:30 +01:00
2021-01-18 23:35:41 +01:00
async trackDepositGroup(
req: TrackDepositGroupRequest,
): Promise<TrackDepositGroupResponse> {
return trackDepositGroup(this.ws, req);
}
/**
* Implementation of the "wallet-core" API.
*/
private async dispatchRequestInternal(
operation: string,
payload: unknown,
): Promise<Record<string, any>> {
switch (operation) {
case "withdrawTestkudos": {
await this.withdrawTestBalance({
amount: "TESTKUDOS:10",
bankBaseUrl: "https://bank.test.taler.net/",
exchangeBaseUrl: "https://exchange.test.taler.net/",
});
return {};
}
case "withdrawTestBalance": {
const req = codecForWithdrawTestBalance().decode(payload);
await this.withdrawTestBalance(req);
return {};
}
2020-08-14 12:48:48 +02:00
case "runIntegrationTest": {
const req = codecForIntegrationTestArgs().decode(payload);
await this.runIntegrationtest(req);
2020-09-01 14:30:46 +02:00
return {};
}
case "testPay": {
const req = codecForTestPayArgs().decode(payload);
await this.testPay(req);
2020-09-01 14:30:46 +02:00
return {};
}
case "getTransactions": {
const req = codecForTransactionsRequest().decode(payload);
return await this.getTransactions(req);
}
case "addExchange": {
const req = codecForAddExchangeRequest().decode(payload);
await this.updateExchangeFromUrl(req.exchangeBaseUrl);
return {};
}
case "forceUpdateExchange": {
const req = codecForForceExchangeUpdateRequest().decode(payload);
await this.updateExchangeFromUrl(req.exchangeBaseUrl, true);
return {};
}
case "listExchanges": {
return await this.getExchanges();
}
case "getWithdrawalDetailsForUri": {
const req = codecForGetWithdrawalDetailsForUri().decode(payload);
return await this.getWithdrawalDetailsForUri(req.talerWithdrawUri);
}
case "acceptManualWithdrawal": {
const req = codecForAcceptManualWithdrawalRequet().decode(payload);
const res = await this.acceptManualWithdrawal(
req.exchangeBaseUrl,
Amounts.parseOrThrow(req.amount),
);
return res;
}
case "getWithdrawalDetailsForAmount": {
const req = codecForGetWithdrawalDetailsForAmountRequest().decode(
payload,
);
return await this.getWithdrawalDetailsForAmount(
req.exchangeBaseUrl,
Amounts.parseOrThrow(req.amount),
);
}
case "getBalances": {
return await this.getBalances();
}
case "getPendingOperations": {
return await this.getPendingOperations();
}
case "setExchangeTosAccepted": {
const req = codecForAcceptExchangeTosRequest().decode(payload);
2020-09-01 14:30:46 +02:00
await this.acceptExchangeTermsOfService(req.exchangeBaseUrl, req.etag);
return {};
}
case "applyRefund": {
const req = codecForApplyRefundRequest().decode(payload);
return await this.applyRefund(req.talerRefundUri);
}
case "acceptBankIntegratedWithdrawal": {
const req = codecForAcceptBankIntegratedWithdrawalRequest().decode(
payload,
);
return await this.acceptWithdrawal(
req.talerWithdrawUri,
req.exchangeBaseUrl,
);
}
case "getExchangeTos": {
const req = codecForGetExchangeTosRequest().decode(payload);
return this.getExchangeTos(req.exchangeBaseUrl);
}
case "retryPendingNow": {
await this.runPending(true);
return {};
}
case "preparePay": {
const req = codecForPreparePayRequest().decode(payload);
return await this.preparePayForUri(req.talerPayUri);
}
case "confirmPay": {
const req = codecForConfirmPayRequest().decode(payload);
return await this.confirmPay(req.proposalId, req.sessionId);
}
case "abortFailedPayWithRefund": {
const req = codecForAbortPayWithRefundRequest().decode(payload);
await this.abortFailedPayWithRefund(req.proposalId);
return {};
}
case "dumpCoins": {
return await this.dumpCoins();
}
case "setCoinSuspended": {
const req = codecForSetCoinSuspendedRequest().decode(payload);
await this.setCoinSuspended(req.coinPub, req.suspended);
return {};
}
case "forceRefresh": {
const req = codecForForceRefreshRequest().decode(payload);
const coinPubs = req.coinPubList.map((x) => ({ coinPub: x }));
const refreshGroupId = await this.db.runWithWriteTransaction(
[Stores.refreshGroups, Stores.denominations, Stores.coins],
async (tx) => {
return await createRefreshGroup(
this.ws,
tx,
coinPubs,
RefreshReason.Manual,
);
},
);
return {
refreshGroupId,
};
}
2020-09-08 14:10:47 +02:00
case "prepareTip": {
const req = codecForPrepareTipRequest().decode(payload);
return await this.prepareTip(req.talerTipUri);
}
case "acceptTip": {
const req = codecForAcceptTipRequest().decode(payload);
await this.acceptTip(req.walletTipId);
return {};
}
2020-12-02 14:55:04 +01:00
case "exportBackup": {
return exportBackup(this.ws);
}
case "addBackupProvider": {
const req = codecForAddBackupProviderRequest().decode(payload);
await addBackupProvider(this.ws, req);
return {};
}
case "runBackupCycle": {
await runBackupCycle(this.ws);
return {};
}
case "exportBackupRecovery": {
const resp = await getBackupRecovery(this.ws);
return resp;
}
case "importBackupRecovery": {
const req = codecForAny().decode(payload);
await loadBackupRecovery(this.ws, req);
return {};
}
2021-03-03 21:20:05 +01:00
case "getBackupInfo": {
const resp = await getBackupInfo(this.ws);
return resp;
}
2021-01-18 23:35:41 +01:00
case "createDepositGroup": {
const req = codecForCreateDepositGroupRequest().decode(payload);
return await createDepositGroup(this.ws, req);
}
case "trackDepositGroup":
const req = codecForTrackDepositGroupRequest().decode(payload);
return trackDepositGroup(this.ws, req);
}
throw OperationFailedError.fromCode(
TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN,
"unknown operation",
{
operation,
},
);
}
/**
* Handle a request to the wallet-core API.
*/
async handleCoreApiRequest(
operation: string,
id: string,
payload: unknown,
): Promise<CoreApiResponse> {
try {
const result = await this.dispatchRequestInternal(operation, payload);
return {
type: "response",
operation,
id,
result,
};
} catch (e) {
if (
e instanceof OperationFailedError ||
e instanceof OperationFailedAndReportedError
) {
return {
type: "error",
operation,
id,
error: e.operationError,
};
} else {
return {
type: "error",
operation,
id,
error: makeErrorDetails(
TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
`unexpected exception: ${e}`,
{},
),
};
}
}
}
2016-10-18 01:16:31 +02:00
}