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

347 lines
9.7 KiB
TypeScript
Raw Normal View History

/*
This file is part of GNU Taler
2019-12-16 16:59:09 +01:00
(C) 2019 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { InternalWalletState } from "./state";
import { parseTipUri } from "../util/taleruri";
import { TipStatus, OperationErrorDetails } from "../types/walletTypes";
2019-12-16 16:59:09 +01:00
import {
TipPlanchetDetail,
codecForTipPickupGetResponse,
codecForTipResponse,
2019-12-16 16:59:09 +01:00
} from "../types/talerTypes";
import * as Amounts from "../util/amounts";
2019-12-16 16:59:09 +01:00
import {
Stores,
PlanchetRecord,
WithdrawalGroupRecord,
2019-12-16 16:59:09 +01:00
initRetryInfo,
updateRetryInfoTimeout,
WithdrawalSourceType,
TipPlanchet,
2019-12-16 16:59:09 +01:00
} from "../types/dbTypes";
import {
getExchangeWithdrawalInfo,
selectWithdrawalDenoms,
processWithdrawGroup,
denomSelectionInfoToState,
2019-12-16 16:59:09 +01:00
} from "./withdraw";
import { updateExchangeFromUrl } from "./exchanges";
import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
2019-12-05 19:38:19 +01:00
import { guardOperationException } from "./errors";
import { NotificationType } from "../types/notifications";
import { getTimestampNow } from "../util/time";
import { readSuccessResponseJsonOrThrow } from "../util/http";
import { URL } from "../util/url";
import { Logger } from "../util/logging";
const logger = new Logger("operations/tip.ts");
export async function getTipStatus(
ws: InternalWalletState,
2019-12-16 16:59:09 +01:00
talerTipUri: string,
): Promise<TipStatus> {
const res = parseTipUri(talerTipUri);
if (!res) {
throw Error("invalid taler://tip URI");
}
const tipStatusUrl = new URL("tip-pickup", res.merchantBaseUrl);
tipStatusUrl.searchParams.set("tip_id", res.merchantTipId);
logger.trace("checking tip status from", tipStatusUrl.href);
const merchantResp = await ws.http.get(tipStatusUrl.href);
const tipPickupStatus = await readSuccessResponseJsonOrThrow(
merchantResp,
codecForTipPickupGetResponse(),
);
logger.trace(`status ${tipPickupStatus}`);
2020-04-06 17:45:41 +02:00
const amount = Amounts.parseOrThrow(tipPickupStatus.amount);
2020-07-27 13:39:52 +02:00
const merchantOrigin = new URL(res.merchantBaseUrl).origin;
2019-12-12 22:39:45 +01:00
let tipRecord = await ws.db.get(Stores.tips, [
res.merchantTipId,
2020-07-27 13:39:52 +02:00
merchantOrigin,
]);
if (!tipRecord) {
await updateExchangeFromUrl(ws, tipPickupStatus.exchange_url);
2019-12-09 19:59:08 +01:00
const withdrawDetails = await getExchangeWithdrawalInfo(
ws,
tipPickupStatus.exchange_url,
amount,
);
const tipId = encodeCrock(getRandomBytes(32));
const selectedDenoms = await selectWithdrawalDenoms(
ws,
tipPickupStatus.exchange_url,
amount,
);
tipRecord = {
tipId,
2019-12-16 12:53:22 +01:00
acceptedTimestamp: undefined,
rejectedTimestamp: undefined,
amount,
deadline: tipPickupStatus.stamp_expire,
exchangeUrl: tipPickupStatus.exchange_url,
merchantBaseUrl: res.merchantBaseUrl,
nextUrl: undefined,
pickedUp: false,
planchets: undefined,
response: undefined,
2019-12-05 19:38:19 +01:00
createdTimestamp: getTimestampNow(),
merchantTipId: res.merchantTipId,
totalFees: Amounts.add(
withdrawDetails.overhead,
withdrawDetails.withdrawFee,
).amount,
2019-12-05 19:38:19 +01:00
retryInfo: initRetryInfo(),
lastError: undefined,
denomsSel: denomSelectionInfoToState(selectedDenoms),
};
2019-12-12 22:39:45 +01:00
await ws.db.put(Stores.tips, tipRecord);
}
const tipStatus: TipStatus = {
2019-12-16 12:53:22 +01:00
accepted: !!tipRecord && !!tipRecord.acceptedTimestamp,
amount: Amounts.parseOrThrow(tipPickupStatus.amount),
amountLeft: Amounts.parseOrThrow(tipPickupStatus.amount_left),
exchangeUrl: tipPickupStatus.exchange_url,
nextUrl: tipPickupStatus.extra.next_url,
2020-07-27 13:39:52 +02:00
merchantOrigin: merchantOrigin,
merchantTipId: res.merchantTipId,
expirationTimestamp: tipPickupStatus.stamp_expire,
timestamp: tipPickupStatus.stamp_created,
totalFees: tipRecord.totalFees,
tipId: tipRecord.tipId,
};
return tipStatus;
}
2019-12-05 19:38:19 +01:00
async function incrementTipRetry(
ws: InternalWalletState,
refreshSessionId: string,
err: OperationErrorDetails | undefined,
2019-12-05 19:38:19 +01:00
): Promise<void> {
2020-03-30 12:39:32 +02:00
await ws.db.runWithWriteTransaction([Stores.tips], async (tx) => {
2019-12-05 19:38:19 +01:00
const t = await tx.get(Stores.tips, refreshSessionId);
if (!t) {
return;
}
if (!t.retryInfo) {
return;
}
t.retryInfo.retryCounter++;
updateRetryInfoTimeout(t.retryInfo);
t.lastError = err;
await tx.put(Stores.tips, t);
});
ws.notify({ type: NotificationType.TipOperationError });
2019-12-05 19:38:19 +01:00
}
export async function processTip(
ws: InternalWalletState,
tipId: string,
2020-04-06 17:45:41 +02:00
forceNow = false,
2019-12-05 19:38:19 +01:00
): Promise<void> {
const onOpErr = (e: OperationErrorDetails): Promise<void> =>
2020-04-07 10:07:32 +02:00
incrementTipRetry(ws, tipId, e);
2019-12-16 16:59:09 +01:00
await guardOperationException(
() => processTipImpl(ws, tipId, forceNow),
onOpErr,
);
}
async function resetTipRetry(
ws: InternalWalletState,
tipId: string,
): Promise<void> {
2020-03-30 12:39:32 +02:00
await ws.db.mutate(Stores.tips, tipId, (x) => {
if (x.retryInfo.active) {
x.retryInfo = initRetryInfo();
}
return x;
2019-12-16 16:59:09 +01:00
});
2019-12-05 19:38:19 +01:00
}
async function processTipImpl(
ws: InternalWalletState,
tipId: string,
forceNow: boolean,
2020-04-07 10:07:32 +02:00
): Promise<void> {
if (forceNow) {
await resetTipRetry(ws, tipId);
}
2019-12-12 22:39:45 +01:00
let tipRecord = await ws.db.get(Stores.tips, tipId);
if (!tipRecord) {
return;
}
if (tipRecord.pickedUp) {
logger.warn("tip already picked up");
return;
}
const denomsForWithdraw = tipRecord.denomsSel;
if (!tipRecord.planchets) {
const planchets: TipPlanchet[] = [];
for (const sd of denomsForWithdraw.selectedDenoms) {
const denom = await ws.db.getIndexed(
Stores.denominations.denomPubHashIndex,
sd.denomPubHash,
);
if (!denom) {
throw Error("denom does not exist anymore");
}
for (let i = 0; i < sd.count; i++) {
const r = await ws.cryptoApi.createTipPlanchet(denom);
planchets.push(r);
}
}
2020-03-30 12:39:32 +02:00
await ws.db.mutate(Stores.tips, tipId, (r) => {
if (!r.planchets) {
r.planchets = planchets;
}
return r;
});
}
2019-12-12 22:39:45 +01:00
tipRecord = await ws.db.get(Stores.tips, tipId);
if (!tipRecord) {
throw Error("tip not in database");
}
if (!tipRecord.planchets) {
throw Error("invariant violated");
}
logger.trace("got planchets for tip!");
// Planchets in the form that the merchant expects
2020-03-30 12:39:32 +02:00
const planchetsDetail: TipPlanchetDetail[] = tipRecord.planchets.map((p) => ({
coin_ev: p.coinEv,
denom_pub_hash: p.denomPubHash,
}));
let merchantResp;
const tipStatusUrl = new URL("tip-pickup", tipRecord.merchantBaseUrl);
try {
const req = { planchets: planchetsDetail, tip_id: tipRecord.merchantTipId };
merchantResp = await ws.http.postJson(tipStatusUrl.href, req);
2019-12-09 13:29:11 +01:00
if (merchantResp.status !== 200) {
throw Error(`unexpected status ${merchantResp.status} for tip-pickup`);
}
logger.trace("got merchant resp:", merchantResp);
} catch (e) {
logger.warn("tipping failed", e);
throw e;
}
const response = codecForTipResponse().decode(await merchantResp.json());
if (response.reserve_sigs.length !== tipRecord.planchets.length) {
throw Error("number of tip responses does not match requested planchets");
}
const withdrawalGroupId = encodeCrock(getRandomBytes(32));
const planchets: PlanchetRecord[] = [];
for (let i = 0; i < tipRecord.planchets.length; i++) {
const tipPlanchet = tipRecord.planchets[i];
const coinEvHash = await ws.cryptoApi.hashEncoded(tipPlanchet.coinEv);
const planchet: PlanchetRecord = {
blindingKey: tipPlanchet.blindingKey,
coinEv: tipPlanchet.coinEv,
coinPriv: tipPlanchet.coinPriv,
coinPub: tipPlanchet.coinPub,
coinValue: tipPlanchet.coinValue,
denomPub: tipPlanchet.denomPub,
denomPubHash: tipPlanchet.denomPubHash,
reservePub: response.reserve_pub,
withdrawSig: response.reserve_sigs[i].reserve_sig,
isFromTip: true,
coinEvHash,
coinIdx: i,
withdrawalDone: false,
withdrawalGroupId: withdrawalGroupId,
lastError: undefined,
};
planchets.push(planchet);
}
const withdrawalGroup: WithdrawalGroupRecord = {
exchangeBaseUrl: tipRecord.exchangeUrl,
source: {
type: WithdrawalSourceType.Tip,
tipId: tipRecord.tipId,
},
2019-12-16 16:20:45 +01:00
timestampStart: getTimestampNow(),
withdrawalGroupId: withdrawalGroupId,
2019-12-03 01:33:25 +01:00
rawWithdrawalAmount: tipRecord.amount,
2019-12-05 19:38:19 +01:00
retryInfo: initRetryInfo(),
2019-12-16 16:20:45 +01:00
timestampFinish: undefined,
2019-12-05 19:38:19 +01:00
lastError: undefined,
denomsSel: tipRecord.denomsSel,
};
2019-12-16 16:59:09 +01:00
await ws.db.runWithWriteTransaction(
[Stores.tips, Stores.withdrawalGroups],
2020-03-30 12:39:32 +02:00
async (tx) => {
2019-12-16 16:59:09 +01:00
const tr = await tx.get(Stores.tips, tipId);
if (!tr) {
return;
}
if (tr.pickedUp) {
return;
}
tr.pickedUp = true;
tr.retryInfo = initRetryInfo(false);
2019-12-16 16:59:09 +01:00
await tx.put(Stores.tips, tr);
await tx.put(Stores.withdrawalGroups, withdrawalGroup);
for (const p of planchets) {
await tx.put(Stores.planchets, p);
}
2019-12-16 16:59:09 +01:00
},
);
await processWithdrawGroup(ws, withdrawalGroupId);
}
export async function acceptTip(
ws: InternalWalletState,
tipId: string,
): Promise<void> {
2019-12-12 22:39:45 +01:00
const tipRecord = await ws.db.get(Stores.tips, tipId);
if (!tipRecord) {
logger.error("tip not found");
return;
}
2019-12-16 12:53:22 +01:00
tipRecord.acceptedTimestamp = getTimestampNow();
2019-12-12 22:39:45 +01:00
await ws.db.put(Stores.tips, tipRecord);
await processTip(ws, tipId);
return;
}