From efed6b32c5f4680f3175305985c4371517131927 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 2 Aug 2023 07:40:28 -0300 Subject: fix #7717 --- packages/taler-wallet-core/src/wallet.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) (limited to 'packages/taler-wallet-core/src') diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 628a55e92..1f5c6ee3b 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -310,6 +310,7 @@ import { WalletCoreApiClient, WalletCoreResponseType, } from "./wallet-api-types.js"; +import versionInfo from "./version.json" assert { type: 'json' }; const logger = new Logger("wallet.ts"); @@ -1016,12 +1017,6 @@ export async function getClientFromWalletState( return client; } -declare const __VERSION__: string; -declare const __GIT_HASH__: string; - -const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : "dev"; -const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined; - /** * Implementation of the "wallet-core" API. */ @@ -1590,15 +1585,15 @@ async function dispatchRequestInternal( } export function getVersion(ws: InternalWalletState): WalletCoreVersion { - const version: WalletCoreVersion = { - hash: GIT_HASH, - version: VERSION, + const result: WalletCoreVersion = { + hash: undefined, + version: versionInfo.version, exchange: WALLET_EXCHANGE_PROTOCOL_VERSION, merchant: WALLET_MERCHANT_PROTOCOL_VERSION, bank: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION, devMode: false, }; - return version; + return result; } /** -- cgit v1.2.3 From 22a3017d52f44e140971ce3791f7d75227c07f90 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 2 Aug 2023 09:49:14 -0300 Subject: removing import assert since it breaks with linaria --- packages/taler-wallet-core/src/wallet.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages/taler-wallet-core/src') diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 1f5c6ee3b..f8bbd21fc 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -310,7 +310,7 @@ import { WalletCoreApiClient, WalletCoreResponseType, } from "./wallet-api-types.js"; -import versionInfo from "./version.json" assert { type: 'json' }; +import versionInfo from "./version.json"; const logger = new Logger("wallet.ts"); -- cgit v1.2.3 From 475fdb502b30afd52fce82fb712e309565c01a0d Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 2 Aug 2023 16:27:54 +0200 Subject: organize imports --- packages/taler-wallet-core/src/crypto/cryptoImplementation.ts | 6 ------ 1 file changed, 6 deletions(-) (limited to 'packages/taler-wallet-core/src') diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts index 76c13bcb4..16d96eaa9 100644 --- a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts +++ b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts @@ -28,7 +28,6 @@ import { AgeCommitmentProof, AgeRestriction, AmountJson, - AmountLike, Amounts, AmountString, amountToBuffer, @@ -64,7 +63,6 @@ import { hashCoinPub, hashDenomPub, hashTruncate32, - j2s, kdf, kdfKw, keyExchangeEcdhEddsa, @@ -81,16 +79,13 @@ import { rsaVerify, setupTipPlanchet, stringToBytes, - TalerProtocolDuration, TalerProtocolTimestamp, TalerSignaturePurpose, timestampRoundedToBuffer, UnblindedSignature, - validateIban, WireFee, WithdrawalPlanchet, } from "@gnu-taler/taler-util"; -import bigint from "big-integer"; // FIXME: Crypto should not use DB Types! import { DenominationRecord } from "../db.js"; import { @@ -108,7 +103,6 @@ import { EncryptContractForDepositResponse, EncryptContractRequest, EncryptContractResponse, - EncryptedContract, SignDeletePurseRequest, SignDeletePurseResponse, SignPurseMergeRequest, -- cgit v1.2.3 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-harness/src/harness/harness.ts | 8 +- .../test-age-restrictions-merchant.ts | 8 +- .../src/integrationtests/test-tipping.ts | 14 +- packages/taler-util/src/backup-types.ts | 4 +- packages/taler-util/src/http-impl.node.ts | 22 +- packages/taler-util/src/merchant-api-types.ts | 14 +- packages/taler-util/src/transactions-types.ts | 8 +- packages/taler-util/src/wallet-types.ts | 38 +- packages/taler-wallet-cli/src/index.ts | 8 +- 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 +- .../src/components/HistoryItem.tsx | 2 +- .../src/cta/Reward/index.ts | 84 +++ .../src/cta/Reward/state.ts | 99 ++++ .../src/cta/Reward/stories.tsx | 46 ++ .../src/cta/Reward/test.ts | 228 ++++++++ .../src/cta/Reward/views.tsx | 89 +++ .../taler-wallet-webextension/src/cta/Tip/index.ts | 84 --- .../taler-wallet-webextension/src/cta/Tip/state.ts | 99 ---- .../src/cta/Tip/stories.tsx | 46 -- .../taler-wallet-webextension/src/cta/Tip/test.ts | 228 -------- .../src/cta/Tip/views.tsx | 89 --- .../src/cta/index.stories.ts | 2 +- .../src/wallet/Application.tsx | 2 +- .../src/wallet/History.stories.tsx | 6 +- .../src/wallet/Transaction.stories.tsx | 6 +- .../src/wallet/Transaction.tsx | 2 +- 37 files changed, 1403 insertions(+), 1399 deletions(-) create mode 100644 packages/taler-wallet-core/src/operations/reward.ts delete mode 100644 packages/taler-wallet-core/src/operations/tip.ts create mode 100644 packages/taler-wallet-webextension/src/cta/Reward/index.ts create mode 100644 packages/taler-wallet-webextension/src/cta/Reward/state.ts create mode 100644 packages/taler-wallet-webextension/src/cta/Reward/stories.tsx create mode 100644 packages/taler-wallet-webextension/src/cta/Reward/test.ts create mode 100644 packages/taler-wallet-webextension/src/cta/Reward/views.tsx delete mode 100644 packages/taler-wallet-webextension/src/cta/Tip/index.ts delete mode 100644 packages/taler-wallet-webextension/src/cta/Tip/state.ts delete mode 100644 packages/taler-wallet-webextension/src/cta/Tip/stories.tsx delete mode 100644 packages/taler-wallet-webextension/src/cta/Tip/test.ts delete mode 100644 packages/taler-wallet-webextension/src/cta/Tip/views.tsx (limited to 'packages/taler-wallet-core/src') diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts index 940e4258a..c9202c60e 100644 --- a/packages/taler-harness/src/harness/harness.ts +++ b/packages/taler-harness/src/harness/harness.ts @@ -51,8 +51,8 @@ import { stringToBytes, TalerError, TalerProtocolDuration, - TipCreateConfirmation, - TipCreateRequest, + RewardCreateConfirmation, + RewardCreateRequest, TippingReserveStatus, WalletNotification, } from "@gnu-taler/taler-util"; @@ -1751,8 +1751,8 @@ export namespace MerchantPrivateApi { export async function giveTip( merchantService: MerchantServiceInterface, instance: string, - req: TipCreateRequest, - ): Promise { + req: RewardCreateRequest, + ): Promise { const reqUrl = new URL( `private/tips`, merchantService.makeInstanceBaseUrl(instance), diff --git a/packages/taler-harness/src/integrationtests/test-age-restrictions-merchant.ts b/packages/taler-harness/src/integrationtests/test-age-restrictions-merchant.ts index c4db7022d..919097deb 100644 --- a/packages/taler-harness/src/integrationtests/test-age-restrictions-merchant.ts +++ b/packages/taler-harness/src/integrationtests/test-age-restrictions-merchant.ts @@ -191,12 +191,12 @@ export async function runAgeRestrictionsMerchantTest(t: GlobalTestState) { const walletTipping = new WalletCli(t, "age-tipping"); - const ptr = await walletTipping.client.call(WalletApiOperation.PrepareTip, { - talerTipUri: tip.taler_tip_uri, + const ptr = await walletTipping.client.call(WalletApiOperation.PrepareReward, { + talerRewardUri: tip.taler_reward_uri, }); - await walletTipping.client.call(WalletApiOperation.AcceptTip, { - walletTipId: ptr.walletTipId, + await walletTipping.client.call(WalletApiOperation.AcceptReward, { + walletRewardId: ptr.walletRewardId, }); await walletTipping.runUntilDone(); diff --git a/packages/taler-harness/src/integrationtests/test-tipping.ts b/packages/taler-harness/src/integrationtests/test-tipping.ts index ff6fc9ceb..332f702d7 100644 --- a/packages/taler-harness/src/integrationtests/test-tipping.ts +++ b/packages/taler-harness/src/integrationtests/test-tipping.ts @@ -99,17 +99,17 @@ export async function runTippingTest(t: GlobalTestState) { console.log("created tip", tip); const doTip = async (): Promise => { - const ptr = await wallet.client.call(WalletApiOperation.PrepareTip, { - talerTipUri: tip.taler_tip_uri, + const ptr = await wallet.client.call(WalletApiOperation.PrepareReward, { + talerRewardUri: tip.taler_reward_uri, }); console.log(ptr); - t.assertAmountEquals(ptr.tipAmountRaw, "TESTKUDOS:5"); - t.assertAmountEquals(ptr.tipAmountEffective, "TESTKUDOS:4.85"); + t.assertAmountEquals(ptr.rewardAmountRaw, "TESTKUDOS:5"); + t.assertAmountEquals(ptr.rewardAmountEffective, "TESTKUDOS:4.85"); - await wallet.client.call(WalletApiOperation.AcceptTip, { - walletTipId: ptr.walletTipId, + await wallet.client.call(WalletApiOperation.AcceptReward, { + walletRewardId: ptr.walletRewardId, }); await wallet.runUntilDone(); @@ -127,7 +127,7 @@ export async function runTippingTest(t: GlobalTestState) { console.log("Transactions:", JSON.stringify(txns, undefined, 2)); - t.assertDeepEqual(txns.transactions[0].type, "tip"); + t.assertDeepEqual(txns.transactions[0].type, "reward"); t.assertDeepEqual(txns.transactions[0].txState.major, TransactionMajorState.Done); t.assertAmountEquals( txns.transactions[0].amountEffective, diff --git a/packages/taler-util/src/backup-types.ts b/packages/taler-util/src/backup-types.ts index 2920838dd..0211ff740 100644 --- a/packages/taler-util/src/backup-types.ts +++ b/packages/taler-util/src/backup-types.ts @@ -499,7 +499,7 @@ export interface BackupRecoupGroup { export enum BackupCoinSourceType { Withdraw = "withdraw", Refresh = "refresh", - Tip = "tip", + Reward = "reward", } /** @@ -546,7 +546,7 @@ export interface BackupRefreshCoinSource { * Metadata about a coin obtained from a tip. */ export interface BackupTipCoinSource { - type: BackupCoinSourceType.Tip; + type: BackupCoinSourceType.Reward; /** * Wallet's identifier for the tip that this coin diff --git a/packages/taler-util/src/http-impl.node.ts b/packages/taler-util/src/http-impl.node.ts index 639043201..07648a28d 100644 --- a/packages/taler-util/src/http-impl.node.ts +++ b/packages/taler-util/src/http-impl.node.ts @@ -183,7 +183,16 @@ export class HttpLibImpl implements HttpRequestLibrary { resolve(resp); }); res.on("error", (e) => { - reject(e); + const err = TalerError.fromDetail( + TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR, + { + requestUrl: url, + requestMethod: method, + httpStatusCode: 0, + }, + `Error in HTTP response handler: ${e.message}`, + ); + reject(err); }); }; @@ -197,7 +206,16 @@ export class HttpLibImpl implements HttpRequestLibrary { } req.on("error", (e: Error) => { - reject(e); + const err = TalerError.fromDetail( + TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR, + { + requestUrl: url, + requestMethod: method, + httpStatusCode: 0, + }, + `Error in HTTP request: ${e.message}`, + ); + reject(err); }); if (reqBody) { diff --git a/packages/taler-util/src/merchant-api-types.ts b/packages/taler-util/src/merchant-api-types.ts index d7a5cf576..9f00173f2 100644 --- a/packages/taler-util/src/merchant-api-types.ts +++ b/packages/taler-util/src/merchant-api-types.ts @@ -290,22 +290,22 @@ export interface ReserveStatusEntry { active: boolean; } -export interface TipCreateConfirmation { +export interface RewardCreateConfirmation { // Unique tip identifier for the tip that was created. - tip_id: string; + reward_id: string; // taler://tip URI for the tip - taler_tip_uri: string; + taler_reward_uri: string; // URL that will directly trigger processing // the tip when the browser is redirected to it - tip_status_url: string; + reward_status_url: string; - // when does the tip expire - tip_expiration: AbsoluteTime; + // when does the reward expire + reward_expiration: AbsoluteTime; } -export interface TipCreateRequest { +export interface RewardCreateRequest { // Amount that the customer should be tipped amount: AmountString; diff --git a/packages/taler-util/src/transactions-types.ts b/packages/taler-util/src/transactions-types.ts index a498d3471..2d278e3e8 100644 --- a/packages/taler-util/src/transactions-types.ts +++ b/packages/taler-util/src/transactions-types.ts @@ -186,7 +186,7 @@ export type Transaction = | TransactionWithdrawal | TransactionPayment | TransactionRefund - | TransactionTip + | TransactionReward | TransactionRefresh | TransactionDeposit | TransactionPeerPullCredit @@ -201,7 +201,7 @@ export enum TransactionType { Payment = "payment", Refund = "refund", Refresh = "refresh", - Tip = "tip", + Reward = "reward", Deposit = "deposit", PeerPushDebit = "peer-push-debit", PeerPushCredit = "peer-push-credit", @@ -591,8 +591,8 @@ export interface TransactionRefund extends TransactionCommon { paymentInfo: RefundPaymentInfo | undefined; } -export interface TransactionTip extends TransactionCommon { - type: TransactionType.Tip; +export interface TransactionReward extends TransactionCommon { + type: TransactionType.Reward; // Raw amount of the tip, without extra fees that apply amountRaw: AmountString; diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts index 5151b8aae..38e5787ba 100644 --- a/packages/taler-util/src/wallet-types.ts +++ b/packages/taler-util/src/wallet-types.ts @@ -605,7 +605,7 @@ export interface PrepareTipResult { * * @deprecated use transactionId instead */ - walletTipId: string; + walletRewardId: string; /** * Tip transaction ID. @@ -620,13 +620,13 @@ export interface PrepareTipResult { /** * Amount that the merchant gave. */ - tipAmountRaw: AmountString; + rewardAmountRaw: AmountString; /** * Amount that arrived at the wallet. * Might be lower than the raw amount due to fees. */ - tipAmountEffective: AmountString; + rewardAmountEffective: AmountString; /** * Base URL of the merchant backend giving then tip. @@ -654,14 +654,14 @@ export interface AcceptTipResponse { export const codecForPrepareTipResult = (): Codec => buildCodecForObject() .property("accepted", codecForBoolean()) - .property("tipAmountRaw", codecForAmountString()) - .property("tipAmountEffective", codecForAmountString()) + .property("rewardAmountRaw", codecForAmountString()) + .property("rewardAmountEffective", codecForAmountString()) .property("exchangeBaseUrl", codecForString()) .property("merchantBaseUrl", codecForString()) .property("expirationTimestamp", codecForTimestamp) - .property("walletTipId", codecForString()) + .property("walletRewardId", codecForString()) .property("transactionId", codecForString()) - .build("PrepareTipResult"); + .build("PrepareRewardResult"); export interface BenchmarkResult { time: { [s: string]: number }; @@ -1933,23 +1933,23 @@ export const codecForStartRefundQueryRequest = .property("transactionId", codecForTransactionIdStr()) .build("StartRefundQueryRequest"); -export interface PrepareTipRequest { - talerTipUri: string; +export interface PrepareRewardRequest { + talerRewardUri: string; } -export const codecForPrepareTipRequest = (): Codec => - buildCodecForObject() - .property("talerTipUri", codecForString()) - .build("PrepareTipRequest"); +export const codecForPrepareRewardRequest = (): Codec => + buildCodecForObject() + .property("talerRewardUri", codecForString()) + .build("PrepareRewardRequest"); -export interface AcceptTipRequest { - walletTipId: string; +export interface AcceptRewardRequest { + walletRewardId: string; } -export const codecForAcceptTipRequest = (): Codec => - buildCodecForObject() - .property("walletTipId", codecForString()) - .build("AcceptTipRequest"); +export const codecForAcceptTipRequest = (): Codec => + buildCodecForObject() + .property("walletRewardId", codecForString()) + .build("AcceptRewardRequest"); export interface FailTransactionRequest { transactionId: TransactionIdStr; diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts index 4e56a40f4..a34f6be03 100644 --- a/packages/taler-wallet-cli/src/index.ts +++ b/packages/taler-wallet-cli/src/index.ts @@ -652,12 +652,12 @@ walletCli }); break; case TalerUriAction.Tip: { - const res = await wallet.client.call(WalletApiOperation.PrepareTip, { - talerTipUri: uri, + const res = await wallet.client.call(WalletApiOperation.PrepareReward, { + talerRewardUri: uri, }); console.log("tip status", res); - await wallet.client.call(WalletApiOperation.AcceptTip, { - walletTipId: res.walletTipId, + await wallet.client.call(WalletApiOperation.AcceptReward, { + walletRewardId: res.walletRewardId, }); break; } 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); diff --git a/packages/taler-wallet-webextension/src/components/HistoryItem.tsx b/packages/taler-wallet-webextension/src/components/HistoryItem.tsx index a0ce04460..e072d2581 100644 --- a/packages/taler-wallet-webextension/src/components/HistoryItem.tsx +++ b/packages/taler-wallet-webextension/src/components/HistoryItem.tsx @@ -134,7 +134,7 @@ export function HistoryItem(props: { tx: Transaction }): VNode { } /> ); - case TransactionType.Tip: + case TransactionType.Reward: return ( + */ + +import { AmountJson } from "@gnu-taler/taler-util"; +import { ErrorAlertView } from "../../components/CurrentAlerts.js"; +import { Loading } from "../../components/Loading.js"; +import { ErrorAlert } from "../../context/alert.js"; +import { ButtonHandler } from "../../mui/handlers.js"; +import { compose, StateViewMap } from "../../utils/index.js"; +import { useComponentState } from "./state.js"; +import { AcceptedView, IgnoredView, ReadyView } from "./views.js"; + +export interface Props { + talerTipUri?: string; + onCancel: () => Promise; + onSuccess: (tx: string) => Promise; +} + +export type State = + | State.Loading + | State.LoadingUriError + | State.Ignored + | State.Accepted + | State.Ready + | State.Ignored; + +export namespace State { + export interface Loading { + status: "loading"; + error: undefined; + } + + export interface LoadingUriError { + status: "error"; + error: ErrorAlert; + } + + export interface BaseInfo { + merchantBaseUrl: string; + amount: AmountJson; + exchangeBaseUrl: string; + error: undefined; + cancel: ButtonHandler; + } + + export interface Ignored extends BaseInfo { + status: "ignored"; + } + + export interface Accepted extends BaseInfo { + status: "accepted"; + } + export interface Ready extends BaseInfo { + status: "ready"; + accept: ButtonHandler; + } +} + +const viewMapping: StateViewMap = { + loading: Loading, + error: ErrorAlertView, + accepted: AcceptedView, + ignored: IgnoredView, + ready: ReadyView, +}; + +export const TipPage = compose( + "Tip", + (p: Props) => useComponentState(p), + viewMapping, +); diff --git a/packages/taler-wallet-webextension/src/cta/Reward/state.ts b/packages/taler-wallet-webextension/src/cta/Reward/state.ts new file mode 100644 index 000000000..98463cb60 --- /dev/null +++ b/packages/taler-wallet-webextension/src/cta/Reward/state.ts @@ -0,0 +1,99 @@ +/* + This file is part of GNU Taler + (C) 2022 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 + */ + +import { Amounts } from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { alertFromError, useAlertContext } from "../../context/alert.js"; +import { useBackendContext } from "../../context/backend.js"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; +import { Props, State } from "./index.js"; + +export function useComponentState({ + talerTipUri: talerRewardUri, + onCancel, + onSuccess, +}: Props): State { + const api = useBackendContext(); + const { i18n } = useTranslationContext(); + const { pushAlertOnError } = useAlertContext(); + const tipInfo = useAsyncAsHook(async () => { + if (!talerRewardUri) throw Error("ERROR_NO-URI-FOR-TIP"); + const tip = await api.wallet.call(WalletApiOperation.PrepareReward, { + talerRewardUri, + }); + return { tip }; + }); + + if (!tipInfo) { + return { + status: "loading", + error: undefined, + }; + } + if (tipInfo.hasError) { + return { + status: "error", + error: alertFromError( + i18n.str`Could not load the status of the term of service`, + tipInfo, + ), + }; + } + // if (tipInfo.hasError) { + // return { + // status: "loading-uri", + // error: tipInfo, + // }; + // } + + const { tip } = tipInfo.response; + + const doAccept = async (): Promise => { + const res = await api.wallet.call(WalletApiOperation.AcceptReward, { + walletRewardId: tip.walletRewardId, + }); + + //FIX: this may not be seen since we are moving to the success also + tipInfo.retry(); + onSuccess(res.transactionId); + }; + + const baseInfo = { + merchantBaseUrl: tip.merchantBaseUrl, + exchangeBaseUrl: tip.exchangeBaseUrl, + amount: Amounts.parseOrThrow(tip.rewardAmountEffective), + error: undefined, + cancel: { + onClick: pushAlertOnError(onCancel), + }, + }; + + if (tip.accepted) { + return { + status: "accepted", + ...baseInfo, + }; + } + + return { + status: "ready", + ...baseInfo, + accept: { + onClick: pushAlertOnError(doAccept), + }, + }; +} diff --git a/packages/taler-wallet-webextension/src/cta/Reward/stories.tsx b/packages/taler-wallet-webextension/src/cta/Reward/stories.tsx new file mode 100644 index 000000000..bd5fdefd9 --- /dev/null +++ b/packages/taler-wallet-webextension/src/cta/Reward/stories.tsx @@ -0,0 +1,46 @@ +/* + This file is part of GNU Taler + (C) 2022 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 + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { Amounts } from "@gnu-taler/taler-util"; +import * as tests from "@gnu-taler/web-util/testing"; +import { AcceptedView, ReadyView } from "./views.js"; + +export default { + title: "tip", +}; + +export const Accepted = tests.createExample(AcceptedView, { + status: "accepted", + error: undefined, + amount: Amounts.parseOrThrow("EUR:1"), + exchangeBaseUrl: "", + merchantBaseUrl: "", +}); + +export const Ready = tests.createExample(ReadyView, { + status: "ready", + error: undefined, + amount: Amounts.parseOrThrow("EUR:1"), + merchantBaseUrl: "http://merchant.url/", + exchangeBaseUrl: "http://exchange.url/", + accept: {}, + cancel: {}, +}); diff --git a/packages/taler-wallet-webextension/src/cta/Reward/test.ts b/packages/taler-wallet-webextension/src/cta/Reward/test.ts new file mode 100644 index 000000000..6d7bad0c8 --- /dev/null +++ b/packages/taler-wallet-webextension/src/cta/Reward/test.ts @@ -0,0 +1,228 @@ +/* + This file is part of GNU Taler + (C) 2022 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 + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { Amounts } from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { expect } from "chai"; +import * as tests from "@gnu-taler/web-util/testing"; +import { nullFunction } from "../../mui/handlers.js"; +import { createWalletApiMock } from "../../test-utils.js"; +import { Props } from "./index.js"; +import { useComponentState } from "./state.js"; + +describe("Tip CTA states", () => { + it("should tell the user that the URI is missing", async () => { + const { handler, TestingContext } = createWalletApiMock(); + + const props: Props = { + talerTipUri: undefined, + onCancel: nullFunction, + onSuccess: nullFunction, + }; + + const hookBehavior = await tests.hookBehaveLikeThis( + useComponentState, + props, + [ + ({ status, error }) => { + expect(status).equals("loading"); + expect(error).undefined; + }, + ({ status, error }) => { + expect(status).equals("error"); + if (!error) expect.fail(); + expect(error.description).eq("ERROR_NO-URI-FOR-TIP"); + }, + ], + TestingContext, + ); + + expect(hookBehavior).deep.equal({ result: "ok" }); + expect(handler.getCallingQueueState()).eq("empty"); + }); + + it("should be ready for accepting the tip", async () => { + const { handler, TestingContext } = createWalletApiMock(); + + handler.addWalletCallResponse(WalletApiOperation.PrepareReward, undefined, { + accepted: false, + exchangeBaseUrl: "exchange url", + merchantBaseUrl: "merchant url", + rewardAmountEffective: "EUR:1", + walletRewardId: "tip_id", + transactionId: "txn:tip:ABC1234", + expirationTimestamp: { + t_s: 1, + }, + rewardAmountRaw: "", + }); + + const props: Props = { + talerTipUri: "taler://tip/asd", + onCancel: nullFunction, + onSuccess: nullFunction, + }; + + const hookBehavior = await tests.hookBehaveLikeThis( + useComponentState, + props, + [ + ({ status, error }) => { + expect(status).equals("loading"); + expect(error).undefined; + }, + (state) => { + if (state.status !== "ready") { + expect(state).eq({ status: "ready" }); + return; + } + if (state.error) expect.fail(); + expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1")); + expect(state.merchantBaseUrl).eq("merchant url"); + expect(state.exchangeBaseUrl).eq("exchange url"); + if (state.accept.onClick === undefined) expect.fail(); + + handler.addWalletCallResponse(WalletApiOperation.AcceptReward); + state.accept.onClick(); + + handler.addWalletCallResponse( + WalletApiOperation.PrepareReward, + undefined, + { + accepted: true, + exchangeBaseUrl: "exchange url", + merchantBaseUrl: "merchant url", + rewardAmountEffective: "EUR:1", + walletRewardId: "tip_id", + transactionId: "txn:tip:ABC1234", + expirationTimestamp: { + t_s: 1, + }, + rewardAmountRaw: "", + }, + ); + }, + (state) => { + if (state.status !== "accepted") expect.fail(); + if (state.error) expect.fail(); + expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1")); + expect(state.merchantBaseUrl).eq("merchant url"); + expect(state.exchangeBaseUrl).eq("exchange url"); + }, + ], + TestingContext, + ); + + expect(hookBehavior).deep.equal({ result: "ok" }); + expect(handler.getCallingQueueState()).eq("empty"); + }); + + it.skip("should be ignored after clicking the ignore button", async () => { + const { handler, TestingContext } = createWalletApiMock(); + handler.addWalletCallResponse(WalletApiOperation.PrepareReward, undefined, { + exchangeBaseUrl: "exchange url", + merchantBaseUrl: "merchant url", + rewardAmountEffective: "EUR:1", + walletRewardId: "tip_id", + transactionId: "txn:tip:ABC1234", + accepted: false, + expirationTimestamp: { + t_s: 1, + }, + rewardAmountRaw: "", + }); + + const props: Props = { + talerTipUri: "taler://tip/asd", + onCancel: nullFunction, + onSuccess: nullFunction, + }; + + const hookBehavior = await tests.hookBehaveLikeThis( + useComponentState, + props, + [ + ({ status, error }) => { + expect(status).equals("loading"); + expect(error).undefined; + }, + (state) => { + if (state.status !== "ready") expect.fail(); + if (state.error) expect.fail(); + expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1")); + expect(state.merchantBaseUrl).eq("merchant url"); + expect(state.exchangeBaseUrl).eq("exchange url"); + + //FIXME: add ignore button + }, + ], + TestingContext, + ); + + expect(hookBehavior).deep.equal({ result: "ok" }); + expect(handler.getCallingQueueState()).eq("empty"); + }); + + it("should render accepted if the tip has been used previously", async () => { + const { handler, TestingContext } = createWalletApiMock(); + + handler.addWalletCallResponse(WalletApiOperation.PrepareReward, undefined, { + accepted: true, + exchangeBaseUrl: "exchange url", + merchantBaseUrl: "merchant url", + rewardAmountEffective: "EUR:1", + walletRewardId: "tip_id", + transactionId: "txn:tip:ABC1234", + expirationTimestamp: { + t_s: 1, + }, + rewardAmountRaw: "", + }); + + const props: Props = { + talerTipUri: "taler://tip/asd", + onCancel: nullFunction, + onSuccess: nullFunction, + }; + + const hookBehavior = await tests.hookBehaveLikeThis( + useComponentState, + props, + [ + ({ status, error }) => { + expect(status).equals("loading"); + expect(error).undefined; + }, + (state) => { + if (state.status !== "accepted") expect.fail(); + if (state.error) expect.fail(); + expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1")); + expect(state.merchantBaseUrl).eq("merchant url"); + expect(state.exchangeBaseUrl).eq("exchange url"); + }, + ], + TestingContext, + ); + + expect(hookBehavior).deep.equal({ result: "ok" }); + expect(handler.getCallingQueueState()).eq("empty"); + }); +}); diff --git a/packages/taler-wallet-webextension/src/cta/Reward/views.tsx b/packages/taler-wallet-webextension/src/cta/Reward/views.tsx new file mode 100644 index 000000000..5d99a0132 --- /dev/null +++ b/packages/taler-wallet-webextension/src/cta/Reward/views.tsx @@ -0,0 +1,89 @@ +/* + This file is part of GNU Taler + (C) 2022 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 + */ + +import { TranslatedString } from "@gnu-taler/taler-util"; +import { Fragment, h, VNode } from "preact"; +import { Amount } from "../../components/Amount.js"; +import { LogoHeader } from "../../components/LogoHeader.js"; +import { Part } from "../../components/Part.js"; +import { Link, SubTitle, WalletAction } from "../../components/styled/index.js"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Button } from "../../mui/Button.js"; +import { State } from "./index.js"; + +export function IgnoredView(state: State.Ignored): VNode { + const { i18n } = useTranslationContext(); + return ( + + + You've ignored the tip. + + + ); +} + +export function ReadyView(state: State.Ready): VNode { + const { i18n } = useTranslationContext(); + return ( + +
+

+ The merchant is offering you a tip +

+ } + kind="positive" + /> + + +
+
+ +
+
+ ); +} + +export function AcceptedView(state: State.Accepted): VNode { + const { i18n } = useTranslationContext(); + return ( + +
+ + Tip from {state.merchantBaseUrl} accepted. Check your + transactions list for more details. + +
+
+ ); +} diff --git a/packages/taler-wallet-webextension/src/cta/Tip/index.ts b/packages/taler-wallet-webextension/src/cta/Tip/index.ts deleted file mode 100644 index 5e56db7bc..000000000 --- a/packages/taler-wallet-webextension/src/cta/Tip/index.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022 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 - */ - -import { AmountJson } from "@gnu-taler/taler-util"; -import { ErrorAlertView } from "../../components/CurrentAlerts.js"; -import { Loading } from "../../components/Loading.js"; -import { ErrorAlert } from "../../context/alert.js"; -import { ButtonHandler } from "../../mui/handlers.js"; -import { compose, StateViewMap } from "../../utils/index.js"; -import { useComponentState } from "./state.js"; -import { AcceptedView, IgnoredView, ReadyView } from "./views.js"; - -export interface Props { - talerTipUri?: string; - onCancel: () => Promise; - onSuccess: (tx: string) => Promise; -} - -export type State = - | State.Loading - | State.LoadingUriError - | State.Ignored - | State.Accepted - | State.Ready - | State.Ignored; - -export namespace State { - export interface Loading { - status: "loading"; - error: undefined; - } - - export interface LoadingUriError { - status: "error"; - error: ErrorAlert; - } - - export interface BaseInfo { - merchantBaseUrl: string; - amount: AmountJson; - exchangeBaseUrl: string; - error: undefined; - cancel: ButtonHandler; - } - - export interface Ignored extends BaseInfo { - status: "ignored"; - } - - export interface Accepted extends BaseInfo { - status: "accepted"; - } - export interface Ready extends BaseInfo { - status: "ready"; - accept: ButtonHandler; - } -} - -const viewMapping: StateViewMap = { - loading: Loading, - error: ErrorAlertView, - accepted: AcceptedView, - ignored: IgnoredView, - ready: ReadyView, -}; - -export const TipPage = compose( - "Tip", - (p: Props) => useComponentState(p), - viewMapping, -); diff --git a/packages/taler-wallet-webextension/src/cta/Tip/state.ts b/packages/taler-wallet-webextension/src/cta/Tip/state.ts deleted file mode 100644 index d34e9e264..000000000 --- a/packages/taler-wallet-webextension/src/cta/Tip/state.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022 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 - */ - -import { Amounts } from "@gnu-taler/taler-util"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { alertFromError, useAlertContext } from "../../context/alert.js"; -import { useBackendContext } from "../../context/backend.js"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; -import { Props, State } from "./index.js"; - -export function useComponentState({ - talerTipUri, - onCancel, - onSuccess, -}: Props): State { - const api = useBackendContext(); - const { i18n } = useTranslationContext(); - const { pushAlertOnError } = useAlertContext(); - const tipInfo = useAsyncAsHook(async () => { - if (!talerTipUri) throw Error("ERROR_NO-URI-FOR-TIP"); - const tip = await api.wallet.call(WalletApiOperation.PrepareTip, { - talerTipUri, - }); - return { tip }; - }); - - if (!tipInfo) { - return { - status: "loading", - error: undefined, - }; - } - if (tipInfo.hasError) { - return { - status: "error", - error: alertFromError( - i18n.str`Could not load the status of the term of service`, - tipInfo, - ), - }; - } - // if (tipInfo.hasError) { - // return { - // status: "loading-uri", - // error: tipInfo, - // }; - // } - - const { tip } = tipInfo.response; - - const doAccept = async (): Promise => { - const res = await api.wallet.call(WalletApiOperation.AcceptTip, { - walletTipId: tip.walletTipId, - }); - - //FIX: this may not be seen since we are moving to the success also - tipInfo.retry(); - onSuccess(res.transactionId); - }; - - const baseInfo = { - merchantBaseUrl: tip.merchantBaseUrl, - exchangeBaseUrl: tip.exchangeBaseUrl, - amount: Amounts.parseOrThrow(tip.tipAmountEffective), - error: undefined, - cancel: { - onClick: pushAlertOnError(onCancel), - }, - }; - - if (tip.accepted) { - return { - status: "accepted", - ...baseInfo, - }; - } - - return { - status: "ready", - ...baseInfo, - accept: { - onClick: pushAlertOnError(doAccept), - }, - }; -} diff --git a/packages/taler-wallet-webextension/src/cta/Tip/stories.tsx b/packages/taler-wallet-webextension/src/cta/Tip/stories.tsx deleted file mode 100644 index bd5fdefd9..000000000 --- a/packages/taler-wallet-webextension/src/cta/Tip/stories.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022 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 - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { Amounts } from "@gnu-taler/taler-util"; -import * as tests from "@gnu-taler/web-util/testing"; -import { AcceptedView, ReadyView } from "./views.js"; - -export default { - title: "tip", -}; - -export const Accepted = tests.createExample(AcceptedView, { - status: "accepted", - error: undefined, - amount: Amounts.parseOrThrow("EUR:1"), - exchangeBaseUrl: "", - merchantBaseUrl: "", -}); - -export const Ready = tests.createExample(ReadyView, { - status: "ready", - error: undefined, - amount: Amounts.parseOrThrow("EUR:1"), - merchantBaseUrl: "http://merchant.url/", - exchangeBaseUrl: "http://exchange.url/", - accept: {}, - cancel: {}, -}); diff --git a/packages/taler-wallet-webextension/src/cta/Tip/test.ts b/packages/taler-wallet-webextension/src/cta/Tip/test.ts deleted file mode 100644 index e0b6210a2..000000000 --- a/packages/taler-wallet-webextension/src/cta/Tip/test.ts +++ /dev/null @@ -1,228 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022 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 - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { Amounts } from "@gnu-taler/taler-util"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { expect } from "chai"; -import * as tests from "@gnu-taler/web-util/testing"; -import { nullFunction } from "../../mui/handlers.js"; -import { createWalletApiMock } from "../../test-utils.js"; -import { Props } from "./index.js"; -import { useComponentState } from "./state.js"; - -describe("Tip CTA states", () => { - it("should tell the user that the URI is missing", async () => { - const { handler, TestingContext } = createWalletApiMock(); - - const props: Props = { - talerTipUri: undefined, - onCancel: nullFunction, - onSuccess: nullFunction, - }; - - const hookBehavior = await tests.hookBehaveLikeThis( - useComponentState, - props, - [ - ({ status, error }) => { - expect(status).equals("loading"); - expect(error).undefined; - }, - ({ status, error }) => { - expect(status).equals("error"); - if (!error) expect.fail(); - expect(error.description).eq("ERROR_NO-URI-FOR-TIP"); - }, - ], - TestingContext, - ); - - expect(hookBehavior).deep.equal({ result: "ok" }); - expect(handler.getCallingQueueState()).eq("empty"); - }); - - it("should be ready for accepting the tip", async () => { - const { handler, TestingContext } = createWalletApiMock(); - - handler.addWalletCallResponse(WalletApiOperation.PrepareTip, undefined, { - accepted: false, - exchangeBaseUrl: "exchange url", - merchantBaseUrl: "merchant url", - tipAmountEffective: "EUR:1", - walletTipId: "tip_id", - transactionId: "txn:tip:ABC1234", - expirationTimestamp: { - t_s: 1, - }, - tipAmountRaw: "", - }); - - const props: Props = { - talerTipUri: "taler://tip/asd", - onCancel: nullFunction, - onSuccess: nullFunction, - }; - - const hookBehavior = await tests.hookBehaveLikeThis( - useComponentState, - props, - [ - ({ status, error }) => { - expect(status).equals("loading"); - expect(error).undefined; - }, - (state) => { - if (state.status !== "ready") { - expect(state).eq({ status: "ready" }); - return; - } - if (state.error) expect.fail(); - expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1")); - expect(state.merchantBaseUrl).eq("merchant url"); - expect(state.exchangeBaseUrl).eq("exchange url"); - if (state.accept.onClick === undefined) expect.fail(); - - handler.addWalletCallResponse(WalletApiOperation.AcceptTip); - state.accept.onClick(); - - handler.addWalletCallResponse( - WalletApiOperation.PrepareTip, - undefined, - { - accepted: true, - exchangeBaseUrl: "exchange url", - merchantBaseUrl: "merchant url", - tipAmountEffective: "EUR:1", - walletTipId: "tip_id", - transactionId: "txn:tip:ABC1234", - expirationTimestamp: { - t_s: 1, - }, - tipAmountRaw: "", - }, - ); - }, - (state) => { - if (state.status !== "accepted") expect.fail(); - if (state.error) expect.fail(); - expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1")); - expect(state.merchantBaseUrl).eq("merchant url"); - expect(state.exchangeBaseUrl).eq("exchange url"); - }, - ], - TestingContext, - ); - - expect(hookBehavior).deep.equal({ result: "ok" }); - expect(handler.getCallingQueueState()).eq("empty"); - }); - - it.skip("should be ignored after clicking the ignore button", async () => { - const { handler, TestingContext } = createWalletApiMock(); - handler.addWalletCallResponse(WalletApiOperation.PrepareTip, undefined, { - exchangeBaseUrl: "exchange url", - merchantBaseUrl: "merchant url", - tipAmountEffective: "EUR:1", - walletTipId: "tip_id", - transactionId: "txn:tip:ABC1234", - accepted: false, - expirationTimestamp: { - t_s: 1, - }, - tipAmountRaw: "", - }); - - const props: Props = { - talerTipUri: "taler://tip/asd", - onCancel: nullFunction, - onSuccess: nullFunction, - }; - - const hookBehavior = await tests.hookBehaveLikeThis( - useComponentState, - props, - [ - ({ status, error }) => { - expect(status).equals("loading"); - expect(error).undefined; - }, - (state) => { - if (state.status !== "ready") expect.fail(); - if (state.error) expect.fail(); - expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1")); - expect(state.merchantBaseUrl).eq("merchant url"); - expect(state.exchangeBaseUrl).eq("exchange url"); - - //FIXME: add ignore button - }, - ], - TestingContext, - ); - - expect(hookBehavior).deep.equal({ result: "ok" }); - expect(handler.getCallingQueueState()).eq("empty"); - }); - - it("should render accepted if the tip has been used previously", async () => { - const { handler, TestingContext } = createWalletApiMock(); - - handler.addWalletCallResponse(WalletApiOperation.PrepareTip, undefined, { - accepted: true, - exchangeBaseUrl: "exchange url", - merchantBaseUrl: "merchant url", - tipAmountEffective: "EUR:1", - walletTipId: "tip_id", - transactionId: "txn:tip:ABC1234", - expirationTimestamp: { - t_s: 1, - }, - tipAmountRaw: "", - }); - - const props: Props = { - talerTipUri: "taler://tip/asd", - onCancel: nullFunction, - onSuccess: nullFunction, - }; - - const hookBehavior = await tests.hookBehaveLikeThis( - useComponentState, - props, - [ - ({ status, error }) => { - expect(status).equals("loading"); - expect(error).undefined; - }, - (state) => { - if (state.status !== "accepted") expect.fail(); - if (state.error) expect.fail(); - expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1")); - expect(state.merchantBaseUrl).eq("merchant url"); - expect(state.exchangeBaseUrl).eq("exchange url"); - }, - ], - TestingContext, - ); - - expect(hookBehavior).deep.equal({ result: "ok" }); - expect(handler.getCallingQueueState()).eq("empty"); - }); -}); diff --git a/packages/taler-wallet-webextension/src/cta/Tip/views.tsx b/packages/taler-wallet-webextension/src/cta/Tip/views.tsx deleted file mode 100644 index 5d99a0132..000000000 --- a/packages/taler-wallet-webextension/src/cta/Tip/views.tsx +++ /dev/null @@ -1,89 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022 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 - */ - -import { TranslatedString } from "@gnu-taler/taler-util"; -import { Fragment, h, VNode } from "preact"; -import { Amount } from "../../components/Amount.js"; -import { LogoHeader } from "../../components/LogoHeader.js"; -import { Part } from "../../components/Part.js"; -import { Link, SubTitle, WalletAction } from "../../components/styled/index.js"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Button } from "../../mui/Button.js"; -import { State } from "./index.js"; - -export function IgnoredView(state: State.Ignored): VNode { - const { i18n } = useTranslationContext(); - return ( - - - You've ignored the tip. - - - ); -} - -export function ReadyView(state: State.Ready): VNode { - const { i18n } = useTranslationContext(); - return ( - -
-

- The merchant is offering you a tip -

- } - kind="positive" - /> - - -
-
- -
-
- ); -} - -export function AcceptedView(state: State.Accepted): VNode { - const { i18n } = useTranslationContext(); - return ( - -
- - Tip from {state.merchantBaseUrl} accepted. Check your - transactions list for more details. - -
-
- ); -} diff --git a/packages/taler-wallet-webextension/src/cta/index.stories.ts b/packages/taler-wallet-webextension/src/cta/index.stories.ts index 84863f84f..06b11ef6d 100644 --- a/packages/taler-wallet-webextension/src/cta/index.stories.ts +++ b/packages/taler-wallet-webextension/src/cta/index.stories.ts @@ -22,7 +22,7 @@ export * as a1 from "./Deposit/stories.jsx"; export * as a3 from "./Payment/stories.jsx"; export * as a4 from "./Refund/stories.jsx"; -export * as a5 from "./Tip/stories.jsx"; +export * as a5 from "./Reward/stories.js"; export * as a6 from "./Withdraw/stories.jsx"; export * as a8 from "./InvoiceCreate/stories.js"; export * as a9 from "./InvoicePay/stories.js"; diff --git a/packages/taler-wallet-webextension/src/wallet/Application.tsx b/packages/taler-wallet-webextension/src/wallet/Application.tsx index d8cb22bf0..98515aac0 100644 --- a/packages/taler-wallet-webextension/src/wallet/Application.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Application.tsx @@ -58,7 +58,7 @@ import { PaymentPage } from "../cta/Payment/index.js"; import { PaymentTemplatePage } from "../cta/PaymentTemplate/index.js"; import { RecoveryPage } from "../cta/Recovery/index.js"; import { RefundPage } from "../cta/Refund/index.js"; -import { TipPage } from "../cta/Tip/index.js"; +import { TipPage } from "../cta/Reward/index.js"; import { TransferCreatePage } from "../cta/TransferCreate/index.js"; import { TransferPickupPage } from "../cta/TransferPickup/index.js"; import { diff --git a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx index 149c8c1f4..1ddb24b02 100644 --- a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx @@ -34,7 +34,7 @@ import { TransactionPeerPushDebit, TransactionRefresh, TransactionRefund, - TransactionTip, + TransactionReward, TransactionType, TransactionWithdrawal, WithdrawalType, @@ -113,9 +113,9 @@ const exampleData = { } as TransactionRefresh, tip: { ...commonTransaction(), - type: TransactionType.Tip, + type: TransactionType.Reward, merchantBaseUrl: "http://ads.merchant.taler.net/", - } as TransactionTip, + } as TransactionReward, refund: { ...commonTransaction(), type: TransactionType.Refund, diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx index f2e3982f6..3ba3ac591 100644 --- a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx @@ -37,7 +37,7 @@ import { TransactionPeerPushDebit, TransactionRefresh, TransactionRefund, - TransactionTip, + TransactionReward, TransactionType, TransactionWithdrawal, WithdrawalDetails, @@ -138,7 +138,7 @@ const exampleData = { } as TransactionRefresh, tip: { ...commonTransaction, - type: TransactionType.Tip, + type: TransactionType.Reward, // merchant: { // name: "the merchant", // logo: merchantIcon, @@ -146,7 +146,7 @@ const exampleData = { // email: "contact@merchant.taler", // }, merchantBaseUrl: "http://merchant.taler", - } as TransactionTip, + } as TransactionReward, refund: { ...commonTransaction, type: TransactionType.Refund, diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx index 8d564a275..e54137016 100644 --- a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx @@ -745,7 +745,7 @@ export function TransactionView({ ); } - if (transaction.type === TransactionType.Tip) { + if (transaction.type === TransactionType.Reward) { return ( Date: Thu, 3 Aug 2023 18:58:44 +0200 Subject: fix version substitution --- packages/taler-wallet-core/package.json | 2 +- packages/taler-wallet-core/src/versions.ts | 8 ++++++++ packages/taler-wallet-core/src/wallet.ts | 4 ++-- 3 files changed, 11 insertions(+), 3 deletions(-) (limited to 'packages/taler-wallet-core/src') diff --git a/packages/taler-wallet-core/package.json b/packages/taler-wallet-core/package.json index 90ef6a453..25e7c2eb2 100644 --- a/packages/taler-wallet-core/package.json +++ b/packages/taler-wallet-core/package.json @@ -12,7 +12,7 @@ "author": "Florian Dold", "license": "GPL-3.0", "scripts": { - "compile": "jq '{version}' package.json > src/version.json && tsc --build", + "compile": "tsc --build", "pretty": "prettier --write src", "test": "tsc && ava", "coverage": "tsc && c8 --src src --all ava", diff --git a/packages/taler-wallet-core/src/versions.ts b/packages/taler-wallet-core/src/versions.ts index eedaf68f6..f0f747e22 100644 --- a/packages/taler-wallet-core/src/versions.ts +++ b/packages/taler-wallet-core/src/versions.ts @@ -34,3 +34,11 @@ export const WALLET_MERCHANT_PROTOCOL_VERSION = "2:0:1"; * Uses libtool's current:revision:age versioning. */ export const WALLET_BANK_INTEGRATION_PROTOCOL_VERSION = "0:0:0"; + +/** + * Semver of the wallet-core implementation. + * Will be replaced with the value from package.json in a + * post-compilation step (inside lib/). + */ +export const WALLET_CORE_IMPLEMENTATION_VERSION = + "__WALLET_CORE_IMPLEMENTATION_VERSION__"; diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 4a83db856..aab414e94 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -300,6 +300,7 @@ import { import { TimerAPI, TimerGroup } from "./util/timer.js"; import { WALLET_BANK_INTEGRATION_PROTOCOL_VERSION, + WALLET_CORE_IMPLEMENTATION_VERSION, WALLET_EXCHANGE_PROTOCOL_VERSION, WALLET_MERCHANT_PROTOCOL_VERSION, } from "./versions.js"; @@ -310,7 +311,6 @@ import { WalletCoreApiClient, WalletCoreResponseType, } from "./wallet-api-types.js"; -import versionInfo from "./version.json"; const logger = new Logger("wallet.ts"); @@ -1587,7 +1587,7 @@ async function dispatchRequestInternal( export function getVersion(ws: InternalWalletState): WalletCoreVersion { const result: WalletCoreVersion = { hash: undefined, - version: versionInfo.version, + version: WALLET_CORE_IMPLEMENTATION_VERSION, exchange: WALLET_EXCHANGE_PROTOCOL_VERSION, merchant: WALLET_MERCHANT_PROTOCOL_VERSION, bank: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION, -- 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') 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') 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