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 --- .../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 +-- 8 files changed, 726 insertions(+), 724 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/src/operations') 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"); -- cgit v1.2.3 From 57e86b759e0376238f993fea1617609977fefb46 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 3 Aug 2023 19:03:24 +0200 Subject: rename tip->reward in URI --- packages/taler-util/src/taleruri.test.ts | 8 +++---- packages/taler-util/src/taleruri.ts | 28 +++++++++++----------- packages/taler-wallet-cli/src/index.ts | 2 +- .../taler-wallet-core/src/operations/reward.ts | 4 ++-- .../src/NavigationBar.tsx | 2 +- .../src/platform/chrome.ts | 2 +- .../src/popup/TalerActionFound.tsx | 2 +- .../src/wallet/AddNewActionView.tsx | 2 +- 8 files changed, 25 insertions(+), 25 deletions(-) (limited to 'packages/taler-wallet-core/src/operations') diff --git a/packages/taler-util/src/taleruri.test.ts b/packages/taler-util/src/taleruri.test.ts index 3244bbbd9..0dcf92252 100644 --- a/packages/taler-util/src/taleruri.test.ts +++ b/packages/taler-util/src/taleruri.test.ts @@ -21,7 +21,7 @@ import { parsePayUri, parseRefundUri, parseRestoreUri, - parseTipUri, + parseRewardUri, parseWithdrawExchangeUri, parseWithdrawUri, stringifyPayPushUri, @@ -161,7 +161,7 @@ test("taler refund uri parsing with instance", (t) => { test("taler tip pickup uri", (t) => { const url1 = "taler://tip/merchant.example.com/tipid"; - const r1 = parseTipUri(url1); + const r1 = parseRewardUri(url1); if (!r1) { t.fail(); return; @@ -171,7 +171,7 @@ test("taler tip pickup uri", (t) => { test("taler tip pickup uri with instance", (t) => { const url1 = "taler://tip/merchant.example.com/instances/tipm/tipid"; - const r1 = parseTipUri(url1); + const r1 = parseRewardUri(url1); if (!r1) { t.fail(); return; @@ -182,7 +182,7 @@ test("taler tip pickup uri with instance", (t) => { test("taler tip pickup uri with instance and prefix", (t) => { const url1 = "taler://tip/merchant.example.com/my/pfx/tipm/tipid"; - const r1 = parseTipUri(url1); + const r1 = parseRewardUri(url1); if (!r1) { t.fail(); return; diff --git a/packages/taler-util/src/taleruri.ts b/packages/taler-util/src/taleruri.ts index fc140811b..777cb5245 100644 --- a/packages/taler-util/src/taleruri.ts +++ b/packages/taler-util/src/taleruri.ts @@ -26,7 +26,7 @@ export type TalerUri = | PayPushUriResult | BackupRestoreUri | RefundUriResult - | TipUriResult + | RewardUriResult | WithdrawUriResult | ExchangeUri | WithdrawExchangeUri @@ -60,8 +60,8 @@ export interface RefundUriResult { orderId: string; } -export interface TipUriResult { - type: TalerUriAction.Tip; +export interface RewardUriResult { + type: TalerUriAction.Reward; merchantBaseUrl: string; merchantTipId: string; } @@ -167,7 +167,7 @@ export enum TalerUriAction { Pay = "pay", Withdraw = "withdraw", Refund = "refund", - Tip = "tip", + Reward = "reward", PayPull = "pay-pull", PayPush = "pay-push", PayTemplate = "pay-template", @@ -212,7 +212,7 @@ const parsers: { [A in TalerUriAction]: Parser } = { [TalerUriAction.PayTemplate]: parsePayTemplateUri, [TalerUriAction.Restore]: parseRestoreUri, [TalerUriAction.Refund]: parseRefundUri, - [TalerUriAction.Tip]: parseTipUri, + [TalerUriAction.Reward]: parseRewardUri, [TalerUriAction.Withdraw]: parseWithdrawUri, [TalerUriAction.DevExperiment]: parseDevExperimentUri, [TalerUriAction.Exchange]: parseExchangeUri, @@ -255,8 +255,8 @@ export function stringifyTalerUri(uri: TalerUri): string { case TalerUriAction.Refund: { return stringifyRefundUri(uri); } - case TalerUriAction.Tip: { - return stringifyTipUri(uri); + case TalerUriAction.Reward: { + return stringifyRewardUri(uri); } case TalerUriAction.Withdraw: { return stringifyWithdrawUri(uri); @@ -394,11 +394,11 @@ export function parsePayPullUri(s: string): PayPullUriResult | undefined { } /** - * Parse a taler[+http]://tip URI. + * Parse a taler[+http]://reward URI. * Return undefined if not passed a valid URI. */ -export function parseTipUri(s: string): TipUriResult | undefined { - const pi = parseProtoInfo(s, "tip"); +export function parseRewardUri(s: string): RewardUriResult | undefined { + const pi = parseProtoInfo(s, "reward"); if (!pi) { return undefined; } @@ -416,7 +416,7 @@ export function parseTipUri(s: string): TipUriResult | undefined { ); return { - type: TalerUriAction.Tip, + type: TalerUriAction.Reward, merchantBaseUrl, merchantTipId: tipId, }; @@ -699,12 +699,12 @@ export function stringifyRefundUri({ const { proto, path } = getUrlInfo(merchantBaseUrl); return `${proto}://refund/${path}${orderId}`; } -export function stringifyTipUri({ +export function stringifyRewardUri({ merchantBaseUrl, merchantTipId, -}: Omit): string { +}: Omit): string { const { proto, path } = getUrlInfo(merchantBaseUrl); - return `${proto}://tip/${path}${merchantTipId}`; + return `${proto}://reward/${path}${merchantTipId}`; } export function stringifyExchangeUri({ diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts index a34f6be03..e2787db66 100644 --- a/packages/taler-wallet-cli/src/index.ts +++ b/packages/taler-wallet-cli/src/index.ts @@ -651,7 +651,7 @@ walletCli alwaysYes: args.handleUri.autoYes, }); break; - case TalerUriAction.Tip: { + case TalerUriAction.Reward: { const res = await wallet.client.call(WalletApiOperation.PrepareReward, { talerRewardUri: uri, }); diff --git a/packages/taler-wallet-core/src/operations/reward.ts b/packages/taler-wallet-core/src/operations/reward.ts index 58c745780..47956f15f 100644 --- a/packages/taler-wallet-core/src/operations/reward.ts +++ b/packages/taler-wallet-core/src/operations/reward.ts @@ -31,7 +31,7 @@ import { j2s, Logger, NotificationType, - parseTipUri, + parseRewardUri, PrepareTipResult, TalerErrorCode, TalerPreciseTimestamp, @@ -141,7 +141,7 @@ export async function prepareTip( ws: InternalWalletState, talerTipUri: string, ): Promise { - const res = parseTipUri(talerTipUri); + const res = parseRewardUri(talerTipUri); if (!res) { throw Error("invalid taler://tip URI"); } diff --git a/packages/taler-wallet-webextension/src/NavigationBar.tsx b/packages/taler-wallet-webextension/src/NavigationBar.tsx index 231418861..167f1797c 100644 --- a/packages/taler-wallet-webextension/src/NavigationBar.tsx +++ b/packages/taler-wallet-webextension/src/NavigationBar.tsx @@ -146,7 +146,7 @@ const talerUriActionToPageName: { } = { [TalerUriAction.Withdraw]: "ctaWithdraw", [TalerUriAction.Pay]: "ctaPay", - [TalerUriAction.Tip]: "ctaTips", + [TalerUriAction.Reward]: "ctaTips", [TalerUriAction.Refund]: "ctaRefund", [TalerUriAction.PayPull]: "ctaInvoicePay", [TalerUriAction.PayPush]: "ctaTransferPickup", diff --git a/packages/taler-wallet-webextension/src/platform/chrome.ts b/packages/taler-wallet-webextension/src/platform/chrome.ts index 1295fa514..a50f225dc 100644 --- a/packages/taler-wallet-webextension/src/platform/chrome.ts +++ b/packages/taler-wallet-webextension/src/platform/chrome.ts @@ -279,7 +279,7 @@ function openWalletURIFromPopup(uri: TalerUri): void { `static/wallet.html#/cta/pay?talerUri=${encodeURIComponent(talerUri)}`, ); break; - case TalerUriAction.Tip: + case TalerUriAction.Reward: url = chrome.runtime.getURL( `static/wallet.html#/cta/tip?talerUri=${encodeURIComponent(talerUri)}`, ); diff --git a/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx b/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx index 6a0585907..e120334e8 100644 --- a/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx +++ b/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx @@ -65,7 +65,7 @@ function ContentByUriType({ ); - case TalerUriAction.Tip: + case TalerUriAction.Reward: return (

diff --git a/packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx b/packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx index fd9815401..fc3a0916c 100644 --- a/packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx +++ b/packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx @@ -66,7 +66,7 @@ export function AddNewActionView({ onCancel }: Props): VNode { return Open pay page; case TalerUriAction.Refund: return Open refund page; - case TalerUriAction.Tip: + case TalerUriAction.Reward: return Open tip page; case TalerUriAction.Withdraw: return Open withdraw page; -- cgit v1.2.3 From ee47aa4837fedcaa8257b57138ea34fda220d2b7 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 3 Aug 2023 19:24:04 +0200 Subject: fix integration tests Instead of using the deprecated runUntilDone, we now wait for specific notifications. The old way doesn't work, since p2p push transactions are not considered done until the counterparty has accepted the payment. --- .../integrationtests/test-age-restrictions-peer.ts | 66 +++++++++++++++------- .../src/integrationtests/test-peer-to-peer-pull.ts | 14 ++++- .../src/operations/pay-peer-pull-credit.ts | 11 +++- 3 files changed, 68 insertions(+), 23 deletions(-) (limited to 'packages/taler-wallet-core/src/operations') diff --git a/packages/taler-harness/src/integrationtests/test-age-restrictions-peer.ts b/packages/taler-harness/src/integrationtests/test-age-restrictions-peer.ts index eae04cd2e..d15858322 100644 --- a/packages/taler-harness/src/integrationtests/test-age-restrictions-peer.ts +++ b/packages/taler-harness/src/integrationtests/test-age-restrictions-peer.ts @@ -17,13 +17,20 @@ /** * Imports. */ -import { AbsoluteTime, Duration } from "@gnu-taler/taler-util"; +import { + AbsoluteTime, + Duration, + NotificationType, + TransactionMajorState, + TransactionMinorState, +} from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { defaultCoinConfig } from "../harness/denomStructures.js"; import { GlobalTestState, WalletCli } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironment, - withdrawViaBank, + createSimpleTestkudosEnvironmentV2, + createWalletDaemonWithClient, + withdrawViaBankV2, } from "../harness/helpers.js"; /** @@ -32,12 +39,7 @@ import { export async function runAgeRestrictionsPeerTest(t: GlobalTestState) { // Set up test environment - const { - wallet: walletOne, - bank, - exchange, - merchant, - } = await createSimpleTestkudosEnvironment( + const { bank, exchange } = await createSimpleTestkudosEnvironmentV2( t, defaultCoinConfig.map((x) => x("TESTKUDOS")), { @@ -45,20 +47,29 @@ export async function runAgeRestrictionsPeerTest(t: GlobalTestState) { }, ); - const walletTwo = new WalletCli(t, "walletTwo"); - const walletThree = new WalletCli(t, "walletThree"); + const w1 = await createWalletDaemonWithClient(t, { + name: "w1", + persistent: true, + }); + const w2 = await createWalletDaemonWithClient(t, { + name: "w2", + persistent: true, + }); - { - const wallet = walletOne; + const wallet1 = w1.walletClient; + const wallet2 = w2.walletClient; - await withdrawViaBank(t, { - wallet, + { + const withdrawalRes = await withdrawViaBankV2(t, { + walletClient: wallet1, bank, exchange, amount: "TESTKUDOS:20", restrictAge: 13, }); + await withdrawalRes.withdrawalFinishedCond; + const purse_expiration = AbsoluteTime.toProtocolTimestamp( AbsoluteTime.addDuration( AbsoluteTime.now(), @@ -66,7 +77,7 @@ export async function runAgeRestrictionsPeerTest(t: GlobalTestState) { ), ); - const initResp = await wallet.client.call( + const initResp = await wallet1.client.call( WalletApiOperation.InitiatePeerPushDebit, { partialContractTerms: { @@ -77,20 +88,35 @@ export async function runAgeRestrictionsPeerTest(t: GlobalTestState) { }, ); - await wallet.runUntilDone(); + const peerPushReadyCond = wallet1.waitForNotificationCond( + (x) => + x.type === NotificationType.TransactionStateTransition && + x.newTxState.major === TransactionMajorState.Pending && + x.newTxState.minor === TransactionMinorState.Ready && + x.transactionId === initResp.transactionId, + ); + + await peerPushReadyCond; - const checkResp = await walletTwo.client.call( + const checkResp = await wallet2.call( WalletApiOperation.PreparePeerPushCredit, { talerUri: initResp.talerUri, }, ); - await walletTwo.client.call(WalletApiOperation.ConfirmPeerPushCredit, { + await wallet2.call(WalletApiOperation.ConfirmPeerPushCredit, { peerPushPaymentIncomingId: checkResp.peerPushPaymentIncomingId, }); - await walletTwo.runUntilDone(); + const peerPullCreditDoneCond = wallet2.waitForNotificationCond( + (x) => + x.type === NotificationType.TransactionStateTransition && + x.newTxState.major === TransactionMajorState.Done && + x.transactionId === checkResp.transactionId, + ); + + await peerPullCreditDoneCond; } } diff --git a/packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts b/packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts index 30287b51b..5b55b1de1 100644 --- a/packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts +++ b/packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts @@ -23,6 +23,7 @@ import { j2s, NotificationType, TransactionMajorState, + TransactionMinorState, WalletNotification, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; @@ -46,12 +47,14 @@ export async function runPeerToPeerPullTest(t: GlobalTestState) { const w1 = await createWalletDaemonWithClient(t, { name: "w1", + persistent: true, handleNotification(wn) { allW1Notifications.push(wn); }, }); const w2 = await createWalletDaemonWithClient(t, { name: "w2", + persistent: true, handleNotification(wn) { allW2Notifications.push(wn); }, @@ -89,6 +92,15 @@ export async function runPeerToPeerPullTest(t: GlobalTestState) { }, ); + const peerPullCreditReadyCond = wallet1.waitForNotificationCond( + (x) => x.type === NotificationType.TransactionStateTransition && + x.transactionId === resp.transactionId && + x.newTxState.major === TransactionMajorState.Pending && + x.newTxState.minor === TransactionMinorState.Ready, + ); + + await peerPullCreditReadyCond; + const checkResp = await wallet2.client.call( WalletApiOperation.PreparePeerPullDebit, { @@ -98,8 +110,6 @@ export async function runPeerToPeerPullTest(t: GlobalTestState) { console.log(`checkResp: ${j2s(checkResp)}`); - // FIXME: The wallet should emit a more appropriate notification here. - // Yes, it's technically a withdrawal. const peerPullCreditDoneCond = wallet1.waitForNotificationCond( (x) => x.type === NotificationType.TransactionStateTransition && x.transactionId === resp.transactionId && diff --git a/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts index c7e13754f..ac501aae2 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts @@ -436,16 +436,25 @@ async function handlePeerPullCreditCreatePurse( logger.info(`reserve merge response: ${j2s(resp)}`); - await ws.db + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPullCredit, + pursePub: pullIni.pursePub, + }); + + const transitionInfo = await ws.db .mktx((x) => [x.peerPullPaymentInitiations]) .runReadWrite(async (tx) => { const pi2 = await tx.peerPullPaymentInitiations.get(pursePub); if (!pi2) { return; } + const oldTxState = computePeerPullCreditTransactionState(pi2); pi2.status = PeerPullPaymentInitiationStatus.PendingReady; await tx.peerPullPaymentInitiations.put(pi2); + const newTxState = computePeerPullCreditTransactionState(pi2); + return { oldTxState, newTxState }; }); + notifyTransition(ws, transactionId, transitionInfo); return TaskRunResult.finished(); } -- cgit v1.2.3