From 65a656163797e9dd298b34ec916b982082db7f52 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 3 Apr 2024 16:21:33 +0200 Subject: wallet-core: allow deposits with balance locked behind refresh --- .../test-wallet-blocked-deposit.ts | 152 ++++++++++++++ .../test-wallet-refresh-blocked.ts | 120 ----------- .../src/integrationtests/testrunner.ts | 4 +- packages/taler-wallet-core/src/db.ts | 6 +- packages/taler-wallet-core/src/deposits.ts | 230 +++++++++++++++------ packages/taler-wallet-core/src/dev-experiments.ts | 96 ++++----- packages/taler-wallet-core/src/transactions.ts | 29 ++- 7 files changed, 396 insertions(+), 241 deletions(-) create mode 100644 packages/taler-harness/src/integrationtests/test-wallet-blocked-deposit.ts delete mode 100644 packages/taler-harness/src/integrationtests/test-wallet-refresh-blocked.ts diff --git a/packages/taler-harness/src/integrationtests/test-wallet-blocked-deposit.ts b/packages/taler-harness/src/integrationtests/test-wallet-blocked-deposit.ts new file mode 100644 index 000000000..cb9c54f1d --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-wallet-blocked-deposit.ts @@ -0,0 +1,152 @@ +/* + This file is part of GNU Taler + (C) 2020 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 { + AmountString, + NotificationType, + TransactionMajorState, + TransactionMinorState, + j2s, +} from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { CoinConfig } from "../harness/denomStructures.js"; +import { GlobalTestState, generateRandomPayto } from "../harness/harness.js"; +import { + createSimpleTestkudosEnvironmentV2, + createWalletDaemonWithClient, + makeTestPaymentV2, + withdrawViaBankV2, +} from "../harness/helpers.js"; + +const coinCommon = { + cipher: "RSA" as const, + durationLegal: "3 years", + durationSpend: "2 years", + durationWithdraw: "7 days", + feeDeposit: "TESTKUDOS:0", + feeRefresh: "TESTKUDOS:0", + feeRefund: "TESTKUDOS:0", + feeWithdraw: "TESTKUDOS:0", + rsaKeySize: 1024, +}; + +/** + * Run test for refreshe after a payment. + */ +export async function runWalletBlockedDeposit(t: GlobalTestState) { + // Set up test environment + + const coinConfigList: CoinConfig[] = [ + { + ...coinCommon, + name: "n1", + value: "TESTKUDOS:1", + }, + { + ...coinCommon, + name: "n5", + value: "TESTKUDOS:5", + }, + ]; + + const { walletClient, bank, exchange, merchant } = + await createSimpleTestkudosEnvironmentV2(t, coinConfigList); + + // Withdraw digital cash into the wallet. + + const { walletClient: w1 } = await createWalletDaemonWithClient(t, { + name: "w1", + persistent: true, + config: { + testing: { + devModeActive: true, + }, + }, + }); + + await withdrawViaBankV2(t, { + walletClient: w1, + bank, + exchange, + amount: "TESTKUDOS:20", + }); + + await w1.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); + + // Prevent the wallet from doing refreshes by injecting a 5xx + // status for all refresh requests. + await w1.call(WalletApiOperation.ApplyDevExperiment, { + devExperimentUri: "taler://dev-experiment/start-block-refresh", + }); + + await makeTestPaymentV2(t, { + merchant, + walletClient: w1, + order: { + summary: "test", + amount: "TESTKUDOS:2", + }, + }); + + const userPayto = generateRandomPayto("foo"); + + const bal = await w1.call(WalletApiOperation.GetBalances, {}); + console.log(`balance: ${j2s(bal)}`); + + const balDet = await w1.call(WalletApiOperation.GetBalanceDetail, { + currency: "TESTKUDOS", + }); + console.log(`balance details: ${j2s(balDet)}`); + + // FIXME: Now check deposit/p2p/pay + + const depositCheckResp = await w1.call(WalletApiOperation.PrepareDeposit, { + amount: "TESTKUDOS:18" as AmountString, + depositPaytoUri: userPayto, + }); + + console.log(`check resp: ${j2s(depositCheckResp)}`); + + const depositCreateResp = await w1.call( + WalletApiOperation.CreateDepositGroup, + { + amount: "TESTKUDOS:18" as AmountString, + depositPaytoUri: userPayto, + }, + ); + + console.log(`create resp: ${j2s(depositCreateResp)}`); + + const depositTrackCond = w1.waitForNotificationCond((n) => { + return ( + n.type === NotificationType.TransactionStateTransition && + n.transactionId === depositCreateResp.transactionId && + n.newTxState.major === TransactionMajorState.Pending && + n.newTxState.minor === TransactionMinorState.Track + ); + }); + + await w1.call(WalletApiOperation.ApplyDevExperiment, { + devExperimentUri: "taler://dev-experiment/stop-block-refresh", + }); + + await depositTrackCond; +} + +runWalletBlockedDeposit.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-wallet-refresh-blocked.ts b/packages/taler-harness/src/integrationtests/test-wallet-refresh-blocked.ts deleted file mode 100644 index 4662c5110..000000000 --- a/packages/taler-harness/src/integrationtests/test-wallet-refresh-blocked.ts +++ /dev/null @@ -1,120 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 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 { AmountString, j2s } from "@gnu-taler/taler-util"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { CoinConfig } from "../harness/denomStructures.js"; -import { GlobalTestState } from "../harness/harness.js"; -import { - createSimpleTestkudosEnvironmentV2, - createWalletDaemonWithClient, - makeTestPaymentV2, - withdrawViaBankV2, -} from "../harness/helpers.js"; - -const coinCommon = { - cipher: "RSA" as const, - durationLegal: "3 years", - durationSpend: "2 years", - durationWithdraw: "7 days", - feeDeposit: "TESTKUDOS:0", - feeRefresh: "TESTKUDOS:0", - feeRefund: "TESTKUDOS:0", - feeWithdraw: "TESTKUDOS:0", - rsaKeySize: 1024, -}; - -/** - * Run test for refreshe after a payment. - */ -export async function runWalletRefreshBlockedTest(t: GlobalTestState) { - // Set up test environment - - const coinConfigList: CoinConfig[] = [ - { - ...coinCommon, - name: "n1", - value: "TESTKUDOS:1", - }, - { - ...coinCommon, - name: "n5", - value: "TESTKUDOS:5", - }, - ]; - - const { walletClient, bank, exchange, merchant } = - await createSimpleTestkudosEnvironmentV2(t, coinConfigList); - - // Withdraw digital cash into the wallet. - - const { walletClient: w1 } = await createWalletDaemonWithClient(t, { - name: "w1", - config: { - testing: { - devModeActive: true, - }, - }, - }); - - await withdrawViaBankV2(t, { - walletClient: w1, - bank, - exchange, - amount: "TESTKUDOS:20", - }); - - await w1.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); - - // Prevent the wallet from doing refreshes by injecting a 5xx - // status for all refresh requests. - await w1.call(WalletApiOperation.ApplyDevExperiment, { - devExperimentUri: "taler://dev-experiment/start-block-refresh", - }); - - await makeTestPaymentV2(t, { - merchant, - walletClient: w1, - order: { - summary: "test", - amount: "TESTKUDOS:2", - }, - }); - - const bal = await w1.call(WalletApiOperation.GetBalances, {}); - console.log(`balance: ${j2s(bal)}`); - - const balDet = await w1.call(WalletApiOperation.GetBalanceDetail, { - currency: "TESTKUDOS", - }); - console.log(`balance details: ${j2s(balDet)}`); - - // FIXME: Now check deposit/p2p/pay - - const depositCheckResp = await w1.call(WalletApiOperation.PrepareDeposit, { - amount: "TESTKUDOS:18" as AmountString, - depositPaytoUri: "payto://x-taler-bank/localhost/myuser", - }); - - console.log(`check resp: ${j2s(depositCheckResp)}`); - - // t.assertTrue(false); -} - -runWalletRefreshBlockedTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts index 2bca91e45..063aefa43 100644 --- a/packages/taler-harness/src/integrationtests/testrunner.ts +++ b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -101,7 +101,7 @@ import { runWalletGenDbTest } from "./test-wallet-gendb.js"; import { runWalletInsufficientBalanceTest } from "./test-wallet-insufficient-balance.js"; import { runWalletNotificationsTest } from "./test-wallet-notifications.js"; import { runWalletObservabilityTest } from "./test-wallet-observability.js"; -import { runWalletRefreshBlockedTest } from "./test-wallet-refresh-blocked.js"; +import { runWalletBlockedDeposit } from "./test-wallet-blocked-deposit.js"; import { runWalletRefreshTest } from "./test-wallet-refresh.js"; import { runWalletWirefeesTest } from "./test-wallet-wirefees.js"; import { runWallettestingTest } from "./test-wallettesting.js"; @@ -213,7 +213,7 @@ const allTests: TestMainFunction[] = [ runWalletWirefeesTest, runDenomLostTest, runWalletDenomExpireTest, - runWalletRefreshBlockedTest, + runWalletBlockedDeposit, ]; export interface TestRunSpec { diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index de22d78a8..7b9dfa2a2 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -1787,9 +1787,9 @@ export interface DepositGroupRecord { contractTermsHash: string; - payCoinSelection: DbCoinSelection; + payCoinSelection?: DbCoinSelection; - payCoinSelectionUid: string; + payCoinSelectionUid?: string; totalPayCost: AmountString; @@ -1804,7 +1804,7 @@ export interface DepositGroupRecord { operationStatus: DepositOperationStatus; - statusPerCoin: DepositElementStatus[]; + statusPerCoin?: DepositElementStatus[]; infoPerExchange?: Record; diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts index 05a5d780a..5b23d8325 100644 --- a/packages/taler-wallet-core/src/deposits.ts +++ b/packages/taler-wallet-core/src/deposits.ts @@ -413,16 +413,28 @@ async function refundDepositGroup( wex: WalletExecutionContext, depositGroup: DepositGroupRecord, ): Promise { - const newTxPerCoin = [...depositGroup.statusPerCoin]; + const statusPerCoin = depositGroup.statusPerCoin; + const payCoinSelection = depositGroup.payCoinSelection; + if (!statusPerCoin) { + throw Error( + "unable to refund deposit group without coin selection (status missing)", + ); + } + if (!payCoinSelection) { + throw Error( + "unable to refund deposit group without coin selection (selection missing)", + ); + } + const newTxPerCoin = [...statusPerCoin]; logger.info(`status per coin: ${j2s(depositGroup.statusPerCoin)}`); - for (let i = 0; i < depositGroup.statusPerCoin.length; i++) { - const st = depositGroup.statusPerCoin[i]; + for (let i = 0; i < statusPerCoin.length; i++) { + const st = statusPerCoin[i]; switch (st) { case DepositElementStatus.RefundFailed: case DepositElementStatus.RefundSuccess: break; default: { - const coinPub = depositGroup.payCoinSelection.coinPubs[i]; + const coinPub = payCoinSelection.coinPubs[i]; const coinExchange = await wex.db.runReadOnlyTx( ["coins"], async (tx) => { @@ -431,7 +443,7 @@ async function refundDepositGroup( return coinRecord.exchangeBaseUrl; }, ); - const refundAmount = depositGroup.payCoinSelection.coinContributions[i]; + const refundAmount = payCoinSelection.coinContributions[i]; // We use a constant refund transaction ID, since there can // only be one refund. const rtid = 1; @@ -503,8 +515,8 @@ async function refundDepositGroup( const refreshCoins: CoinRefreshRequest[] = []; for (let i = 0; i < newTxPerCoin.length; i++) { refreshCoins.push({ - amount: depositGroup.payCoinSelection.coinContributions[i], - coinPub: depositGroup.payCoinSelection.coinPubs[i], + amount: payCoinSelection.coinContributions[i], + coinPub: payCoinSelection.coinPubs[i], }); } let refreshRes: CreateRefreshGroupResult | undefined = undefined; @@ -740,9 +752,21 @@ async function processDepositGroupPendingTrack( wex: WalletExecutionContext, depositGroup: DepositGroupRecord, ): Promise { + const statusPerCoin = depositGroup.statusPerCoin; + const payCoinSelection = depositGroup.payCoinSelection; + if (!statusPerCoin) { + throw Error( + "unable to refund deposit group without coin selection (status missing)", + ); + } + if (!payCoinSelection) { + throw Error( + "unable to refund deposit group without coin selection (selection missing)", + ); + } const { depositGroupId } = depositGroup; - for (let i = 0; i < depositGroup.statusPerCoin.length; i++) { - const coinPub = depositGroup.payCoinSelection.coinPubs[i]; + for (let i = 0; i < statusPerCoin.length; i++) { + const coinPub = payCoinSelection.coinPubs[i]; // FIXME: Make the URL part of the coin selection? const exchangeBaseUrl = await wex.db.runReadWriteTx( ["coins"], @@ -761,7 +785,7 @@ async function processDepositGroupPendingTrack( } | undefined; - if (depositGroup.statusPerCoin[i] !== DepositElementStatus.Wired) { + if (statusPerCoin[i] !== DepositElementStatus.Wired) { const track = await trackDeposit( wex, depositGroup, @@ -826,6 +850,9 @@ async function processDepositGroupPendingTrack( if (!dg) { return; } + if (!dg.statusPerCoin) { + return; + } if (updatedTxStatus !== undefined) { dg.statusPerCoin[i] = updatedTxStatus; } @@ -858,9 +885,12 @@ async function processDepositGroupPendingTrack( if (!dg) { return undefined; } + if (!dg.statusPerCoin) { + return undefined; + } const oldTxState = computeDepositTransactionStatus(dg); - for (let i = 0; i < depositGroup.statusPerCoin.length; i++) { - if (depositGroup.statusPerCoin[i] !== DepositElementStatus.Wired) { + for (let i = 0; i < dg.statusPerCoin.length; i++) { + if (dg.statusPerCoin[i] !== DepositElementStatus.Wired) { allWired = false; break; } @@ -924,6 +954,87 @@ async function processDepositGroupPendingDeposit( // Check for cancellation before expensive operations. cancellationToken?.throwIfCancelled(); + if (!depositGroup.payCoinSelection) { + logger.info("missing coin selection for deposit group, selecting now"); + // FIXME: Consider doing the coin selection inside the txn + const payCoinSel = await selectPayCoins(wex, { + restrictExchanges: { + auditors: [], + exchanges: contractData.allowedExchanges, + }, + restrictWireMethod: contractData.wireMethod, + contractTermsAmount: Amounts.parseOrThrow(contractData.amount), + depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee), + wireFeeAmortization: 1, // FIXME #8653 + prevPayCoins: [], + }); + + switch (payCoinSel.type) { + case "success": + logger.info("coin selection success"); + break; + case "failure": + logger.info("coin selection failure"); + throw TalerError.fromDetail( + TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE, + { + insufficientBalanceDetails: payCoinSel.insufficientBalanceDetails, + }, + ); + case "prospective": + logger.info("coin selection prospective"); + throw Error("insufficient balance (waiting on pending refresh)"); + default: + assertUnreachable(payCoinSel); + } + + const transitionDone = await wex.db.runReadWriteTx( + [ + "depositGroups", + "coins", + "coinAvailability", + "refreshGroups", + "refreshSessions", + "denominations", + ], + async (tx) => { + const dg = await tx.depositGroups.get(depositGroupId); + if (!dg) { + return false; + } + if (dg.statusPerCoin) { + return false; + } + dg.payCoinSelection = { + coinContributions: payCoinSel.coinSel.coins.map( + (x) => x.contribution, + ), + coinPubs: payCoinSel.coinSel.coins.map((x) => x.coinPub), + }; + dg.payCoinSelectionUid = encodeCrock(getRandomBytes(32)); + dg.statusPerCoin = payCoinSel.coinSel.coins.map( + () => DepositElementStatus.DepositPending, + ); + await tx.depositGroups.put(dg); + await spendCoins(wex, tx, { + allocationId: transactionId, + coinPubs: dg.payCoinSelection.coinPubs, + contributions: dg.payCoinSelection.coinContributions.map((x) => + Amounts.parseOrThrow(x), + ), + refreshReason: RefreshReason.PayDeposit, + }); + return true; + }, + ); + + if (transitionDone) { + return TaskRunResult.progress(); + } else { + return TaskRunResult.backoff(); + } + } + // FIXME: Cache these! const depositPermissions = await generateDepositPermissions( wex, @@ -990,6 +1101,9 @@ async function processDepositGroupPendingDeposit( if (!dg) { return; } + if (!dg.statusPerCoin) { + return; + } for (const batchIndex of batchIndexes) { const coinStatus = dg.statusPerCoin[batchIndex]; switch (coinStatus) { @@ -1360,8 +1474,11 @@ export async function createDepositGroup( prevPayCoins: [], }); + let coins: SelectedProspectiveCoin[] | undefined = undefined; + switch (payCoinSel.type) { case "success": + coins = payCoinSel.coinSel.coins; break; case "failure": throw TalerError.fromDetail( @@ -1371,17 +1488,13 @@ export async function createDepositGroup( }, ); case "prospective": - // FIXME: Here we need to create the deposit group without a full coin selection! - throw Error("insufficient balance (pending refresh)"); + coins = payCoinSel.result.prospectiveCoins; + break; default: assertUnreachable(payCoinSel); } - const totalDepositCost = await getTotalPaymentCost( - wex, - currency, - payCoinSel.coinSel.coins, - ); + const totalDepositCost = await getTotalPaymentCost(wex, currency, coins); let depositGroupId: string; if (req.transactionId) { @@ -1396,34 +1509,23 @@ export async function createDepositGroup( const infoPerExchange: Record = {}; - await wex.db.runReadOnlyTx(["coins"], async (tx) => { - for (let i = 0; i < payCoinSel.coinSel.coins.length; i++) { - const coin = await tx.coins.get(payCoinSel.coinSel.coins[i].coinPub); - if (!coin) { - logger.error("coin not found anymore"); - continue; - } - let depPerExchange = infoPerExchange[coin.exchangeBaseUrl]; - if (!depPerExchange) { - infoPerExchange[coin.exchangeBaseUrl] = depPerExchange = { - amountEffective: Amounts.stringify( - Amounts.zeroOfAmount(totalDepositCost), - ), - }; - } - const contrib = payCoinSel.coinSel.coins[i].contribution; - depPerExchange.amountEffective = Amounts.stringify( - Amounts.add(depPerExchange.amountEffective, contrib).amount, - ); + for (let i = 0; i < coins.length; i++) { + let depPerExchange = infoPerExchange[coins[i].exchangeBaseUrl]; + if (!depPerExchange) { + infoPerExchange[coins[i].exchangeBaseUrl] = depPerExchange = { + amountEffective: Amounts.stringify( + Amounts.zeroOfAmount(totalDepositCost), + ), + }; } - }); + const contrib = coins[i].contribution; + depPerExchange.amountEffective = Amounts.stringify( + Amounts.add(depPerExchange.amountEffective, contrib).amount, + ); + } const counterpartyEffectiveDepositAmount = - await getCounterpartyEffectiveDepositAmount( - wex, - p.targetType, - payCoinSel.coinSel.coins, - ); + await getCounterpartyEffectiveDepositAmount(wex, p.targetType, coins); const depositGroup: DepositGroupRecord = { contractTermsHash, @@ -1436,14 +1538,9 @@ export async function createDepositGroup( AbsoluteTime.toPreciseTimestamp(now), ), timestampFinished: undefined, - statusPerCoin: payCoinSel.coinSel.coins.map( - () => DepositElementStatus.DepositPending, - ), - payCoinSelection: { - coinContributions: payCoinSel.coinSel.coins.map((x) => x.contribution), - coinPubs: payCoinSel.coinSel.coins.map((x) => x.coinPub), - }, - payCoinSelectionUid: encodeCrock(getRandomBytes(32)), + statusPerCoin: undefined, + payCoinSelection: undefined, + payCoinSelectionUid: undefined, merchantPriv: merchantPair.priv, merchantPub: merchantPair.pub, totalPayCost: Amounts.stringify(totalDepositCost), @@ -1461,6 +1558,17 @@ export async function createDepositGroup( infoPerExchange, }; + if (payCoinSel.type === "success") { + depositGroup.payCoinSelection = { + coinContributions: payCoinSel.coinSel.coins.map((x) => x.contribution), + coinPubs: payCoinSel.coinSel.coins.map((x) => x.coinPub), + }; + depositGroup.payCoinSelectionUid = encodeCrock(getRandomBytes(32)); + depositGroup.statusPerCoin = payCoinSel.coinSel.coins.map( + () => DepositElementStatus.DepositPending, + ); + } + const ctx = new DepositTransactionContext(wex, depositGroupId); const transactionId = ctx.transactionId; @@ -1476,14 +1584,16 @@ export async function createDepositGroup( "contractTerms", ], async (tx) => { - await spendCoins(wex, tx, { - allocationId: transactionId, - coinPubs: payCoinSel.coinSel.coins.map((x) => x.coinPub), - contributions: payCoinSel.coinSel.coins.map((x) => - Amounts.parseOrThrow(x.contribution), - ), - refreshReason: RefreshReason.PayDeposit, - }); + if (depositGroup.payCoinSelection) { + await spendCoins(wex, tx, { + allocationId: transactionId, + coinPubs: depositGroup.payCoinSelection.coinPubs, + contributions: depositGroup.payCoinSelection.coinContributions.map( + (x) => Amounts.parseOrThrow(x), + ), + refreshReason: RefreshReason.PayDeposit, + }); + } await tx.depositGroups.put(depositGroup); await tx.contractTerms.put({ contractTermsRaw: contractTerms, diff --git a/packages/taler-wallet-core/src/dev-experiments.ts b/packages/taler-wallet-core/src/dev-experiments.ts index 57810dbf4..7cf18e36c 100644 --- a/packages/taler-wallet-core/src/dev-experiments.ts +++ b/packages/taler-wallet-core/src/dev-experiments.ts @@ -67,52 +67,56 @@ export async function applyDevExperiment( throw Error("can't handle devmode URI unless devmode is active"); } - if (parsedUri.devExperimentId === "start-block-refresh") { - wex.ws.devExperimentState.blockRefreshes = true; - return; - } - - if (parsedUri.devExperimentId == "insert-pending-refresh") { - await wex.db.runReadWriteTx(["refreshGroups"], async (tx) => { - const refreshGroupId = encodeCrock(getRandomBytes(32)); - const newRg: RefreshGroupRecord = { - currency: "TESTKUDOS", - expectedOutputPerCoin: [], - inputPerCoin: [], - oldCoinPubs: [], - operationStatus: RefreshOperationStatus.Pending, - reason: RefreshReason.Manual, - refreshGroupId, - statusPerCoin: [], - timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()), - timestampFinished: undefined, - originatingTransactionId: undefined, - infoPerExchange: {}, - }; - await tx.refreshGroups.put(newRg); - }); - return; - } - - if (parsedUri.devExperimentId == "insert-denom-loss") { - await wex.db.runReadWriteTx(["denomLossEvents"], async (tx) => { - const eventId = encodeCrock(getRandomBytes(32)); - const newRg: DenomLossEventRecord = { - amount: "TESTKUDOS:42", - currency: "TESTKUDOS", - exchangeBaseUrl: "https://exchange.test.taler.net/", - denomLossEventId: eventId, - denomPubHashes: [ - encodeCrock(getRandomBytes(64)), - encodeCrock(getRandomBytes(64)), - ], - eventType: DenomLossEventType.DenomExpired, - status: DenomLossStatus.Done, - timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()), - }; - await tx.denomLossEvents.put(newRg); - }); - return; + switch (parsedUri.devExperimentId) { + case "start-block-refresh": { + wex.ws.devExperimentState.blockRefreshes = true; + return; + } + case "stop-block-refresh": { + wex.ws.devExperimentState.blockRefreshes = false; + return; + } + case "insert-pending-refresh": { + await wex.db.runReadWriteTx(["refreshGroups"], async (tx) => { + const refreshGroupId = encodeCrock(getRandomBytes(32)); + const newRg: RefreshGroupRecord = { + currency: "TESTKUDOS", + expectedOutputPerCoin: [], + inputPerCoin: [], + oldCoinPubs: [], + operationStatus: RefreshOperationStatus.Pending, + reason: RefreshReason.Manual, + refreshGroupId, + statusPerCoin: [], + timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()), + timestampFinished: undefined, + originatingTransactionId: undefined, + infoPerExchange: {}, + }; + await tx.refreshGroups.put(newRg); + }); + return; + } + case "insert-denom-loss": { + await wex.db.runReadWriteTx(["denomLossEvents"], async (tx) => { + const eventId = encodeCrock(getRandomBytes(32)); + const newRg: DenomLossEventRecord = { + amount: "TESTKUDOS:42", + currency: "TESTKUDOS", + exchangeBaseUrl: "https://exchange.test.taler.net/", + denomLossEventId: eventId, + denomPubHashes: [ + encodeCrock(getRandomBytes(64)), + encodeCrock(getRandomBytes(64)), + ], + eventType: DenomLossEventType.DenomExpired, + status: DenomLossStatus.Done, + timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()), + }; + await tx.denomLossEvents.put(newRg); + }); + return; + } } throw Error(`dev-experiment id not understood ${parsedUri.devExperimentId}`); diff --git a/packages/taler-wallet-core/src/transactions.ts b/packages/taler-wallet-core/src/transactions.ts index e404c0354..463aa97ba 100644 --- a/packages/taler-wallet-core/src/transactions.ts +++ b/packages/taler-wallet-core/src/transactions.ts @@ -894,10 +894,14 @@ function buildTransactionForDeposit( ort?: OperationRetryRecord, ): Transaction { let deposited = true; - for (const d of dg.statusPerCoin) { - if (d == DepositElementStatus.DepositPending) { - deposited = false; + if (dg.statusPerCoin) { + for (const d of dg.statusPerCoin) { + if (d == DepositElementStatus.DepositPending) { + deposited = false; + } } + } else { + deposited = false; } const trackingState: DepositTransactionTrackingState[] = []; @@ -911,6 +915,17 @@ function buildTransactionForDeposit( }); } + let wireTransferProgress = 0; + if (dg.statusPerCoin) { + wireTransferProgress = + (100 * + dg.statusPerCoin.reduce( + (prev, cur) => prev + (cur === DepositElementStatus.Wired ? 1 : 0), + 0, + )) / + dg.statusPerCoin.length; + } + const txState = computeDepositTransactionStatus(dg); return { type: TransactionType.Deposit, @@ -927,13 +942,7 @@ function buildTransactionForDeposit( tag: TransactionType.Deposit, depositGroupId: dg.depositGroupId, }), - wireTransferProgress: - (100 * - dg.statusPerCoin.reduce( - (prev, cur) => prev + (cur === DepositElementStatus.Wired ? 1 : 0), - 0, - )) / - dg.statusPerCoin.length, + wireTransferProgress, depositGroupId: dg.depositGroupId, trackingState, deposited, -- cgit v1.2.3