wallet-core/src/wallet.ts

675 lines
18 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.
*/
2019-08-15 19:10:23 +02:00
import { CryptoApi, CryptoWorkerFactory } from "./crypto/cryptoApi";
import { HttpRequestLibrary } from "./util/http";
2017-05-28 01:10:54 +02:00
import {
oneShotPut,
oneShotGet,
runWithWriteTransaction,
oneShotIter,
oneShotIterIndex,
} from "./util/query";
import { AmountJson } from "./util/amounts";
import * as Amounts from "./util/amounts";
import {
acceptWithdrawal,
getWithdrawalInfo,
getWithdrawDetailsForUri,
getWithdrawDetailsForAmount,
} from "./wallet-impl/withdraw";
import {
abortFailedPayment,
preparePay,
confirmPay,
SpeculativePayData,
} from "./wallet-impl/pay";
2016-05-24 01:53:56 +02:00
import {
2017-05-28 01:10:54 +02:00
CoinRecord,
CoinStatus,
CurrencyRecord,
DenominationRecord,
2016-11-15 15:07:17 +01:00
ExchangeRecord,
2019-11-30 00:36:20 +01:00
PlanchetRecord,
ProposalRecord,
PurchaseRecord,
ReserveRecord,
Stores,
2019-11-21 23:09:43 +01:00
ReserveRecordStatus,
} from "./dbTypes";
import { MerchantRefundPermission } from "./talerTypes";
import {
Badge,
2018-09-20 02:56:13 +02:00
BenchmarkResult,
ConfirmPayResult,
ConfirmReserveRequest,
CreateReserveRequest,
CreateReserveResponse,
2019-11-21 23:09:43 +01:00
HistoryEvent,
Notifier,
ReturnCoinsRequest,
SenderWireInfos,
2017-11-30 04:07:36 +01:00
TipStatus,
WalletBalance,
PreparePayResult,
2019-08-28 02:49:27 +02:00
DownloadedWithdrawInfo,
WithdrawDetails,
AcceptWithdrawalResponse,
2019-08-31 13:27:12 +02:00
PurchaseDetails,
2019-11-19 16:16:12 +01:00
PendingOperationInfo,
PendingOperationsResponse,
HistoryQuery,
} from "./walletTypes";
import { Logger } from "./util/logging";
2015-12-13 23:47:30 +01:00
import { assertUnreachable } from "./util/assertUnreachable";
import { applyRefund, getFullRefundFees } from "./wallet-impl/refund";
import {
updateExchangeFromUrl,
getExchangeTrust,
getExchangePaytoUri,
} from "./wallet-impl/exchanges";
import { processReserve } from "./wallet-impl/reserves";
import { AsyncOpMemo } from "./util/asyncMemo";
import { InternalWalletState } from "./wallet-impl/state";
import { createReserve, confirmReserve } from "./wallet-impl/reserves";
import { processRefreshSession, refresh } from "./wallet-impl/refresh";
import { processWithdrawSession } from "./wallet-impl/withdraw";
import { getHistory } from "./wallet-impl/history";
import { getPendingOperations } from "./wallet-impl/pending";
import { getBalances } from "./wallet-impl/balance";
import { acceptTip, getTipStatus } from "./wallet-impl/tip";
import { returnCoins } from "./wallet-impl/return";
import { payback } from "./wallet-impl/payback";
/**
* Wallet protocol version spoken with the exchange
* and merchant.
*
* Uses libtool's current:revision:age versioning.
*/
2019-05-08 07:01:17 +02:00
export const WALLET_PROTOCOL_VERSION = "3:0:0";
2017-06-04 18:46:32 +02:00
export const WALLET_CACHE_BREAKER_CLIENT_VERSION = "2";
2019-08-31 22:07:16 +02:00
const builtinCurrencies: CurrencyRecord[] = [
{
auditors: [
{
2017-06-04 19:41:43 +02:00
auditorPub: "BW9DC48PHQY4NH011SHHX36DZZ3Q22Y6X7FZ1VD1CMZ2PTFZ6PN0",
baseUrl: "https://auditor.demo.taler.net/",
2019-06-26 15:30:32 +02:00
expirationStamp: new Date(2027, 1).getTime(),
},
2017-04-12 17:47:14 +02:00
],
exchanges: [],
2017-05-28 01:10:54 +02:00
fractionalDigits: 2,
name: "KUDOS",
},
];
/**
* This error is thrown when an
*/
2019-11-21 23:09:43 +01:00
export class OperationFailedAndReportedError extends Error {
constructor(message: string) {
super(message);
// Set the prototype explicitly.
Object.setPrototypeOf(this, OperationFailedAndReportedError.prototype);
}
}
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;
get db(): IDBDatabase {
return this.ws.db;
}
private get badge(): Badge {
return this.ws.badge;
}
private get cryptoApi(): CryptoApi {
return this.ws.cryptoApi;
}
2016-02-11 18:17:02 +01:00
private get notifier(): Notifier {
return this.ws.notifier;
}
2019-11-30 00:36:20 +01:00
2019-06-26 15:30:32 +02:00
constructor(
db: IDBDatabase,
http: HttpRequestLibrary,
badge: Badge,
notifier: Notifier,
2019-08-15 19:10:23 +02:00
cryptoWorkerFactory: CryptoWorkerFactory,
2019-06-26 15:30:32 +02:00
) {
this.ws = {
badge,
cachedNextUrl: {},
cryptoApi: new CryptoApi(cryptoWorkerFactory),
db,
http,
notifier,
speculativePayData: undefined,
memoProcessReserve: new AsyncOpMemo<void>(),
};
}
getExchangePaytoUri(exchangeBaseUrl: string, supportedTargetTypes: string[]) {
return getExchangePaytoUri(this.ws, exchangeBaseUrl, supportedTargetTypes);
}
getWithdrawDetailsForAmount(baseUrl: any, amount: AmountJson): any {
return getWithdrawDetailsForAmount(this.ws, baseUrl, amount);
}
2017-06-05 02:00:03 +02:00
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,
): Promise<void> {
switch (pending.type) {
case "bug":
return;
case "dirty-coin":
await this.refresh(pending.coinPub);
break;
case "exchange-update":
await this.updateExchangeFromUrl(pending.exchangeBaseUrl);
break;
case "planchet":
// Nothing to do, since the withdraw session will process the planchet
2019-11-30 00:36:20 +01:00
break;
case "refresh":
await this.processRefreshSession(pending.refreshSessionId);
break;
case "reserve":
await this.processReserve(pending.reservePub);
break;
case "withdraw":
await this.processWithdrawSession(pending.withdrawSessionId);
break;
case "proposal":
// Nothing to do, user needs to accept/reject
break;
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.
*/
public async runPending(): Promise<void> {
const pendingOpsResponse = await this.getPendingOperations();
for (const p of pendingOpsResponse.pendingOperations) {
try {
await this.processOnePendingOperation(p);
} catch (e) {
console.error(e);
}
2019-11-21 23:09:43 +01:00
}
}
/**
2019-11-21 23:09:43 +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
public async runUntilStopped(): Promise<void> {
throw Error("not implemented");
}
/**
* Run until all coins have been withdrawn from the given reserve,
* or an error has occured.
*/
public async runUntilReserveDepleted(reservePub: string) {
while (true) {
2019-11-30 00:36:20 +01:00
const r = await this.getPendingOperations();
const allPending = r.pendingOperations;
const relevantPending = allPending.filter(x => {
switch (x.type) {
case "planchet":
case "reserve":
return x.reservePub === reservePub;
default:
return false;
2019-11-21 23:09:43 +01:00
}
2019-11-30 00:36:20 +01:00
});
if (relevantPending.length === 0) {
return;
}
for (const p of relevantPending) {
await this.processOnePendingOperation(p);
2019-11-21 23:09:43 +01:00
}
}
}
/**
* Insert the hard-coded defaults for exchanges, coins and
* auditors into the database, unless these defaults have
* already been applied.
*/
async fillDefaults() {
2019-11-20 20:02:48 +01:00
await runWithWriteTransaction(
this.db,
[Stores.config, Stores.currencies],
async tx => {
let applied = false;
2019-11-20 20:02:48 +01:00
await tx.iter(Stores.config).forEach(x => {
if (x.key == "currencyDefaultsApplied" && x.value == true) {
applied = true;
}
});
if (!applied) {
for (let c of builtinCurrencies) {
await tx.put(Stores.currencies, 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.
*/
async preparePay(talerPayUri: string): Promise<PreparePayResult> {
return preparePay(this.ws, talerPayUri);
}
/**
* Refresh all dirty coins.
* The returned promise resolves only after all refresh
* operations have completed.
*/
async refreshDirtyCoins(): Promise<{ numRefreshed: number }> {
let n = 0;
const coins = await oneShotIter(this.db, Stores.coins).toArray();
for (let coin of coins) {
if (coin.status == CoinStatus.Dirty) {
try {
await this.refresh(coin.coinPub);
} catch (e) {
console.log("error during refresh");
}
n += 1;
}
}
return { numRefreshed: n };
}
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> {
return confirmPay(this.ws, proposalId, sessionIdOverride);
}
2019-11-30 00:36:20 +01:00
2016-02-09 21:56:06 +01:00
/**
* First fetch information requred to withdraw from the reserve,
* 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> {
return processReserve(this.ws, reservePub);
}
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
*/
2019-06-26 15:30:32 +02:00
async createReserve(
req: CreateReserveRequest,
): Promise<CreateReserveResponse> {
return createReserve(this.ws, req);
2016-02-09 21:56:06 +01:00
}
/**
* Mark an existing reserve as confirmed. The wallet will start trying
* to withdraw from that reserve. This may not immediately succeed,
2016-03-01 19:39:17 +01:00
* since the exchange might not know about the reserve yet, even though the
2016-02-09 21:56:06 +01:00
* bank confirmed its creation.
*
* A confirmed reserve should be shown to the user in the UI, while
* an unconfirmed reserve should be hidden.
*/
2016-09-28 18:54:48 +02:00
async confirmReserve(req: ConfirmReserveRequest): Promise<void> {
return confirmReserve(this.ws, req);
}
2016-09-28 18:54:48 +02:00
private async processWithdrawSession(
withdrawalSessionId: string,
): Promise<void> {
return processWithdrawSession(this.ws, withdrawalSessionId);
}
2015-12-13 23:47:30 +01: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 getWithdrawDetailsForUri(
talerWithdrawUri: string,
maybeSelectedExchange?: string,
): Promise<WithdrawDetails> {
return getWithdrawDetailsForUri(
this.ws,
talerWithdrawUri,
maybeSelectedExchange,
2019-11-30 00:36:20 +01:00
);
}
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,
force: boolean = false,
): Promise<ExchangeRecord> {
return updateExchangeFromUrl(this.ws, baseUrl, force);
}
/**
* Get detailed balance information, sliced by exchange and by currency.
*/
async getBalances(): Promise<WalletBalance> {
return getBalances(this.ws);
}
async refresh(oldCoinPub: string, force: boolean = false): Promise<void> {
return refresh(this.ws, oldCoinPub, force);
}
async processRefreshSession(refreshSessionId: string) {
return processRefreshSession(this.ws, refreshSessionId);
2016-10-14 02:13:06 +02:00
}
2019-11-20 20:02:48 +01:00
async findExchange(
exchangeBaseUrl: string,
): Promise<ExchangeRecord | undefined> {
return await oneShotGet(this.db, Stores.exchanges, exchangeBaseUrl);
}
2016-02-11 18:17:02 +01:00
/**
* Retrive the full event history for this wallet.
*/
async getHistory(
historyQuery?: HistoryQuery,
2019-11-21 23:09:43 +01:00
): Promise<{ history: HistoryEvent[] }> {
return getHistory(this.ws, historyQuery);
2016-10-12 02:55:53 +02:00
}
2019-11-19 16:16:12 +01:00
async getPendingOperations(): Promise<PendingOperationsResponse> {
return getPendingOperations(this.ws);
2019-11-19 16:16:12 +01:00
}
async getDenoms(exchangeUrl: string): Promise<DenominationRecord[]> {
2019-11-20 20:02:48 +01:00
const denoms = await oneShotIterIndex(
this.db,
Stores.denominations.exchangeBaseUrlIndex,
exchangeUrl,
).toArray();
return denoms;
}
2016-11-13 10:17:39 +01:00
2019-11-30 00:36:20 +01:00
async getProposal(proposalId: string): Promise<ProposalRecord | undefined> {
const proposal = await oneShotGet(this.db, Stores.proposals, proposalId);
return proposal;
2016-11-13 10:17:39 +01:00
}
2016-11-15 15:07:17 +01:00
async getExchanges(): Promise<ExchangeRecord[]> {
return await oneShotIter(this.db, Stores.exchanges).toArray();
}
2016-02-23 14:07:53 +01:00
2017-03-24 17:54:22 +01:00
async getCurrencies(): Promise<CurrencyRecord[]> {
return await oneShotIter(this.db, Stores.currencies).toArray();
2017-03-24 17:54:22 +01:00
}
async updateCurrency(currencyRecord: CurrencyRecord): Promise<void> {
logger.trace("updating currency to", currencyRecord);
await oneShotPut(this.db, Stores.currencies, currencyRecord);
2017-03-24 17:54:22 +01:00
this.notifier.notify();
}
2016-10-13 02:23:24 +02:00
async getReserves(exchangeBaseUrl: string): Promise<ReserveRecord[]> {
2019-11-20 20:02:48 +01:00
return await oneShotIter(this.db, Stores.reserves).filter(
2019-11-21 23:09:43 +01:00
r => r.exchangeBaseUrl === exchangeBaseUrl,
2019-11-20 20:02:48 +01:00
);
2016-10-12 02:55:53 +02:00
}
2019-11-21 23:09:43 +01:00
async getCoinsForExchange(exchangeBaseUrl: string): Promise<CoinRecord[]> {
2019-11-20 20:02:48 +01:00
return await oneShotIter(this.db, Stores.coins).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[]> {
return await oneShotIter(this.db, Stores.coins).toArray();
}
async payback(coinPub: string): Promise<void> {
return payback(this.ws, coinPub);
}
async getPaybackReserves(): Promise<ReserveRecord[]> {
2019-11-20 20:02:48 +01:00
return await oneShotIter(this.db, Stores.reserves).filter(
r => r.hasPayback,
);
}
2017-06-05 02:00:03 +02:00
/**
* Stop ongoing processing.
*/
stop() {
2019-11-21 23:09:43 +01:00
//this.timerGroup.stopCurrentAndFutureTimers();
this.cryptoApi.stop();
2017-06-05 02:00:03 +02:00
}
async getSenderWireInfos(): Promise<SenderWireInfos> {
const m: { [url: string]: Set<string> } = {};
2019-11-20 20:02:48 +01:00
await oneShotIter(this.db, 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[] } = {};
2019-06-26 15:30:32 +02:00
Object.keys(m).map(e => {
exchangeWireTypes[e] = Array.from(m[e]);
});
2019-07-21 23:50:10 +02:00
const senderWiresSet: Set<string> = new Set();
2019-11-20 20:02:48 +01:00
await oneShotIter(this.db, Stores.senderWires).forEach(x => {
senderWiresSet.add(x.paytoUri);
});
2019-07-21 23:50:10 +02:00
const senderWires: string[] = Array.from(senderWiresSet);
return {
exchangeWireTypes,
senderWires,
};
}
/**
* Trigger paying coins back into the user's account.
*/
async returnCoins(req: ReturnCoinsRequest): Promise<void> {
return returnCoins(this.ws, req);
}
/**
* Accept a refund, return the contract hash for the contract
* that was involved in the refund.
*/
2019-08-31 11:49:36 +02:00
async applyRefund(talerRefundUri: string): Promise<string> {
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> {
return oneShotGet(this.db, Stores.purchases, contractTermsHash);
2017-08-27 03:56:19 +02:00
}
2019-06-26 15:30:32 +02:00
async getFullRefundFees(
refundPermissions: MerchantRefundPermission[],
): Promise<AmountJson> {
return getFullRefundFees(this.ws, refundPermissions);
}
2017-11-30 04:07:36 +01:00
2019-11-30 00:36:20 +01:00
async acceptTip(talerTipUri: string): Promise<void> {
return acceptTip(this.ws, talerTipUri);
2017-11-30 04:07:36 +01:00
}
2019-08-30 17:27:59 +02:00
async getTipStatus(talerTipUri: string): Promise<TipStatus> {
return getTipStatus(this.ws, talerTipUri);
2017-11-30 04:07:36 +01:00
}
async abortFailedPayment(contractTermsHash: string): Promise<void> {
return abortFailedPayment(this.ws, contractTermsHash);
}
2019-11-30 00:36:20 +01:00
public async handleNotifyReserve() {
const reserves = await oneShotIter(this.db, Stores.reserves).toArray();
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.
*/
async collectGarbage() {
// 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.
}
2019-11-30 00:36:20 +01:00
/**
* Get information about a withdrawal from
* a taler://withdraw URI.
*/
2019-09-01 01:05:38 +02:00
async getWithdrawalInfo(
2019-08-28 02:49:27 +02:00
talerWithdrawUri: string,
): Promise<DownloadedWithdrawInfo> {
return getWithdrawalInfo(this.ws, talerWithdrawUri);
2019-08-28 02:49:27 +02:00
}
async acceptWithdrawal(
2019-08-28 02:49:27 +02:00
talerWithdrawUri: string,
selectedExchange: string,
): Promise<AcceptWithdrawalResponse> {
return acceptWithdrawal(this.ws, talerWithdrawUri, selectedExchange);
2019-08-28 02:49:27 +02:00
}
2019-08-31 13:27:12 +02:00
async getPurchaseDetails(hc: string): Promise<PurchaseDetails> {
const purchase = await oneShotGet(this.db, Stores.purchases, hc);
2019-08-31 13:27:12 +02:00
if (!purchase) {
throw Error("unknown purchase");
}
const refundsDoneAmounts = Object.values(purchase.refundsDone).map(x =>
Amounts.parseOrThrow(x.refund_amount),
);
2019-11-20 20:02:48 +01:00
const refundsPendingAmounts = Object.values(
purchase.refundsPending,
).map(x => Amounts.parseOrThrow(x.refund_amount));
2019-08-31 13:27:12 +02:00
const totalRefundAmount = Amounts.sum([
...refundsDoneAmounts,
...refundsPendingAmounts,
]).amount;
const refundsDoneFees = Object.values(purchase.refundsDone).map(x =>
Amounts.parseOrThrow(x.refund_amount),
);
const refundsPendingFees = Object.values(purchase.refundsPending).map(x =>
Amounts.parseOrThrow(x.refund_amount),
2019-08-31 13:27:12 +02:00
);
const totalRefundFees = Amounts.sum([
...refundsDoneFees,
...refundsPendingFees,
]).amount;
const totalFees = totalRefundFees;
return {
contractTerms: purchase.contractTerms,
2019-11-21 23:09:43 +01:00
hasRefund: purchase.timestamp_refund !== undefined,
2019-08-31 13:27:12 +02:00
totalRefundAmount: totalRefundAmount,
totalRefundAndRefreshFees: totalFees,
};
}
clearNotification(): void {
this.badge.clearNotification();
}
2018-09-20 02:56:13 +02:00
benchmarkCrypto(repetitions: number): Promise<BenchmarkResult> {
return this.cryptoApi.benchmark(repetitions);
}
2016-10-18 01:16:31 +02:00
}