summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/operations/tip.ts
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2023-08-03 18:35:07 +0200
committerFlorian Dold <florian@dold.me>2023-08-03 18:35:07 +0200
commitfdbd55d2bde0961a4c1ff26b04e442459ab782b0 (patch)
treed0d04f42a5477f6d7d39a8940d59ff1548166711 /packages/taler-wallet-core/src/operations/tip.ts
parent0fe4840ca2612dda06417cdebe5229eea98180be (diff)
downloadwallet-core-fdbd55d2bde0961a4c1ff26b04e442459ab782b0.tar.gz
wallet-core-fdbd55d2bde0961a4c1ff26b04e442459ab782b0.tar.bz2
wallet-core-fdbd55d2bde0961a4c1ff26b04e442459ab782b0.zip
-towards tip->reward rename
Diffstat (limited to 'packages/taler-wallet-core/src/operations/tip.ts')
-rw-r--r--packages/taler-wallet-core/src/operations/tip.ts630
1 files changed, 0 insertions, 630 deletions
diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts
deleted file mode 100644
index e56fb1e8d..000000000
--- a/packages/taler-wallet-core/src/operations/tip.ts
+++ /dev/null
@@ -1,630 +0,0 @@
-/*
- This file is part of GNU Taler
- (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/>
- */
-
-/**
- * Imports.
- */
-import {
- AcceptTipResponse,
- AgeRestriction,
- Amounts,
- BlindedDenominationSignature,
- codecForMerchantTipResponseV2,
- codecForTipPickupGetResponse,
- CoinStatus,
- DenomKeyType,
- encodeCrock,
- getRandomBytes,
- j2s,
- Logger,
- NotificationType,
- parseTipUri,
- PrepareTipResult,
- TalerErrorCode,
- TalerPreciseTimestamp,
- TipPlanchetDetail,
- TransactionAction,
- TransactionMajorState,
- TransactionMinorState,
- TransactionState,
- TransactionType,
- URL,
-} from "@gnu-taler/taler-util";
-import { DerivedTipPlanchet } from "../crypto/cryptoTypes.js";
-import {
- CoinRecord,
- CoinSourceType,
- DenominationRecord,
- TipRecord,
- TipRecordStatus,
-} from "../db.js";
-import { makeErrorDetail } from "@gnu-taler/taler-util";
-import { InternalWalletState } from "../internal-wallet-state.js";
-import {
- getHttpResponseErrorDetails,
- readSuccessResponseJsonOrThrow,
-} from "@gnu-taler/taler-util/http";
-import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
-import {
- constructTaskIdentifier,
- makeCoinAvailable,
- makeCoinsVisible,
- TaskRunResult,
- TaskRunResultType,
-} from "./common.js";
-import { updateExchangeFromUrl } from "./exchanges.js";
-import {
- getCandidateWithdrawalDenoms,
- getExchangeWithdrawalInfo,
- updateWithdrawalDenoms,
-} from "./withdraw.js";
-import { selectWithdrawalDenominations } from "../util/coinSelection.js";
-import {
- constructTransactionIdentifier,
- notifyTransition,
- stopLongpolling,
-} from "./transactions.js";
-import { PendingTaskType } from "../pending-types.js";
-import { assertUnreachable } from "../util/assertUnreachable.js";
-
-const logger = new Logger("operations/tip.ts");
-
-/**
- * Get the (DD37-style) transaction status based on the
- * database record of a tip.
- */
-export function computeTipTransactionStatus(
- tipRecord: TipRecord,
-): TransactionState {
- switch (tipRecord.status) {
- case TipRecordStatus.Done:
- return {
- major: TransactionMajorState.Done,
- };
- case TipRecordStatus.Aborted:
- return {
- major: TransactionMajorState.Aborted,
- };
- case TipRecordStatus.PendingPickup:
- return {
- major: TransactionMajorState.Pending,
- minor: TransactionMinorState.Pickup,
- };
- case TipRecordStatus.DialogAccept:
- return {
- major: TransactionMajorState.Dialog,
- minor: TransactionMinorState.Proposed,
- };
- case TipRecordStatus.SuspendidPickup:
- return {
- major: TransactionMajorState.Pending,
- minor: TransactionMinorState.Pickup,
- };
- default:
- assertUnreachable(tipRecord.status);
- }
-}
-
-export function computeTipTransactionActions(
- tipRecord: TipRecord,
-): TransactionAction[] {
- switch (tipRecord.status) {
- case TipRecordStatus.Done:
- return [TransactionAction.Delete];
- case TipRecordStatus.Aborted:
- return [TransactionAction.Delete];
- case TipRecordStatus.PendingPickup:
- return [TransactionAction.Suspend, TransactionAction.Fail];
- case TipRecordStatus.SuspendidPickup:
- return [TransactionAction.Resume, TransactionAction.Fail];
- case TipRecordStatus.DialogAccept:
- return [TransactionAction.Abort];
- default:
- assertUnreachable(tipRecord.status);
- }
-}
-
-export async function prepareTip(
- ws: InternalWalletState,
- talerTipUri: string,
-): Promise<PrepareTipResult> {
- const res = parseTipUri(talerTipUri);
- if (!res) {
- throw Error("invalid taler://tip URI");
- }
-
- let tipRecord = await ws.db
- .mktx((x) => [x.tips])
- .runReadOnly(async (tx) => {
- return tx.tips.indexes.byMerchantTipIdAndBaseUrl.get([
- res.merchantTipId,
- res.merchantBaseUrl,
- ]);
- });
-
- if (!tipRecord) {
- const tipStatusUrl = new URL(
- `tips/${res.merchantTipId}`,
- res.merchantBaseUrl,
- );
- 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 ${j2s(tipPickupStatus)}`);
-
- const amount = Amounts.parseOrThrow(tipPickupStatus.tip_amount);
-
- logger.trace("new tip, creating tip record");
- await updateExchangeFromUrl(ws, tipPickupStatus.exchange_url);
-
- //FIXME: is this needed? withdrawDetails is not used
- // * if the intention is to update the exchange information in the database
- // maybe we can use another name. `get` seems like a pure-function
- const withdrawDetails = await getExchangeWithdrawalInfo(
- ws,
- tipPickupStatus.exchange_url,
- amount,
- undefined,
- );
-
- const walletTipId = encodeCrock(getRandomBytes(32));
- await updateWithdrawalDenoms(ws, tipPickupStatus.exchange_url);
- const denoms = await getCandidateWithdrawalDenoms(
- ws,
- tipPickupStatus.exchange_url,
- );
- const selectedDenoms = selectWithdrawalDenominations(amount, denoms);
-
- const secretSeed = encodeCrock(getRandomBytes(64));
- const denomSelUid = encodeCrock(getRandomBytes(32));
-
- const newTipRecord: TipRecord = {
- walletTipId: walletTipId,
- acceptedTimestamp: undefined,
- status: TipRecordStatus.DialogAccept,
- tipAmountRaw: Amounts.stringify(amount),
- tipExpiration: tipPickupStatus.expiration,
- exchangeBaseUrl: tipPickupStatus.exchange_url,
- next_url: tipPickupStatus.next_url,
- merchantBaseUrl: res.merchantBaseUrl,
- createdTimestamp: TalerPreciseTimestamp.now(),
- merchantTipId: res.merchantTipId,
- tipAmountEffective: Amounts.stringify(selectedDenoms.totalCoinValue),
- denomsSel: selectedDenoms,
- pickedUpTimestamp: undefined,
- secretSeed,
- denomSelUid,
- };
- await ws.db
- .mktx((x) => [x.tips])
- .runReadWrite(async (tx) => {
- await tx.tips.put(newTipRecord);
- });
- tipRecord = newTipRecord;
- }
-
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Tip,
- walletTipId: tipRecord.walletTipId,
- });
-
- const tipStatus: PrepareTipResult = {
- accepted: !!tipRecord && !!tipRecord.acceptedTimestamp,
- tipAmountRaw: Amounts.stringify(tipRecord.tipAmountRaw),
- exchangeBaseUrl: tipRecord.exchangeBaseUrl,
- merchantBaseUrl: tipRecord.merchantBaseUrl,
- expirationTimestamp: tipRecord.tipExpiration,
- tipAmountEffective: Amounts.stringify(tipRecord.tipAmountEffective),
- walletTipId: tipRecord.walletTipId,
- transactionId,
- };
-
- return tipStatus;
-}
-
-export async function processTip(
- ws: InternalWalletState,
- walletTipId: string,
-): Promise<TaskRunResult> {
- const tipRecord = await ws.db
- .mktx((x) => [x.tips])
- .runReadOnly(async (tx) => {
- return tx.tips.get(walletTipId);
- });
- if (!tipRecord) {
- return TaskRunResult.finished();
- }
-
- switch (tipRecord.status) {
- case TipRecordStatus.Aborted:
- case TipRecordStatus.DialogAccept:
- case TipRecordStatus.Done:
- case TipRecordStatus.SuspendidPickup:
- return TaskRunResult.finished();
- }
-
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Tip,
- walletTipId,
- });
-
- const denomsForWithdraw = tipRecord.denomsSel;
-
- const planchets: DerivedTipPlanchet[] = [];
- // Planchets in the form that the merchant expects
- const planchetsDetail: TipPlanchetDetail[] = [];
- const denomForPlanchet: { [index: number]: DenominationRecord } = [];
-
- for (const dh of denomsForWithdraw.selectedDenoms) {
- const denom = await ws.db
- .mktx((x) => [x.denominations])
- .runReadOnly(async (tx) => {
- return tx.denominations.get([
- tipRecord.exchangeBaseUrl,
- dh.denomPubHash,
- ]);
- });
- checkDbInvariant(!!denom, "denomination should be in database");
- for (let i = 0; i < dh.count; i++) {
- const deriveReq = {
- denomPub: denom.denomPub,
- planchetIndex: planchets.length,
- secretSeed: tipRecord.secretSeed,
- };
- logger.trace(`deriving tip planchet: ${j2s(deriveReq)}`);
- const p = await ws.cryptoApi.createTipPlanchet(deriveReq);
- logger.trace(`derive result: ${j2s(p)}`);
- denomForPlanchet[planchets.length] = denom;
- planchets.push(p);
- planchetsDetail.push({
- coin_ev: p.coinEv,
- denom_pub_hash: denom.denomPubHash,
- });
- }
- }
-
- const tipStatusUrl = new URL(
- `tips/${tipRecord.merchantTipId}/pickup`,
- tipRecord.merchantBaseUrl,
- );
-
- const req = { planchets: planchetsDetail };
- logger.trace(`sending tip request: ${j2s(req)}`);
- const merchantResp = await ws.http.postJson(tipStatusUrl.href, req);
-
- logger.trace(`got tip response, status ${merchantResp.status}`);
-
- // FIXME: Why do we do this?
- if (
- (merchantResp.status >= 500 && merchantResp.status <= 599) ||
- merchantResp.status === 424
- ) {
- logger.trace(`got transient tip error`);
- // FIXME: wrap in another error code that indicates a transient error
- return {
- type: TaskRunResultType.Error,
- errorDetail: makeErrorDetail(
- TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
- getHttpResponseErrorDetails(merchantResp),
- "tip pickup failed (transient)",
- ),
- };
- }
- let blindedSigs: BlindedDenominationSignature[] = [];
-
- const response = await readSuccessResponseJsonOrThrow(
- merchantResp,
- codecForMerchantTipResponseV2(),
- );
- blindedSigs = response.blind_sigs.map((x) => x.blind_sig);
-
- if (blindedSigs.length !== planchets.length) {
- throw Error("number of tip responses does not match requested planchets");
- }
-
- const newCoinRecords: CoinRecord[] = [];
-
- for (let i = 0; i < blindedSigs.length; i++) {
- const blindedSig = blindedSigs[i];
-
- const denom = denomForPlanchet[i];
- checkLogicInvariant(!!denom);
- const planchet = planchets[i];
- checkLogicInvariant(!!planchet);
-
- if (denom.denomPub.cipher !== DenomKeyType.Rsa) {
- throw Error("unsupported cipher");
- }
-
- if (blindedSig.cipher !== DenomKeyType.Rsa) {
- throw Error("unsupported cipher");
- }
-
- const denomSigRsa = await ws.cryptoApi.rsaUnblind({
- bk: planchet.blindingKey,
- blindedSig: blindedSig.blinded_rsa_signature,
- pk: denom.denomPub.rsa_public_key,
- });
-
- const isValid = await ws.cryptoApi.rsaVerify({
- hm: planchet.coinPub,
- pk: denom.denomPub.rsa_public_key,
- sig: denomSigRsa.sig,
- });
-
- if (!isValid) {
- return {
- type: TaskRunResultType.Error,
- errorDetail: makeErrorDetail(
- TalerErrorCode.WALLET_TIPPING_COIN_SIGNATURE_INVALID,
- {},
- "invalid signature from the exchange (via merchant tip) after unblinding",
- ),
- };
- }
-
- newCoinRecords.push({
- blindingKey: planchet.blindingKey,
- coinPriv: planchet.coinPriv,
- coinPub: planchet.coinPub,
- coinSource: {
- type: CoinSourceType.Tip,
- coinIndex: i,
- walletTipId: walletTipId,
- },
- sourceTransactionId: transactionId,
- denomPubHash: denom.denomPubHash,
- denomSig: { cipher: DenomKeyType.Rsa, rsa_signature: denomSigRsa.sig },
- exchangeBaseUrl: tipRecord.exchangeBaseUrl,
- status: CoinStatus.Fresh,
- coinEvHash: planchet.coinEvHash,
- maxAge: AgeRestriction.AGE_UNRESTRICTED,
- ageCommitmentProof: planchet.ageCommitmentProof,
- spendAllocation: undefined,
- });
- }
-
- const transitionInfo = await ws.db
- .mktx((x) => [x.coins, x.coinAvailability, x.denominations, x.tips])
- .runReadWrite(async (tx) => {
- const tr = await tx.tips.get(walletTipId);
- if (!tr) {
- return;
- }
- if (tr.status !== TipRecordStatus.PendingPickup) {
- return;
- }
- const oldTxState = computeTipTransactionStatus(tr);
- tr.pickedUpTimestamp = TalerPreciseTimestamp.now();
- tr.status = TipRecordStatus.Done;
- await tx.tips.put(tr);
- const newTxState = computeTipTransactionStatus(tr);
- for (const cr of newCoinRecords) {
- await makeCoinAvailable(ws, tx, cr);
- }
- await makeCoinsVisible(ws, tx, transactionId);
- return { oldTxState, newTxState };
- });
- notifyTransition(ws, transactionId, transitionInfo);
- ws.notify({ type: NotificationType.BalanceChange });
-
- return TaskRunResult.finished();
-}
-
-export async function acceptTip(
- ws: InternalWalletState,
- walletTipId: string,
-): Promise<AcceptTipResponse> {
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Tip,
- walletTipId,
- });
- const dbRes = await ws.db
- .mktx((x) => [x.tips])
- .runReadWrite(async (tx) => {
- const tipRecord = await tx.tips.get(walletTipId);
- if (!tipRecord) {
- logger.error("tip not found");
- return;
- }
- if (tipRecord.status != TipRecordStatus.DialogAccept) {
- logger.warn("Unable to accept tip in the current state");
- return { tipRecord };
- }
- const oldTxState = computeTipTransactionStatus(tipRecord);
- tipRecord.acceptedTimestamp = TalerPreciseTimestamp.now();
- tipRecord.status = TipRecordStatus.PendingPickup;
- await tx.tips.put(tipRecord);
- const newTxState = computeTipTransactionStatus(tipRecord);
- return { tipRecord, transitionInfo: { oldTxState, newTxState } };
- });
-
- if (!dbRes) {
- throw Error("tip not found");
- }
-
- notifyTransition(ws, transactionId, dbRes.transitionInfo);
-
- const tipRecord = dbRes.tipRecord;
-
- return {
- transactionId: constructTransactionIdentifier({
- tag: TransactionType.Tip,
- walletTipId: walletTipId,
- }),
- next_url: tipRecord.next_url,
- };
-}
-
-export async function suspendTipTransaction(
- ws: InternalWalletState,
- walletTipId: string,
-): Promise<void> {
- const taskId = constructTaskIdentifier({
- tag: PendingTaskType.TipPickup,
- walletTipId,
- });
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Tip,
- walletTipId,
- });
- stopLongpolling(ws, taskId);
- const transitionInfo = await ws.db
- .mktx((x) => [x.tips])
- .runReadWrite(async (tx) => {
- const tipRec = await tx.tips.get(walletTipId);
- if (!tipRec) {
- logger.warn(`transaction tip ${walletTipId} not found`);
- return;
- }
- let newStatus: TipRecordStatus | undefined = undefined;
- switch (tipRec.status) {
- case TipRecordStatus.Done:
- case TipRecordStatus.SuspendidPickup:
- case TipRecordStatus.Aborted:
- case TipRecordStatus.DialogAccept:
- break;
- case TipRecordStatus.PendingPickup:
- newStatus = TipRecordStatus.SuspendidPickup;
- break;
-
- default:
- assertUnreachable(tipRec.status);
- }
- if (newStatus != null) {
- const oldTxState = computeTipTransactionStatus(tipRec);
- tipRec.status = newStatus;
- const newTxState = computeTipTransactionStatus(tipRec);
- await tx.tips.put(tipRec);
- return {
- oldTxState,
- newTxState,
- };
- }
- return undefined;
- });
- ws.workAvailable.trigger();
- notifyTransition(ws, transactionId, transitionInfo);
-}
-
-export async function resumeTipTransaction(
- ws: InternalWalletState,
- walletTipId: string,
-): Promise<void> {
- const taskId = constructTaskIdentifier({
- tag: PendingTaskType.TipPickup,
- walletTipId,
- });
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Tip,
- walletTipId,
- });
- stopLongpolling(ws, taskId);
- const transitionInfo = await ws.db
- .mktx((x) => [x.tips])
- .runReadWrite(async (tx) => {
- const tipRec = await tx.tips.get(walletTipId);
- if (!tipRec) {
- logger.warn(`transaction tip ${walletTipId} not found`);
- return;
- }
- let newStatus: TipRecordStatus | undefined = undefined;
- switch (tipRec.status) {
- case TipRecordStatus.Done:
- case TipRecordStatus.PendingPickup:
- case TipRecordStatus.Aborted:
- case TipRecordStatus.DialogAccept:
- break;
- case TipRecordStatus.SuspendidPickup:
- newStatus = TipRecordStatus.PendingPickup;
- break;
- default:
- assertUnreachable(tipRec.status);
- }
- if (newStatus != null) {
- const oldTxState = computeTipTransactionStatus(tipRec);
- tipRec.status = newStatus;
- const newTxState = computeTipTransactionStatus(tipRec);
- await tx.tips.put(tipRec);
- return {
- oldTxState,
- newTxState,
- };
- }
- return undefined;
- });
- notifyTransition(ws, transactionId, transitionInfo);
-}
-
-export async function failTipTransaction(
- ws: InternalWalletState,
- walletTipId: string,
-): Promise<void> {
- // We don't have an "aborting" state, so this should never happen!
- throw Error("can't run cance-aborting on tip transaction");
-}
-
-export async function abortTipTransaction(
- ws: InternalWalletState,
- walletTipId: string,
-): Promise<void> {
- const taskId = constructTaskIdentifier({
- tag: PendingTaskType.TipPickup,
- walletTipId,
- });
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Tip,
- walletTipId,
- });
- stopLongpolling(ws, taskId);
- const transitionInfo = await ws.db
- .mktx((x) => [x.tips])
- .runReadWrite(async (tx) => {
- const tipRec = await tx.tips.get(walletTipId);
- if (!tipRec) {
- logger.warn(`transaction tip ${walletTipId} not found`);
- return;
- }
- let newStatus: TipRecordStatus | undefined = undefined;
- switch (tipRec.status) {
- case TipRecordStatus.Done:
- case TipRecordStatus.Aborted:
- case TipRecordStatus.PendingPickup:
- case TipRecordStatus.DialogAccept:
- break;
- case TipRecordStatus.SuspendidPickup:
- newStatus = TipRecordStatus.Aborted;
- break;
- default:
- assertUnreachable(tipRec.status);
- }
- if (newStatus != null) {
- const oldTxState = computeTipTransactionStatus(tipRec);
- tipRec.status = newStatus;
- const newTxState = computeTipTransactionStatus(tipRec);
- await tx.tips.put(tipRec);
- return {
- oldTxState,
- newTxState,
- };
- }
- return undefined;
- });
- notifyTransition(ws, transactionId, transitionInfo);
-}