From fdbd55d2bde0961a4c1ff26b04e442459ab782b0 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 3 Aug 2023 18:35:07 +0200 Subject: -towards tip->reward rename --- packages/taler-wallet-core/src/db.ts | 58 +- .../src/operations/backup/export.ts | 18 +- .../src/operations/backup/import.ts | 32 +- .../taler-wallet-core/src/operations/common.ts | 32 +- .../taler-wallet-core/src/operations/pending.ts | 18 +- .../taler-wallet-core/src/operations/recoup.ts | 6 +- .../taler-wallet-core/src/operations/reward.ts | 630 +++++++++++++++++++++ packages/taler-wallet-core/src/operations/tip.ts | 630 --------------------- .../src/operations/transactions.ts | 84 +-- packages/taler-wallet-core/src/pending-types.ts | 4 +- packages/taler-wallet-core/src/wallet-api-types.ts | 30 +- packages/taler-wallet-core/src/wallet.ts | 24 +- 12 files changed, 776 insertions(+), 790 deletions(-) create mode 100644 packages/taler-wallet-core/src/operations/reward.ts delete mode 100644 packages/taler-wallet-core/src/operations/tip.ts (limited to 'packages/taler-wallet-core') diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 6a7a26f2f..3d2878d93 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -677,7 +677,7 @@ export interface PlanchetRecord { export enum CoinSourceType { Withdraw = "withdraw", Refresh = "refresh", - Tip = "tip", + Reward = "reward", } export interface WithdrawCoinSource { @@ -705,13 +705,13 @@ export interface RefreshCoinSource { oldCoinPub: string; } -export interface TipCoinSource { - type: CoinSourceType.Tip; - walletTipId: string; +export interface RewardCoinSource { + type: CoinSourceType.Reward; + walletRewardId: string; coinIndex: number; } -export type CoinSource = WithdrawCoinSource | RefreshCoinSource | TipCoinSource; +export type CoinSource = WithdrawCoinSource | RefreshCoinSource | RewardCoinSource; /** * CoinRecord as stored in the "coins" data store @@ -815,9 +815,9 @@ export interface CoinAllocation { } /** - * Status of a tip we got from a merchant. + * Status of a reward we got from a merchant. */ -export interface TipRecord { +export interface RewardRecord { /** * Has the user accepted the tip? Only after the tip has been accepted coins * withdrawn from the tip may be used. @@ -827,17 +827,17 @@ export interface TipRecord { /** * The tipped amount. */ - tipAmountRaw: AmountString; + rewardAmountRaw: AmountString; /** * Effect on the balance (including fees etc). */ - tipAmountEffective: AmountString; + rewardAmountEffective: AmountString; /** * Timestamp, the tip can't be picked up anymore after this deadline. */ - tipExpiration: TalerProtocolTimestamp; + rewardExpiration: TalerProtocolTimestamp; /** * The exchange that will sign our coins, chosen by the merchant. @@ -863,7 +863,7 @@ export interface TipRecord { /** * Tip ID chosen by the wallet. */ - walletTipId: string; + walletRewardId: string; /** * Secret seed used to derive planchets for this tip. @@ -871,9 +871,9 @@ export interface TipRecord { secretSeed: string; /** - * The merchant's identifier for this tip. + * The merchant's identifier for this reward. */ - merchantTipId: string; + merchantRewardId: string; createdTimestamp: TalerPreciseTimestamp; @@ -888,10 +888,10 @@ export interface TipRecord { */ pickedUpTimestamp: TalerPreciseTimestamp | undefined; - status: TipRecordStatus; + status: RewardRecordStatus; } -export enum TipRecordStatus { +export enum RewardRecordStatus { PendingPickup = 10, SuspendidPickup = 20, @@ -1420,7 +1420,7 @@ export interface KycPendingInfo { } /** * Group of withdrawal operations that need to be executed. - * (Either for a normal withdrawal or from a tip.) + * (Either for a normal withdrawal or from a reward.) * * The withdrawal group record is only created after we know * the coin selection we want to withdraw. @@ -2480,12 +2480,12 @@ export const WalletStoresV1 = { ]), }, ), - tips: describeStore( - "tips", - describeContents({ keyPath: "walletTipId" }), + rewards: describeStore( + "rewards", + describeContents({ keyPath: "walletRewardId" }), { - byMerchantTipIdAndBaseUrl: describeIndex("byMerchantTipIdAndBaseUrl", [ - "merchantTipId", + byMerchantTipIdAndBaseUrl: describeIndex("byMerchantRewardIdAndBaseUrl", [ + "merchantRewardId", "merchantBaseUrl", ]), byStatus: describeIndex("byStatus", "status", { @@ -2935,22 +2935,6 @@ export const walletDbFixups: FixupDescription[] = [ }); }, }, - { - name: "TipRecordRecord_status_add", - async fn(tx): Promise { - await tx.tips.iter().forEachAsync(async (r) => { - // Remove legacy transactions that don't have the totalCost field yet. - if (r.status == null) { - if (r.pickedUpTimestamp) { - r.status = TipRecordStatus.Done; - } else { - r.status = TipRecordStatus.PendingPickup; - } - await tx.tips.put(r); - } - }); - }, - }, { name: "CoinAvailabilityRecord_visibleCoinCount_add", async fn(tx): Promise { diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts b/packages/taler-wallet-core/src/operations/backup/export.ts index 21ba5dc37..c9446a05f 100644 --- a/packages/taler-wallet-core/src/operations/backup/export.ts +++ b/packages/taler-wallet-core/src/operations/backup/export.ts @@ -96,7 +96,7 @@ export async function exportBackup( x.purchases, x.refreshGroups, x.backupProviders, - x.tips, + x.rewards, x.recoupGroups, x.withdrawalGroups, ]) @@ -184,12 +184,12 @@ export async function exportBackup( }); }); - await tx.tips.iter().forEach((tip) => { + await tx.rewards.iter().forEach((tip) => { backupTips.push({ exchange_base_url: tip.exchangeBaseUrl, merchant_base_url: tip.merchantBaseUrl, - merchant_tip_id: tip.merchantTipId, - wallet_tip_id: tip.walletTipId, + merchant_tip_id: tip.merchantRewardId, + wallet_tip_id: tip.walletRewardId, next_url: tip.next_url, secret_seed: tip.secretSeed, selected_denoms: tip.denomsSel.selectedDenoms.map((x) => ({ @@ -199,8 +199,8 @@ export async function exportBackup( timestamp_finished: tip.pickedUpTimestamp, timestamp_accepted: tip.acceptedTimestamp, timestamp_created: tip.createdTimestamp, - timestamp_expiration: tip.tipExpiration, - tip_amount_raw: Amounts.stringify(tip.tipAmountRaw), + timestamp_expiration: tip.rewardExpiration, + tip_amount_raw: Amounts.stringify(tip.rewardAmountRaw), selected_denoms_uid: tip.denomSelUid, }); }); @@ -244,11 +244,11 @@ export async function exportBackup( refresh_group_id: coin.coinSource.refreshGroupId, }; break; - case CoinSourceType.Tip: + case CoinSourceType.Reward: bcs = { - type: BackupCoinSourceType.Tip, + type: BackupCoinSourceType.Reward, coin_index: coin.coinSource.coinIndex, - wallet_tip_id: coin.coinSource.walletTipId, + wallet_tip_id: coin.coinSource.walletRewardId, }; break; case CoinSourceType.Withdraw: diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts index b161aa8f2..a53b624e8 100644 --- a/packages/taler-wallet-core/src/operations/backup/import.ts +++ b/packages/taler-wallet-core/src/operations/backup/import.ts @@ -56,7 +56,7 @@ import { WithdrawalGroupStatus, WithdrawalRecordType, RefreshOperationStatus, - TipRecordStatus, + RewardRecordStatus, } from "../../db.js"; import { InternalWalletState } from "../../internal-wallet-state.js"; import { assertUnreachable } from "../../util/assertUnreachable.js"; @@ -250,11 +250,11 @@ export async function importCoin( refreshGroupId: backupCoin.coin_source.refresh_group_id, }; break; - case BackupCoinSourceType.Tip: + case BackupCoinSourceType.Reward: coinSource = { - type: CoinSourceType.Tip, + type: CoinSourceType.Reward, coinIndex: backupCoin.coin_source.coin_index, - walletTipId: backupCoin.coin_source.wallet_tip_id, + walletRewardId: backupCoin.coin_source.wallet_tip_id, }; break; case BackupCoinSourceType.Withdraw: @@ -311,7 +311,7 @@ export async function importBackup( x.purchases, x.refreshGroups, x.backupProviders, - x.tips, + x.rewards, x.recoupGroups, x.withdrawalGroups, x.tombstones, @@ -812,13 +812,13 @@ export async function importBackup( for (const backupTip of backupBlob.tips) { const ts = constructTombstone({ - tag: TombstoneTag.DeleteTip, + tag: TombstoneTag.DeleteReward, walletTipId: backupTip.wallet_tip_id, }); if (tombstoneSet.has(ts)) { continue; } - const existingTip = await tx.tips.get(backupTip.wallet_tip_id); + const existingTip = await tx.rewards.get(backupTip.wallet_tip_id); if (!existingTip) { const tipAmountRaw = Amounts.parseOrThrow(backupTip.tip_amount_raw); const denomsSel = await getDenomSelStateFromBackup( @@ -827,22 +827,22 @@ export async function importBackup( backupTip.exchange_base_url, backupTip.selected_denoms, ); - await tx.tips.put({ + await tx.rewards.put({ acceptedTimestamp: backupTip.timestamp_accepted, createdTimestamp: backupTip.timestamp_created, denomsSel, next_url: backupTip.next_url, exchangeBaseUrl: backupTip.exchange_base_url, merchantBaseUrl: backupTip.exchange_base_url, - merchantTipId: backupTip.merchant_tip_id, + merchantRewardId: backupTip.merchant_tip_id, pickedUpTimestamp: backupTip.timestamp_finished, secretSeed: backupTip.secret_seed, - tipAmountEffective: Amounts.stringify(denomsSel.totalCoinValue), - tipAmountRaw: Amounts.stringify(tipAmountRaw), - tipExpiration: backupTip.timestamp_expiration, - walletTipId: backupTip.wallet_tip_id, + rewardAmountEffective: Amounts.stringify(denomsSel.totalCoinValue), + rewardAmountRaw: Amounts.stringify(tipAmountRaw), + rewardExpiration: backupTip.timestamp_expiration, + walletRewardId: backupTip.wallet_tip_id, denomSelUid: backupTip.selected_denoms_uid, - status: TipRecordStatus.Done, // FIXME! + status: RewardRecordStatus.Done, // FIXME! }); } } @@ -863,8 +863,8 @@ export async function importBackup( } else if (type === TombstoneTag.DeleteRefund) { // Nothing required, will just prevent display // in the transactions list - } else if (type === TombstoneTag.DeleteTip) { - await tx.tips.delete(rest[0]); + } else if (type === TombstoneTag.DeleteReward) { + await tx.rewards.delete(rest[0]); } else if (type === TombstoneTag.DeleteWithdrawalGroup) { await tx.withdrawalGroups.delete(rest[0]); } else { diff --git a/packages/taler-wallet-core/src/operations/common.ts b/packages/taler-wallet-core/src/operations/common.ts index cc16a4704..7a8b78b53 100644 --- a/packages/taler-wallet-core/src/operations/common.ts +++ b/packages/taler-wallet-core/src/operations/common.ts @@ -57,7 +57,7 @@ import { PurchaseRecord, RecoupGroupRecord, RefreshGroupRecord, - TipRecord, + RewardRecord, WithdrawalGroupRecord, } from "../db.js"; import { makeErrorDetail, TalerError } from "@gnu-taler/taler-util"; @@ -293,10 +293,10 @@ function convertTaskToTransactionId( tag: TransactionType.Refresh, refreshGroupId: parsedTaskId.refreshGroupId, }); - case PendingTaskType.TipPickup: + case PendingTaskType.RewardPickup: return constructTransactionIdentifier({ - tag: TransactionType.Tip, - walletTipId: parsedTaskId.walletTipId, + tag: TransactionType.Reward, + walletRewardId: parsedTaskId.walletRewardId, }); case PendingTaskType.PeerPushDebit: return constructTransactionIdentifier({ @@ -515,7 +515,7 @@ export enum TombstoneTag { DeleteWithdrawalGroup = "delete-withdrawal-group", DeleteReserve = "delete-reserve", DeletePayment = "delete-payment", - DeleteTip = "delete-tip", + DeleteReward = "delete-reward", DeleteRefreshGroup = "delete-refresh-group", DeleteDepositGroup = "delete-deposit-group", DeleteRefund = "delete-refund", @@ -601,7 +601,9 @@ export function runLongpollAsync( }; res = await reqFn(cts.token); } catch (e) { - await storePendingTaskError(ws, retryTag, getErrorDetailFromException(e)); + const errDetail = getErrorDetailFromException(e); + logger.warn(`got error during long-polling: ${j2s(errDetail)}`); + await storePendingTaskError(ws, retryTag, errDetail); return; } finally { delete ws.activeLongpoll[retryTag]; @@ -622,7 +624,7 @@ export type ParsedTombstone = | { tag: TombstoneTag.DeleteRefund; refundGroupId: string } | { tag: TombstoneTag.DeleteReserve; reservePub: string } | { tag: TombstoneTag.DeleteRefreshGroup; refreshGroupId: string } - | { tag: TombstoneTag.DeleteTip; walletTipId: string } + | { tag: TombstoneTag.DeleteReward; walletTipId: string } | { tag: TombstoneTag.DeletePayment; proposalId: string }; export function constructTombstone(p: ParsedTombstone): TombstoneIdStr { @@ -637,7 +639,7 @@ export function constructTombstone(p: ParsedTombstone): TombstoneIdStr { return `tmb:${p.tag}:${p.proposalId}` as TombstoneIdStr; case TombstoneTag.DeleteRefreshGroup: return `tmb:${p.tag}:${p.refreshGroupId}` as TombstoneIdStr; - case TombstoneTag.DeleteTip: + case TombstoneTag.DeleteReward: return `tmb:${p.tag}:${p.walletTipId}` as TombstoneIdStr; default: assertUnreachable(p); @@ -810,7 +812,7 @@ export type ParsedTaskIdentifier = | { tag: PendingTaskType.PeerPushDebit; pursePub: string } | { tag: PendingTaskType.Purchase; proposalId: string } | { tag: PendingTaskType.Recoup; recoupGroupId: string } - | { tag: PendingTaskType.TipPickup; walletTipId: string } + | { tag: PendingTaskType.RewardPickup; walletRewardId: string } | { tag: PendingTaskType.Refresh; refreshGroupId: string }; export function parseTaskIdentifier(x: string): ParsedTaskIdentifier { @@ -844,8 +846,8 @@ export function parseTaskIdentifier(x: string): ParsedTaskIdentifier { return { tag: type, recoupGroupId: rest[0] }; case PendingTaskType.Refresh: return { tag: type, refreshGroupId: rest[0] }; - case PendingTaskType.TipPickup: - return { tag: type, walletTipId: rest[0] }; + case PendingTaskType.RewardPickup: + return { tag: type, walletRewardId: rest[0] }; case PendingTaskType.Withdraw: return { tag: type, withdrawalGroupId: rest[0] }; default: @@ -877,8 +879,8 @@ export function constructTaskIdentifier(p: ParsedTaskIdentifier): TaskId { return `${p.tag}:${p.recoupGroupId}` as TaskId; case PendingTaskType.Refresh: return `${p.tag}:${p.refreshGroupId}` as TaskId; - case PendingTaskType.TipPickup: - return `${p.tag}:${p.walletTipId}` as TaskId; + case PendingTaskType.RewardPickup: + return `${p.tag}:${p.walletRewardId}` as TaskId; case PendingTaskType.Withdraw: return `${p.tag}:${p.withdrawalGroupId}` as TaskId; default: @@ -899,8 +901,8 @@ export namespace TaskIdentifiers { export function forExchangeCheckRefresh(exch: ExchangeRecord): TaskId { return `${PendingTaskType.ExchangeCheckRefresh}:${exch.baseUrl}` as TaskId; } - export function forTipPickup(tipRecord: TipRecord): TaskId { - return `${PendingTaskType.TipPickup}:${tipRecord.walletTipId}` as TaskId; + export function forTipPickup(tipRecord: RewardRecord): TaskId { + return `${PendingTaskType.RewardPickup}:${tipRecord.walletRewardId}` as TaskId; } export function forRefresh(refreshGroupRecord: RefreshGroupRecord): TaskId { return `${PendingTaskType.Refresh}:${refreshGroupRecord.refreshGroupId}` as TaskId; diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts index 870437e2e..cc9217d67 100644 --- a/packages/taler-wallet-core/src/operations/pending.ts +++ b/packages/taler-wallet-core/src/operations/pending.ts @@ -32,7 +32,7 @@ import { PeerPushPaymentIncomingStatus, PeerPullPaymentInitiationStatus, WithdrawalGroupStatus, - TipRecordStatus, + RewardRecordStatus, DepositOperationStatus, } from "../db.js"; import { @@ -232,17 +232,17 @@ async function gatherDepositPending( async function gatherTipPending( ws: InternalWalletState, tx: GetReadOnlyAccess<{ - tips: typeof WalletStoresV1.tips; + rewards: typeof WalletStoresV1.rewards; operationRetries: typeof WalletStoresV1.operationRetries; }>, now: AbsoluteTime, resp: PendingOperationsResponse, ): Promise { const range = GlobalIDB.KeyRange.bound( - TipRecordStatus.PendingPickup, - TipRecordStatus.PendingPickup, + RewardRecordStatus.PendingPickup, + RewardRecordStatus.PendingPickup, ); - await tx.tips.indexes.byStatus.iter(range).forEachAsync(async (tip) => { + await tx.rewards.indexes.byStatus.iter(range).forEachAsync(async (tip) => { // FIXME: The tip record needs a proper status field! if (tip.pickedUpTimestamp) { return; @@ -252,13 +252,13 @@ async function gatherTipPending( const timestampDue = retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now(); if (tip.acceptedTimestamp) { resp.pendingOperations.push({ - type: PendingTaskType.TipPickup, + type: PendingTaskType.RewardPickup, ...getPendingCommon(ws, opId, timestampDue), givesLifeness: true, timestampDue: retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now(), merchantBaseUrl: tip.merchantBaseUrl, - tipId: tip.walletTipId, - merchantTipId: tip.merchantTipId, + tipId: tip.walletRewardId, + merchantTipId: tip.merchantRewardId, }); } }); @@ -494,7 +494,7 @@ export async function getPendingOperations( x.refreshGroups, x.coins, x.withdrawalGroups, - x.tips, + x.rewards, x.purchases, x.planchets, x.depositGroups, diff --git a/packages/taler-wallet-core/src/operations/recoup.ts b/packages/taler-wallet-core/src/operations/recoup.ts index dea2d4b16..abeca1119 100644 --- a/packages/taler-wallet-core/src/operations/recoup.ts +++ b/packages/taler-wallet-core/src/operations/recoup.ts @@ -82,7 +82,7 @@ async function putGroupAsFinished( await tx.recoupGroups.put(recoupGroup); } -async function recoupTipCoin( +async function recoupRewardCoin( ws: InternalWalletState, recoupGroupId: string, coinIdx: number, @@ -482,8 +482,8 @@ async function processRecoup( const cs = coin.coinSource; switch (cs.type) { - case CoinSourceType.Tip: - return recoupTipCoin(ws, recoupGroupId, coinIdx, coin); + case CoinSourceType.Reward: + return recoupRewardCoin(ws, recoupGroupId, coinIdx, coin); case CoinSourceType.Refresh: return recoupRefreshCoin(ws, recoupGroupId, coinIdx, coin, cs); case CoinSourceType.Withdraw: diff --git a/packages/taler-wallet-core/src/operations/reward.ts b/packages/taler-wallet-core/src/operations/reward.ts new file mode 100644 index 000000000..58c745780 --- /dev/null +++ b/packages/taler-wallet-core/src/operations/reward.ts @@ -0,0 +1,630 @@ +/* + 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 + */ + +/** + * 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, + RewardRecord, + RewardRecordStatus, +} 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 reward. + */ +export function computeRewardTransactionStatus( + tipRecord: RewardRecord, +): TransactionState { + switch (tipRecord.status) { + case RewardRecordStatus.Done: + return { + major: TransactionMajorState.Done, + }; + case RewardRecordStatus.Aborted: + return { + major: TransactionMajorState.Aborted, + }; + case RewardRecordStatus.PendingPickup: + return { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.Pickup, + }; + case RewardRecordStatus.DialogAccept: + return { + major: TransactionMajorState.Dialog, + minor: TransactionMinorState.Proposed, + }; + case RewardRecordStatus.SuspendidPickup: + return { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.Pickup, + }; + default: + assertUnreachable(tipRecord.status); + } +} + +export function computeTipTransactionActions( + tipRecord: RewardRecord, +): TransactionAction[] { + switch (tipRecord.status) { + case RewardRecordStatus.Done: + return [TransactionAction.Delete]; + case RewardRecordStatus.Aborted: + return [TransactionAction.Delete]; + case RewardRecordStatus.PendingPickup: + return [TransactionAction.Suspend, TransactionAction.Fail]; + case RewardRecordStatus.SuspendidPickup: + return [TransactionAction.Resume, TransactionAction.Fail]; + case RewardRecordStatus.DialogAccept: + return [TransactionAction.Abort]; + default: + assertUnreachable(tipRecord.status); + } +} + +export async function prepareTip( + ws: InternalWalletState, + talerTipUri: string, +): Promise { + const res = parseTipUri(talerTipUri); + if (!res) { + throw Error("invalid taler://tip URI"); + } + + let tipRecord = await ws.db + .mktx((x) => [x.rewards]) + .runReadOnly(async (tx) => { + return tx.rewards.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: RewardRecord = { + walletRewardId: walletTipId, + acceptedTimestamp: undefined, + status: RewardRecordStatus.DialogAccept, + rewardAmountRaw: Amounts.stringify(amount), + rewardExpiration: tipPickupStatus.expiration, + exchangeBaseUrl: tipPickupStatus.exchange_url, + next_url: tipPickupStatus.next_url, + merchantBaseUrl: res.merchantBaseUrl, + createdTimestamp: TalerPreciseTimestamp.now(), + merchantRewardId: res.merchantTipId, + rewardAmountEffective: Amounts.stringify(selectedDenoms.totalCoinValue), + denomsSel: selectedDenoms, + pickedUpTimestamp: undefined, + secretSeed, + denomSelUid, + }; + await ws.db + .mktx((x) => [x.rewards]) + .runReadWrite(async (tx) => { + await tx.rewards.put(newTipRecord); + }); + tipRecord = newTipRecord; + } + + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.Reward, + walletRewardId: tipRecord.walletRewardId, + }); + + const tipStatus: PrepareTipResult = { + accepted: !!tipRecord && !!tipRecord.acceptedTimestamp, + rewardAmountRaw: Amounts.stringify(tipRecord.rewardAmountRaw), + exchangeBaseUrl: tipRecord.exchangeBaseUrl, + merchantBaseUrl: tipRecord.merchantBaseUrl, + expirationTimestamp: tipRecord.rewardExpiration, + rewardAmountEffective: Amounts.stringify(tipRecord.rewardAmountEffective), + walletRewardId: tipRecord.walletRewardId, + transactionId, + }; + + return tipStatus; +} + +export async function processTip( + ws: InternalWalletState, + walletTipId: string, +): Promise { + const tipRecord = await ws.db + .mktx((x) => [x.rewards]) + .runReadOnly(async (tx) => { + return tx.rewards.get(walletTipId); + }); + if (!tipRecord) { + return TaskRunResult.finished(); + } + + switch (tipRecord.status) { + case RewardRecordStatus.Aborted: + case RewardRecordStatus.DialogAccept: + case RewardRecordStatus.Done: + case RewardRecordStatus.SuspendidPickup: + return TaskRunResult.finished(); + } + + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.Reward, + walletRewardId: 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.merchantRewardId}/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.Reward, + coinIndex: i, + walletRewardId: 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.rewards]) + .runReadWrite(async (tx) => { + const tr = await tx.rewards.get(walletTipId); + if (!tr) { + return; + } + if (tr.status !== RewardRecordStatus.PendingPickup) { + return; + } + const oldTxState = computeRewardTransactionStatus(tr); + tr.pickedUpTimestamp = TalerPreciseTimestamp.now(); + tr.status = RewardRecordStatus.Done; + await tx.rewards.put(tr); + const newTxState = computeRewardTransactionStatus(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 { + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.Reward, + walletRewardId: walletTipId, + }); + const dbRes = await ws.db + .mktx((x) => [x.rewards]) + .runReadWrite(async (tx) => { + const tipRecord = await tx.rewards.get(walletTipId); + if (!tipRecord) { + logger.error("tip not found"); + return; + } + if (tipRecord.status != RewardRecordStatus.DialogAccept) { + logger.warn("Unable to accept tip in the current state"); + return { tipRecord }; + } + const oldTxState = computeRewardTransactionStatus(tipRecord); + tipRecord.acceptedTimestamp = TalerPreciseTimestamp.now(); + tipRecord.status = RewardRecordStatus.PendingPickup; + await tx.rewards.put(tipRecord); + const newTxState = computeRewardTransactionStatus(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.Reward, + walletRewardId: walletTipId, + }), + next_url: tipRecord.next_url, + }; +} + +export async function suspendRewardTransaction( + ws: InternalWalletState, + walletRewardId: string, +): Promise { + const taskId = constructTaskIdentifier({ + tag: PendingTaskType.RewardPickup, + walletRewardId: walletRewardId, + }); + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.Reward, + walletRewardId: walletRewardId, + }); + stopLongpolling(ws, taskId); + const transitionInfo = await ws.db + .mktx((x) => [x.rewards]) + .runReadWrite(async (tx) => { + const tipRec = await tx.rewards.get(walletRewardId); + if (!tipRec) { + logger.warn(`transaction tip ${walletRewardId} not found`); + return; + } + let newStatus: RewardRecordStatus | undefined = undefined; + switch (tipRec.status) { + case RewardRecordStatus.Done: + case RewardRecordStatus.SuspendidPickup: + case RewardRecordStatus.Aborted: + case RewardRecordStatus.DialogAccept: + break; + case RewardRecordStatus.PendingPickup: + newStatus = RewardRecordStatus.SuspendidPickup; + break; + + default: + assertUnreachable(tipRec.status); + } + if (newStatus != null) { + const oldTxState = computeRewardTransactionStatus(tipRec); + tipRec.status = newStatus; + const newTxState = computeRewardTransactionStatus(tipRec); + await tx.rewards.put(tipRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + ws.workAvailable.trigger(); + notifyTransition(ws, transactionId, transitionInfo); +} + +export async function resumeTipTransaction( + ws: InternalWalletState, + walletRewardId: string, +): Promise { + const taskId = constructTaskIdentifier({ + tag: PendingTaskType.RewardPickup, + walletRewardId: walletRewardId, + }); + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.Reward, + walletRewardId: walletRewardId, + }); + stopLongpolling(ws, taskId); + const transitionInfo = await ws.db + .mktx((x) => [x.rewards]) + .runReadWrite(async (tx) => { + const rewardRec = await tx.rewards.get(walletRewardId); + if (!rewardRec) { + logger.warn(`transaction reward ${walletRewardId} not found`); + return; + } + let newStatus: RewardRecordStatus | undefined = undefined; + switch (rewardRec.status) { + case RewardRecordStatus.Done: + case RewardRecordStatus.PendingPickup: + case RewardRecordStatus.Aborted: + case RewardRecordStatus.DialogAccept: + break; + case RewardRecordStatus.SuspendidPickup: + newStatus = RewardRecordStatus.PendingPickup; + break; + default: + assertUnreachable(rewardRec.status); + } + if (newStatus != null) { + const oldTxState = computeRewardTransactionStatus(rewardRec); + rewardRec.status = newStatus; + const newTxState = computeRewardTransactionStatus(rewardRec); + await tx.rewards.put(rewardRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + notifyTransition(ws, transactionId, transitionInfo); +} + +export async function failTipTransaction( + ws: InternalWalletState, + walletTipId: string, +): Promise { + // 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, + walletRewardId: string, +): Promise { + const taskId = constructTaskIdentifier({ + tag: PendingTaskType.RewardPickup, + walletRewardId: walletRewardId, + }); + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.Reward, + walletRewardId: walletRewardId, + }); + stopLongpolling(ws, taskId); + const transitionInfo = await ws.db + .mktx((x) => [x.rewards]) + .runReadWrite(async (tx) => { + const tipRec = await tx.rewards.get(walletRewardId); + if (!tipRec) { + logger.warn(`transaction tip ${walletRewardId} not found`); + return; + } + let newStatus: RewardRecordStatus | undefined = undefined; + switch (tipRec.status) { + case RewardRecordStatus.Done: + case RewardRecordStatus.Aborted: + case RewardRecordStatus.PendingPickup: + case RewardRecordStatus.DialogAccept: + break; + case RewardRecordStatus.SuspendidPickup: + newStatus = RewardRecordStatus.Aborted; + break; + default: + assertUnreachable(tipRec.status); + } + if (newStatus != null) { + const oldTxState = computeRewardTransactionStatus(tipRec); + tipRec.status = newStatus; + const newTxState = computeRewardTransactionStatus(tipRec); + await tx.rewards.put(tipRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + notifyTransition(ws, transactionId, transitionInfo); +} 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 - */ - -/** - * 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - // 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 { - 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); -} diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index 868f00de7..a16809b36 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -58,7 +58,7 @@ import { RefreshGroupRecord, RefreshOperationStatus, RefundGroupRecord, - TipRecord, + RewardRecord, WalletContractData, WithdrawalGroupRecord, WithdrawalGroupStatus, @@ -107,11 +107,11 @@ import { import { abortTipTransaction, failTipTransaction, - computeTipTransactionStatus, + computeRewardTransactionStatus, resumeTipTransaction, - suspendTipTransaction, + suspendRewardTransaction, computeTipTransactionActions, -} from "./tip.js"; +} from "./reward.js"; import { abortWithdrawalTransaction, augmentPaytoUrisForWithdrawal, @@ -187,7 +187,7 @@ function shouldSkipSearch( */ const txOrder: { [t in TransactionType]: number } = { [TransactionType.Withdrawal]: 1, - [TransactionType.Tip]: 2, + [TransactionType.Reward]: 2, [TransactionType.Payment]: 3, [TransactionType.PeerPullCredit]: 4, [TransactionType.PeerPullDebit]: 5, @@ -284,12 +284,12 @@ export async function getTransactionById( throw Error(`no tx for refresh`); } - case TransactionType.Tip: { - const tipId = parsedTx.walletTipId; + case TransactionType.Reward: { + const tipId = parsedTx.walletRewardId; return await ws.db - .mktx((x) => [x.tips, x.operationRetries]) + .mktx((x) => [x.rewards, x.operationRetries]) .runReadWrite(async (tx) => { - const tipRecord = await tx.tips.get(tipId); + const tipRecord = await tx.rewards.get(tipId); if (!tipRecord) throw Error("not found"); const retries = await tx.operationRetries.get( @@ -818,21 +818,21 @@ function buildTransactionForDeposit( } function buildTransactionForTip( - tipRecord: TipRecord, + tipRecord: RewardRecord, ort?: OperationRetryRecord, ): Transaction { checkLogicInvariant(!!tipRecord.acceptedTimestamp); return { - type: TransactionType.Tip, - txState: computeTipTransactionStatus(tipRecord), + type: TransactionType.Reward, + txState: computeRewardTransactionStatus(tipRecord), txActions: computeTipTransactionActions(tipRecord), - amountEffective: Amounts.stringify(tipRecord.tipAmountEffective), - amountRaw: Amounts.stringify(tipRecord.tipAmountRaw), + amountEffective: Amounts.stringify(tipRecord.rewardAmountEffective), + amountRaw: Amounts.stringify(tipRecord.rewardAmountRaw), timestamp: tipRecord.acceptedTimestamp, transactionId: constructTransactionIdentifier({ - tag: TransactionType.Tip, - walletTipId: tipRecord.walletTipId, + tag: TransactionType.Reward, + walletRewardId: tipRecord.walletRewardId, }), merchantBaseUrl: tipRecord.merchantBaseUrl, ...(ort?.lastError ? { error: ort.lastError } : {}), @@ -945,7 +945,7 @@ export async function getTransactions( x.purchases, x.contractTerms, x.recoupGroups, - x.tips, + x.rewards, x.tombstones, x.withdrawalGroups, x.refreshGroups, @@ -1200,11 +1200,11 @@ export async function getTransactions( ); }); - tx.tips.iter().forEachAsync(async (tipRecord) => { + tx.rewards.iter().forEachAsync(async (tipRecord) => { if ( shouldSkipCurrency( transactionsRequest, - Amounts.parseOrThrow(tipRecord.tipAmountRaw).currency, + Amounts.parseOrThrow(tipRecord.rewardAmountRaw).currency, ) ) { return; @@ -1267,7 +1267,7 @@ export type ParsedTransactionIdentifier = | { tag: TransactionType.PeerPushDebit; pursePub: string } | { tag: TransactionType.Refresh; refreshGroupId: string } | { tag: TransactionType.Refund; refundGroupId: string } - | { tag: TransactionType.Tip; walletTipId: string } + | { tag: TransactionType.Reward; walletRewardId: string } | { tag: TransactionType.Withdrawal; withdrawalGroupId: string } | { tag: TransactionType.InternalWithdrawal; withdrawalGroupId: string }; @@ -1291,8 +1291,8 @@ export function constructTransactionIdentifier( return `txn:${pTxId.tag}:${pTxId.refreshGroupId}` as TransactionIdStr; case TransactionType.Refund: return `txn:${pTxId.tag}:${pTxId.refundGroupId}` as TransactionIdStr; - case TransactionType.Tip: - return `txn:${pTxId.tag}:${pTxId.walletTipId}` as TransactionIdStr; + case TransactionType.Reward: + return `txn:${pTxId.tag}:${pTxId.walletRewardId}` as TransactionIdStr; case TransactionType.Withdrawal: return `txn:${pTxId.tag}:${pTxId.withdrawalGroupId}` as TransactionIdStr; case TransactionType.InternalWithdrawal: @@ -1346,10 +1346,10 @@ export function parseTransactionIdentifier( tag: TransactionType.Refund, refundGroupId: rest[0], }; - case TransactionType.Tip: + case TransactionType.Reward: return { - tag: TransactionType.Tip, - walletTipId: rest[0], + tag: TransactionType.Reward, + walletRewardId: rest[0], }; case TransactionType.Withdrawal: return { @@ -1427,10 +1427,10 @@ export async function retryTransaction( stopLongpolling(ws, taskId); break; } - case TransactionType.Tip: { + case TransactionType.Reward: { const taskId = constructTaskIdentifier({ - tag: PendingTaskType.TipPickup, - walletTipId: parsedTx.walletTipId, + tag: PendingTaskType.RewardPickup, + walletRewardId: parsedTx.walletRewardId, }); await resetPendingTaskTimeout(ws, taskId); stopLongpolling(ws, taskId); @@ -1522,8 +1522,8 @@ export async function suspendTransaction( break; case TransactionType.Refund: throw Error("refund transactions can't be suspended or resumed"); - case TransactionType.Tip: - await suspendTipTransaction(ws, tx.walletTipId); + case TransactionType.Reward: + await suspendRewardTransaction(ws, tx.walletRewardId); break; default: assertUnreachable(tx); @@ -1551,8 +1551,8 @@ export async function failTransaction( return; case TransactionType.Refund: throw Error("can't do cancel-aborting on refund transaction"); - case TransactionType.Tip: - await failTipTransaction(ws, tx.walletTipId); + case TransactionType.Reward: + await failTipTransaction(ws, tx.walletRewardId); return; case TransactionType.Refresh: await failRefreshGroup(ws, tx.refreshGroupId); @@ -1613,8 +1613,8 @@ export async function resumeTransaction( break; case TransactionType.Refund: throw Error("refund transactions can't be suspended or resumed"); - case TransactionType.Tip: - await resumeTipTransaction(ws, tx.walletTipId); + case TransactionType.Reward: + await resumeTipTransaction(ws, tx.walletRewardId); break; } } @@ -1763,16 +1763,16 @@ export async function deleteTransaction( return; } - case TransactionType.Tip: { - const tipId = parsedTx.walletTipId; + case TransactionType.Reward: { + const tipId = parsedTx.walletRewardId; await ws.db - .mktx((x) => [x.tips, x.tombstones]) + .mktx((x) => [x.rewards, x.tombstones]) .runReadWrite(async (tx) => { - const tipRecord = await tx.tips.get(tipId); + const tipRecord = await tx.rewards.get(tipId); if (tipRecord) { - await tx.tips.delete(tipId); + await tx.rewards.delete(tipId); await tx.tombstones.put({ - id: TombstoneTag.DeleteTip + ":" + tipId, + id: TombstoneTag.DeleteReward + ":" + tipId, }); } }); @@ -1856,8 +1856,8 @@ export async function abortTransaction( case TransactionType.Deposit: await abortDepositGroup(ws, txId.depositGroupId); break; - case TransactionType.Tip: - await abortTipTransaction(ws, txId.walletTipId); + case TransactionType.Reward: + await abortTipTransaction(ws, txId.walletRewardId); break; case TransactionType.Refund: throw Error("can't abort refund transactions"); diff --git a/packages/taler-wallet-core/src/pending-types.ts b/packages/taler-wallet-core/src/pending-types.ts index 3bb6636ee..82eb542a7 100644 --- a/packages/taler-wallet-core/src/pending-types.ts +++ b/packages/taler-wallet-core/src/pending-types.ts @@ -33,7 +33,7 @@ export enum PendingTaskType { Purchase = "purchase", Refresh = "refresh", Recoup = "recoup", - TipPickup = "tip-pickup", + RewardPickup = "reward-pickup", Withdraw = "withdraw", Deposit = "deposit", Backup = "backup", @@ -144,7 +144,7 @@ export interface PendingRefreshTask { * The wallet is picking up a tip that the user has accepted. */ export interface PendingTipPickupTask { - type: PendingTaskType.TipPickup; + type: PendingTaskType.RewardPickup; tipId: string; merchantBaseUrl: string; merchantTipId: string; diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts index e395237cf..eaa99a6c3 100644 --- a/packages/taler-wallet-core/src/wallet-api-types.ts +++ b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -29,7 +29,7 @@ import { AcceptExchangeTosRequest, AcceptManualWithdrawalRequest, AcceptManualWithdrawalResult, - AcceptTipRequest, + AcceptRewardRequest, AcceptTipResponse, AcceptWithdrawalResponse, AddExchangeRequest, @@ -85,8 +85,8 @@ import { PreparePeerPushCreditRequest, PreparePeerPushCreditResponse, PrepareRefundRequest, - PrepareTipRequest, - PrepareTipResult, + PrepareRewardRequest as PrepareRewardRequest, + PrepareTipResult as PrepareRewardResult, RecoveryLoadRequest, RetryTransactionRequest, SetCoinSuspendedRequest, @@ -178,8 +178,8 @@ export enum WalletApiOperation { DumpCoins = "dumpCoins", SetCoinSuspended = "setCoinSuspended", ForceRefresh = "forceRefresh", - PrepareTip = "prepareTip", - AcceptTip = "acceptTip", + PrepareReward = "prepareReward", + AcceptReward = "acceptReward", ExportBackup = "exportBackup", AddBackupProvider = "addBackupProvider", RemoveBackupProvider = "removeBackupProvider", @@ -507,23 +507,23 @@ export type StartRefundQueryOp = { response: EmptyObject; }; -// group: Tipping +// group: Rewards /** - * Query and store information about a tip. + * Query and store information about a reward. */ export type PrepareTipOp = { - op: WalletApiOperation.PrepareTip; - request: PrepareTipRequest; - response: PrepareTipResult; + op: WalletApiOperation.PrepareReward; + request: PrepareRewardRequest; + response: PrepareRewardResult; }; /** - * Accept a tip. + * Accept a reward. */ export type AcceptTipOp = { - op: WalletApiOperation.AcceptTip; - request: AcceptTipRequest; + op: WalletApiOperation.AcceptReward; + request: AcceptRewardRequest; response: AcceptTipResponse; }; @@ -1023,8 +1023,8 @@ export type WalletOperations = { [WalletApiOperation.ForceRefresh]: ForceRefreshOp; [WalletApiOperation.DeleteTransaction]: DeleteTransactionOp; [WalletApiOperation.RetryTransaction]: RetryTransactionOp; - [WalletApiOperation.PrepareTip]: PrepareTipOp; - [WalletApiOperation.AcceptTip]: AcceptTipOp; + [WalletApiOperation.PrepareReward]: PrepareTipOp; + [WalletApiOperation.AcceptReward]: AcceptTipOp; [WalletApiOperation.StartRefundQueryForUri]: StartRefundQueryForUriOp; [WalletApiOperation.StartRefundQuery]: StartRefundQueryOp; [WalletApiOperation.ListCurrencies]: ListCurrenciesOp; diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index f8bbd21fc..4a83db856 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -93,7 +93,7 @@ import { codecForPreparePeerPullPaymentRequest, codecForPreparePeerPushCreditRequest, codecForPrepareRefundRequest, - codecForPrepareTipRequest, + codecForPrepareRewardRequest, codecForResumeTransaction, codecForRetryTransactionRequest, codecForSetCoinSuspendedRequest, @@ -249,10 +249,10 @@ import { } from "./operations/testing.js"; import { acceptTip, - computeTipTransactionStatus, + computeRewardTransactionStatus, prepareTip, processTip, -} from "./operations/tip.js"; +} from "./operations/reward.js"; import { abortTransaction, deleteTransaction, @@ -329,7 +329,7 @@ async function callOperationHandler( return await processRefreshGroup(ws, pending.refreshGroupId); case PendingTaskType.Withdraw: return await processWithdrawalGroup(ws, pending.withdrawalGroupId); - case PendingTaskType.TipPickup: + case PendingTaskType.RewardPickup: return await processTip(ws, pending.tipId); case PendingTaskType.Purchase: return await processPurchase(ws, pending.proposalId); @@ -1350,9 +1350,9 @@ async function dispatchRequestInternal( refreshGroupId, }; } - case WalletApiOperation.PrepareTip: { - const req = codecForPrepareTipRequest().decode(payload); - return await prepareTip(ws, req.talerTipUri); + case WalletApiOperation.PrepareReward: { + const req = codecForPrepareRewardRequest().decode(payload); + return await prepareTip(ws, req.talerRewardUri); } case WalletApiOperation.StartRefundQueryForUri: { const req = codecForPrepareRefundRequest().decode(payload); @@ -1370,9 +1370,9 @@ async function dispatchRequestInternal( await startQueryRefund(ws, txIdParsed.proposalId); return {}; } - case WalletApiOperation.AcceptTip: { + case WalletApiOperation.AcceptReward: { const req = codecForAcceptTipRequest().decode(payload); - return await acceptTip(ws, req.walletTipId); + return await acceptTip(ws, req.walletRewardId); } case WalletApiOperation.ExportBackupPlain: { return exportBackup(ws); @@ -1884,12 +1884,12 @@ class InternalWalletStateImpl implements InternalWalletState { } return computeRefreshTransactionState(rec); } - case TransactionType.Tip: { - const rec = await tx.tips.get(parsedTxId.walletTipId); + case TransactionType.Reward: { + const rec = await tx.rewards.get(parsedTxId.walletRewardId); if (!rec) { return undefined; } - return computeTipTransactionStatus(rec); + return computeRewardTransactionStatus(rec); } default: assertUnreachable(parsedTxId); -- cgit v1.2.3