From 02ceacd75fa16889925c8eda6b28f181b2ad392c Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 28 May 2024 10:37:58 -0300 Subject: fix #8828 --- packages/taler-util/src/invariants.ts | 2 +- packages/taler-util/src/wallet-types.ts | 4 +- packages/taler-wallet-core/src/backup/index.ts | 16 +++-- packages/taler-wallet-core/src/coinSelection.ts | 5 +- packages/taler-wallet-core/src/common.ts | 51 ++++++++-------- packages/taler-wallet-core/src/db.ts | 4 +- packages/taler-wallet-core/src/deposits.ts | 2 +- packages/taler-wallet-core/src/exchanges.ts | 2 +- .../src/instructedAmountConversion.ts | 22 +++++-- packages/taler-wallet-core/src/pay-merchant.ts | 8 +-- packages/taler-wallet-core/src/pay-peer-common.ts | 6 +- .../taler-wallet-core/src/pay-peer-pull-credit.ts | 2 +- .../taler-wallet-core/src/pay-peer-push-credit.ts | 2 +- .../taler-wallet-core/src/pay-peer-push-debit.ts | 2 +- packages/taler-wallet-core/src/recoup.ts | 4 +- packages/taler-wallet-core/src/refresh.ts | 14 ++--- packages/taler-wallet-core/src/transactions.ts | 22 +++---- packages/taler-wallet-core/src/wallet.ts | 10 +--- packages/taler-wallet-core/src/withdraw.ts | 4 +- .../src/wallet/DepositPage/index.ts | 2 +- .../src/wallet/DepositPage/state.ts | 68 +++++++++++++--------- .../src/wallet/DepositPage/stories.tsx | 15 ++++- .../src/wallet/DepositPage/test.ts | 57 ++++++++---------- .../src/wallet/DepositPage/views.tsx | 11 +--- .../src/wallet/ManageAccount/views.tsx | 1 - 25 files changed, 178 insertions(+), 158 deletions(-) diff --git a/packages/taler-util/src/invariants.ts b/packages/taler-util/src/invariants.ts index c6e9b8113..113d697c3 100644 --- a/packages/taler-util/src/invariants.ts +++ b/packages/taler-util/src/invariants.ts @@ -33,7 +33,7 @@ export class InvariantViolatedError extends Error { * * A violation of this invariant means that the database is inconsistent. */ -export function checkDbInvariant(b: boolean, m?: string): asserts b { +export function checkDbInvariant(b: boolean, m: string): asserts b { if (!b) { if (m) { throw Error(`BUG: database invariant failed (${m})`); diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts index 9301a9723..24a48b415 100644 --- a/packages/taler-util/src/wallet-types.ts +++ b/packages/taler-util/src/wallet-types.ts @@ -54,7 +54,7 @@ import { canonicalizeBaseUrl, } from "./index.js"; import { VersionMatchResult } from "./libtool-version.js"; -import { PaytoUri } from "./payto.js"; +import { PaytoString, PaytoUri, codecForPaytoString } from "./payto.js"; import { AgeCommitmentProof } from "./taler-crypto.js"; import { TalerErrorCode } from "./taler-error-codes.js"; import { @@ -229,11 +229,13 @@ interface GetPlanForWalletInitiatedOperation { export interface ConvertAmountRequest { amount: AmountString; type: TransactionAmountMode; + depositPaytoUri: PaytoString; } export const codecForConvertAmountRequest = buildCodecForObject() .property("amount", codecForAmountString()) + .property("depositPaytoUri", codecForPaytoString()) .property( "type", codecForEither( diff --git a/packages/taler-wallet-core/src/backup/index.ts b/packages/taler-wallet-core/src/backup/index.ts index 15904b470..09d5ae75d 100644 --- a/packages/taler-wallet-core/src/backup/index.ts +++ b/packages/taler-wallet-core/src/backup/index.ts @@ -805,9 +805,10 @@ async function backupRecoveryTheirs( let backupStateEntry: ConfigRecord | undefined = await tx.config.get( ConfigRecordKey.WalletBackupState, ); - checkDbInvariant(!!backupStateEntry); + checkDbInvariant(!!backupStateEntry, `no backup entry`); checkDbInvariant( backupStateEntry.key === ConfigRecordKey.WalletBackupState, + `backup entry inconsistent`, ); backupStateEntry.value.lastBackupNonce = undefined; backupStateEntry.value.lastBackupTimestamp = undefined; @@ -913,7 +914,10 @@ export async function provideBackupState( }, ); if (bs) { - checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState); + checkDbInvariant( + bs.key === ConfigRecordKey.WalletBackupState, + `backup entry inconsistent`, + ); return bs.value; } // We need to generate the key outside of the transaction @@ -941,6 +945,7 @@ export async function provideBackupState( } checkDbInvariant( backupStateEntry.key === ConfigRecordKey.WalletBackupState, + `backup entry inconsistent`, ); return backupStateEntry.value; }); @@ -952,7 +957,10 @@ export async function getWalletBackupState( ): Promise { const bs = await tx.config.get(ConfigRecordKey.WalletBackupState); checkDbInvariant(!!bs, "wallet backup state should be in DB"); - checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState); + checkDbInvariant( + bs.key === ConfigRecordKey.WalletBackupState, + `backup entry inconsistent`, + ); return bs.value; } @@ -962,7 +970,7 @@ export async function setWalletDeviceId( ): Promise { await provideBackupState(wex); await wex.db.runReadWriteTx({ storeNames: ["config"] }, async (tx) => { - let backupStateEntry: ConfigRecord | undefined = await tx.config.get( + const backupStateEntry: ConfigRecord | undefined = await tx.config.get( ConfigRecordKey.WalletBackupState, ); if ( diff --git a/packages/taler-wallet-core/src/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts index a60e41ecd..db6384c93 100644 --- a/packages/taler-wallet-core/src/coinSelection.ts +++ b/packages/taler-wallet-core/src/coinSelection.ts @@ -691,7 +691,7 @@ export function checkAccountRestriction( switch (myRestriction.type) { case "deny": return { ok: false }; - case "regex": + case "regex": { const regex = new RegExp(myRestriction.payto_regex); if (!regex.test(paytoUri)) { return { @@ -700,6 +700,7 @@ export function checkAccountRestriction( hintI18n: myRestriction.human_hint_i18n, }; } + } } } return { @@ -909,7 +910,7 @@ async function selectPayCandidates( coinAvail.exchangeBaseUrl, coinAvail.denomPubHash, ]); - checkDbInvariant(!!denom); + checkDbInvariant(!!denom, `denomination of a coin is missing hash: ${coinAvail.denomPubHash}`); if (denom.isRevoked) { logger.trace("denom is revoked"); continue; diff --git a/packages/taler-wallet-core/src/common.ts b/packages/taler-wallet-core/src/common.ts index 755c46188..00d462d6f 100644 --- a/packages/taler-wallet-core/src/common.ts +++ b/packages/taler-wallet-core/src/common.ts @@ -121,7 +121,7 @@ export async function makeCoinAvailable( coinRecord.exchangeBaseUrl, coinRecord.denomPubHash, ]); - checkDbInvariant(!!denom); + checkDbInvariant(!!denom, `denomination of a coin is missing hash: ${coinRecord.denomPubHash}`); const ageRestriction = coinRecord.maxAge; let car = await tx.coinAvailability.get([ coinRecord.exchangeBaseUrl, @@ -175,13 +175,13 @@ export async function spendCoins( coin.exchangeBaseUrl, coin.denomPubHash, ); - checkDbInvariant(!!denom); + checkDbInvariant(!!denom, `denomination of a coin is missing hash: ${coin.denomPubHash}`); const coinAvailability = await tx.coinAvailability.get([ coin.exchangeBaseUrl, coin.denomPubHash, coin.maxAge, ]); - checkDbInvariant(!!coinAvailability); + checkDbInvariant(!!coinAvailability, `age denom info is missing for ${coin.maxAge}`); const contrib = csi.contributions[i]; if (coin.status !== CoinStatus.Fresh) { const alloc = coin.spendAllocation; @@ -213,7 +213,6 @@ export async function spendCoins( amount: Amounts.stringify(remaining.amount), coinPub: coin.coinPub, }); - checkDbInvariant(!!coinAvailability); if (coinAvailability.freshCoinCount === 0) { throw Error( `invalid coin count ${coinAvailability.freshCoinCount} in DB`, @@ -557,6 +556,28 @@ export function getAutoRefreshExecuteThreshold(d: { return AbsoluteTime.addDuration(expireWithdraw, deltaDiv); } +/** + * Type and schema definitions for pending tasks in the wallet. + * + * These are only used internally, and are not part of the stable public + * interface to the wallet. + */ + +export enum PendingTaskType { + ExchangeUpdate = "exchange-update", + Purchase = "purchase", + Refresh = "refresh", + Recoup = "recoup", + RewardPickup = "reward-pickup", + Withdraw = "withdraw", + Deposit = "deposit", + Backup = "backup", + PeerPushDebit = "peer-push-debit", + PeerPullCredit = "peer-pull-credit", + PeerPushCredit = "peer-push-credit", + PeerPullDebit = "peer-pull-debit", +} + /** * Parsed representation of task identifiers. */ @@ -747,28 +768,6 @@ export interface TransactionContext { deleteTransaction(): Promise; } -/** - * Type and schema definitions for pending tasks in the wallet. - * - * These are only used internally, and are not part of the stable public - * interface to the wallet. - */ - -export enum PendingTaskType { - ExchangeUpdate = "exchange-update", - Purchase = "purchase", - Refresh = "refresh", - Recoup = "recoup", - RewardPickup = "reward-pickup", - Withdraw = "withdraw", - Deposit = "deposit", - Backup = "backup", - PeerPushDebit = "peer-push-debit", - PeerPullCredit = "peer-pull-credit", - PeerPushCredit = "peer-push-credit", - PeerPullDebit = "peer-pull-debit", -} - declare const __taskIdStr: unique symbol; export type TaskIdStr = string & { [__taskIdStr]: true }; diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 640d94753..ad9b4f1cb 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -2941,6 +2941,8 @@ export interface DbDump { }; } +const logger = new Logger("db.ts"); + export async function exportSingleDb( idb: IDBFactory, dbName: string, @@ -3082,8 +3084,6 @@ export interface FixupDescription { */ export const walletDbFixups: FixupDescription[] = []; -const logger = new Logger("db.ts"); - export async function applyFixups( db: DbAccess, ): Promise { diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts index c4cd98d73..32b22c1ef 100644 --- a/packages/taler-wallet-core/src/deposits.ts +++ b/packages/taler-wallet-core/src/deposits.ts @@ -441,7 +441,7 @@ async function refundDepositGroup( { storeNames: ["coins"] }, async (tx) => { const coinRecord = await tx.coins.get(coinPub); - checkDbInvariant(!!coinRecord); + checkDbInvariant(!!coinRecord, `coin ${coinPub} not found in DB`); return coinRecord.exchangeBaseUrl; }, ); diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts index d8063d561..fa3146c38 100644 --- a/packages/taler-wallet-core/src/exchanges.ts +++ b/packages/taler-wallet-core/src/exchanges.ts @@ -1548,7 +1548,7 @@ export async function updateExchangeFromUrlHandler( r.cachebreakNextUpdate = false; await tx.exchanges.put(r); const drRowId = await tx.exchangeDetails.put(newDetails); - checkDbInvariant(typeof drRowId.key === "number"); + checkDbInvariant(typeof drRowId.key === "number", "exchange details key is not a number"); for (const sk of keysInfo.signingKeys) { // FIXME: validate signing keys before inserting them diff --git a/packages/taler-wallet-core/src/instructedAmountConversion.ts b/packages/taler-wallet-core/src/instructedAmountConversion.ts index 1f7d95959..5b399a0a7 100644 --- a/packages/taler-wallet-core/src/instructedAmountConversion.ts +++ b/packages/taler-wallet-core/src/instructedAmountConversion.ts @@ -283,7 +283,7 @@ async function getAvailableDenoms( coinAvail.exchangeBaseUrl, coinAvail.denomPubHash, ]); - checkDbInvariant(!!denom); + checkDbInvariant(!!denom, `denomination of a coin is missing hash: ${coinAvail.denomPubHash}`); if (denom.isRevoked || !denom.isOffered) { continue; } @@ -472,7 +472,7 @@ export async function getMaxDepositAmount( export function getMaxDepositAmountForAvailableCoins( denoms: AvailableCoins, currency: string, -) { +): AmountWithFee { const zero = Amounts.zeroOfCurrency(currency); if (!denoms.list.length) { // no coins in the database @@ -663,8 +663,13 @@ function rankDenominationForWithdrawals( //different exchanges may have different wireFee //ranking should take the relative contribution in the exchange //which is (value - denomFee / fixedFee) - const rate1 = Amounts.divmod(d1.value, d1.denomWithdraw).quotient; - const rate2 = Amounts.divmod(d2.value, d2.denomWithdraw).quotient; + + const rate1 = Amounts.isZero(d1.denomWithdraw) + ? Number.MIN_SAFE_INTEGER + : Amounts.divmod(d1.value, d1.denomWithdraw).quotient; + const rate2 = Amounts.isZero(d2.denomWithdraw) + ? Number.MIN_SAFE_INTEGER + : Amounts.divmod(d2.value, d2.denomWithdraw).quotient; const contribCmp = rate1 === rate2 ? 0 : rate1 < rate2 ? 1 : -1; return ( contribCmp || @@ -719,8 +724,13 @@ function rankDenominationForDeposit( //different exchanges may have different wireFee //ranking should take the relative contribution in the exchange //which is (value - denomFee / fixedFee) - const rate1 = Amounts.divmod(d1.value, d1.denomDeposit).quotient; - const rate2 = Amounts.divmod(d2.value, d2.denomDeposit).quotient; + const rate1 = Amounts.isZero(d1.denomDeposit) + ? Number.MIN_SAFE_INTEGER + : Amounts.divmod(d1.value, d1.denomDeposit).quotient; + const rate2 = Amounts.isZero(d2.denomDeposit) + ? Number.MIN_SAFE_INTEGER + : Amounts.divmod(d2.value, d2.denomDeposit).quotient; + const contribCmp = rate1 === rate2 ? 0 : rate1 < rate2 ? 1 : -1; return ( contribCmp || diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts index 090a11cf0..aa4919285 100644 --- a/packages/taler-wallet-core/src/pay-merchant.ts +++ b/packages/taler-wallet-core/src/pay-merchant.ts @@ -1028,11 +1028,11 @@ async function storeFirstPaySuccess( purchase.merchantPaySig = payResponse.sig; purchase.posConfirmation = payResponse.pos_confirmation; const dl = purchase.download; - checkDbInvariant(!!dl); + checkDbInvariant(!!dl, `purchase ${purchase.orderId} without ct downloaded`); const contractTermsRecord = await tx.contractTerms.get( dl.contractTermsHash, ); - checkDbInvariant(!!contractTermsRecord); + checkDbInvariant(!!contractTermsRecord, `no contract terms found for purchase ${purchase.orderId}`); const contractData = extractContractData( contractTermsRecord.contractTermsRaw, dl.contractTermsHash, @@ -2155,7 +2155,7 @@ async function processPurchasePay( logger.trace(`paying with session ID ${sessionId}`); const payInfo = purchase.payInfo; - checkDbInvariant(!!payInfo, "payInfo"); + checkDbInvariant(!!payInfo, `purchase ${purchase.orderId} without payInfo`); const download = await expectProposalDownload(wex, purchase); @@ -2997,7 +2997,7 @@ async function processPurchaseAbortingRefund( for (let i = 0; i < payCoinSelection.coinPubs.length; i++) { const coinPub = payCoinSelection.coinPubs[i]; const coin = await tx.coins.get(coinPub); - checkDbInvariant(!!coin, "expected coin to be present"); + checkDbInvariant(!!coin, `coin not found for ${coinPub}`); abortingCoins.push({ coin_pub: coinPub, contribution: Amounts.stringify(payCoinSelection.coinContributions[i]), diff --git a/packages/taler-wallet-core/src/pay-peer-common.ts b/packages/taler-wallet-core/src/pay-peer-common.ts index bfd39b657..a1729ced7 100644 --- a/packages/taler-wallet-core/src/pay-peer-common.ts +++ b/packages/taler-wallet-core/src/pay-peer-common.ts @@ -140,10 +140,10 @@ export async function getMergeReserveInfo( { storeNames: ["exchanges", "reserves"] }, async (tx) => { const ex = await tx.exchanges.get(req.exchangeBaseUrl); - checkDbInvariant(!!ex); + checkDbInvariant(!!ex, `no exchange record for ${req.exchangeBaseUrl}`); if (ex.currentMergeReserveRowId != null) { const reserve = await tx.reserves.get(ex.currentMergeReserveRowId); - checkDbInvariant(!!reserve); + checkDbInvariant(!!reserve, `reserver ${ex.currentMergeReserveRowId} missing in db`); return reserve; } const reserve: ReserveRecord = { @@ -151,7 +151,7 @@ export async function getMergeReserveInfo( reservePub: newReservePair.pub, }; const insertResp = await tx.reserves.put(reserve); - checkDbInvariant(typeof insertResp.key === "number"); + checkDbInvariant(typeof insertResp.key === "number", `reserve key is not a number`); reserve.rowId = insertResp.key; ex.currentMergeReserveRowId = reserve.rowId; await tx.exchanges.put(ex); diff --git a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts index 840c244d0..14b3eeaf0 100644 --- a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts +++ b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts @@ -1039,7 +1039,7 @@ export async function initiatePeerPullPayment( const withdrawalGroupId = encodeCrock(getRandomBytes(32)); const mergeReserveRowId = mergeReserveInfo.rowId; - checkDbInvariant(!!mergeReserveRowId); + checkDbInvariant(!!mergeReserveRowId, `merge reserve for ${exchangeBaseUrl} without rowid`); const contractEncNonce = encodeCrock(getRandomBytes(24)); diff --git a/packages/taler-wallet-core/src/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/pay-peer-push-credit.ts index 93f1a63a7..1476a0f4b 100644 --- a/packages/taler-wallet-core/src/pay-peer-push-credit.ts +++ b/packages/taler-wallet-core/src/pay-peer-push-credit.ts @@ -872,7 +872,7 @@ export async function processPeerPushCredit( `processing peerPushCredit in state ${peerInc.status.toString(16)}`, ); - checkDbInvariant(!!contractTerms); + checkDbInvariant(!!contractTerms, `not contract terms for peer push ${peerPushCreditId}`); switch (peerInc.status) { case PeerPushCreditStatus.PendingMergeKycRequired: { diff --git a/packages/taler-wallet-core/src/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/pay-peer-push-debit.ts index 6452407ff..3a936fb04 100644 --- a/packages/taler-wallet-core/src/pay-peer-push-debit.ts +++ b/packages/taler-wallet-core/src/pay-peer-push-debit.ts @@ -406,7 +406,7 @@ async function handlePurseCreationConflict( const instructedAmount = Amounts.parseOrThrow(peerPushInitiation.amount); const sel = peerPushInitiation.coinSel; - checkDbInvariant(!!sel); + checkDbInvariant(!!sel, `no coin selected for peer push initiation ${peerPushInitiation.pursePub}`); const repair: PreviousPayCoins = []; diff --git a/packages/taler-wallet-core/src/recoup.ts b/packages/taler-wallet-core/src/recoup.ts index 6a09f9a0e..be5731b0b 100644 --- a/packages/taler-wallet-core/src/recoup.ts +++ b/packages/taler-wallet-core/src/recoup.ts @@ -199,8 +199,8 @@ async function recoupRefreshCoin( revokedCoin.exchangeBaseUrl, revokedCoin.denomPubHash, ); - checkDbInvariant(!!oldCoinDenom); - checkDbInvariant(!!revokedCoinDenom); + checkDbInvariant(!!oldCoinDenom, `no denom for coin, hash ${oldCoin.denomPubHash}`); + checkDbInvariant(!!revokedCoinDenom, `no revoked denom for coin, hash ${revokedCoin.denomPubHash}`); revokedCoin.status = CoinStatus.Dormant; if (!revokedCoin.spendAllocation) { // We don't know what happened to this coin diff --git a/packages/taler-wallet-core/src/refresh.ts b/packages/taler-wallet-core/src/refresh.ts index 38b8b097c..f160e0731 100644 --- a/packages/taler-wallet-core/src/refresh.ts +++ b/packages/taler-wallet-core/src/refresh.ts @@ -387,7 +387,6 @@ async function getCoinAvailabilityForDenom( denom: DenominationInfo, ageRestriction: number, ): Promise { - checkDbInvariant(!!denom); let car = await tx.coinAvailability.get([ denom.exchangeBaseUrl, denom.denomPubHash, @@ -538,7 +537,7 @@ async function destroyRefreshSession( denom, oldCoin.maxAge, ); - checkDbInvariant(car.pendingRefreshOutputCount != null); + checkDbInvariant(car.pendingRefreshOutputCount != null, `no pendingRefreshOutputCount for denom ${dph}`); car.pendingRefreshOutputCount = car.pendingRefreshOutputCount - refreshSession.newDenoms[i].count; await tx.coinAvailability.put(car); @@ -1251,7 +1250,7 @@ async function refreshReveal( coin.exchangeBaseUrl, coin.denomPubHash, ); - checkDbInvariant(!!denomInfo); + checkDbInvariant(!!denomInfo, `no denom with hash ${coin.denomPubHash}`); const car = await getCoinAvailabilityForDenom( wex, tx, @@ -1261,6 +1260,7 @@ async function refreshReveal( checkDbInvariant( car.pendingRefreshOutputCount != null && car.pendingRefreshOutputCount > 0, + `no pendingRefreshOutputCount for denom ${coin.denomPubHash} age ${coin.maxAge}` ); car.pendingRefreshOutputCount--; car.freshCoinCount++; @@ -1568,8 +1568,8 @@ async function applyRefreshToOldCoins( coin.denomPubHash, coin.maxAge, ]); - checkDbInvariant(!!coinAv); - checkDbInvariant(coinAv.freshCoinCount > 0); + checkDbInvariant(!!coinAv, `no denom info for ${coin.denomPubHash} age ${coin.maxAge}`); + checkDbInvariant(coinAv.freshCoinCount > 0, `no fresh coins for ${coin.denomPubHash}`); coinAv.freshCoinCount--; await tx.coinAvailability.put(coinAv); break; @@ -1779,7 +1779,7 @@ export async function forceRefresh( ], }, async (tx) => { - let coinPubs: CoinRefreshRequest[] = []; + const coinPubs: CoinRefreshRequest[] = []; for (const c of req.refreshCoinSpecs) { const coin = await tx.coins.get(c.coinPub); if (!coin) { @@ -1791,7 +1791,7 @@ export async function forceRefresh( coin.exchangeBaseUrl, coin.denomPubHash, ); - checkDbInvariant(!!denom); + checkDbInvariant(!!denom, `no denom hash: ${coin.denomPubHash}`); coinPubs.push({ coinPub: c.coinPub, amount: c.amount ?? denom.value, diff --git a/packages/taler-wallet-core/src/transactions.ts b/packages/taler-wallet-core/src/transactions.ts index 1adfa5425..72aff319a 100644 --- a/packages/taler-wallet-core/src/transactions.ts +++ b/packages/taler-wallet-core/src/transactions.ts @@ -404,7 +404,7 @@ export async function getTransactionById( const debit = await tx.peerPushDebit.get(parsedTx.pursePub); if (!debit) throw Error("not found"); const ct = await tx.contractTerms.get(debit.contractTermsHash); - checkDbInvariant(!!ct); + checkDbInvariant(!!ct, `no contract terms for p2p push ${parsedTx.pursePub}`); return buildTransactionForPushPaymentDebit( debit, ct.contractTermsRaw, @@ -428,7 +428,7 @@ export async function getTransactionById( const pushInc = await tx.peerPushCredit.get(peerPushCreditId); if (!pushInc) throw Error("not found"); const ct = await tx.contractTerms.get(pushInc.contractTermsHash); - checkDbInvariant(!!ct); + checkDbInvariant(!!ct, `no contract terms for p2p push ${peerPushCreditId}`); let wg: WithdrawalGroupRecord | undefined = undefined; let wgOrt: OperationRetryRecord | undefined = undefined; @@ -440,7 +440,7 @@ export async function getTransactionById( } } const pushIncOpId = TaskIdentifiers.forPeerPushCredit(pushInc); - let pushIncOrt = await tx.operationRetries.get(pushIncOpId); + const pushIncOrt = await tx.operationRetries.get(pushIncOpId); return buildTransactionForPeerPushCredit( pushInc, @@ -468,7 +468,7 @@ export async function getTransactionById( const pushInc = await tx.peerPullCredit.get(pursePub); if (!pushInc) throw Error("not found"); const ct = await tx.contractTerms.get(pushInc.contractTermsHash); - checkDbInvariant(!!ct); + checkDbInvariant(!!ct, `no contract terms for p2p push ${pursePub}`); let wg: WithdrawalGroupRecord | undefined = undefined; let wgOrt: OperationRetryRecord | undefined = undefined; @@ -1034,8 +1034,8 @@ function buildTransactionForPurchase( })); const timestamp = purchaseRecord.timestampAccept; - checkDbInvariant(!!timestamp); - checkDbInvariant(!!purchaseRecord.payInfo); + checkDbInvariant(!!timestamp, `purchase ${purchaseRecord.orderId} without accepted time`); + checkDbInvariant(!!purchaseRecord.payInfo, `purchase ${purchaseRecord.orderId} without payinfo`); const txState = computePayMerchantTransactionState(purchaseRecord); return { @@ -1175,7 +1175,7 @@ export async function getTransactions( return; } const ct = await tx.contractTerms.get(pi.contractTermsHash); - checkDbInvariant(!!ct); + checkDbInvariant(!!ct, `no contract terms for p2p push ${pi.pursePub}`); transactions.push( buildTransactionForPushPaymentDebit(pi, ct.contractTermsRaw), ); @@ -1249,9 +1249,9 @@ export async function getTransactions( } } const pushIncOpId = TaskIdentifiers.forPeerPushCredit(pi); - let pushIncOrt = await tx.operationRetries.get(pushIncOpId); + const pushIncOrt = await tx.operationRetries.get(pushIncOpId); - checkDbInvariant(!!ct); + checkDbInvariant(!!ct, `no contract terms for p2p push ${pi.pursePub}`); transactions.push( buildTransactionForPeerPushCredit( pi, @@ -1283,9 +1283,9 @@ export async function getTransactions( } } const pushIncOpId = TaskIdentifiers.forPeerPullPaymentInitiation(pi); - let pushIncOrt = await tx.operationRetries.get(pushIncOpId); + const pushIncOrt = await tx.operationRetries.get(pushIncOpId); - checkDbInvariant(!!ct); + checkDbInvariant(!!ct, `no contract terms for p2p push ${pi.pursePub}`); transactions.push( buildTransactionForPeerPullCredit( pi, diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 86ea7bb4c..d98106d1f 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -479,7 +479,7 @@ async function setCoinSuspended( c.denomPubHash, c.maxAge, ]); - checkDbInvariant(!!coinAvailability); + checkDbInvariant(!!coinAvailability, `no denom info for ${c.denomPubHash} age ${c.maxAge}`); if (suspended) { if (c.status !== CoinStatus.Fresh) { return; @@ -1063,10 +1063,6 @@ async function dispatchRequestInternal( const req = codecForPreparePayTemplateRequest().decode(payload); return preparePayForTemplate(wex, req); } - case WalletApiOperation.CheckPayForTemplate: { - const req = codecForCheckPayTemplateRequest().decode(payload); - return checkPayForTemplate(wex, req); - } case WalletApiOperation.ConfirmPay: { const req = codecForConfirmPayRequest().decode(payload); let transactionId; @@ -1403,7 +1399,7 @@ async function dispatchRequestInternal( return; } wex.ws.exchangeCache.clear(); - checkDbInvariant(!!existingRec.id); + checkDbInvariant(!!existingRec.id, `no global exchange for ${j2s(key)}`); await tx.globalCurrencyExchanges.delete(existingRec.id); }, ); @@ -1452,7 +1448,7 @@ async function dispatchRequestInternal( if (!existingRec) { return; } - checkDbInvariant(!!existingRec.id); + checkDbInvariant(!!existingRec.id, `no global currency for ${j2s(key)}`); await tx.globalCurrencyAuditors.delete(existingRec.id); wex.ws.exchangeCache.clear(); }, diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts index 1dc4e0999..b2cecea16 100644 --- a/packages/taler-wallet-core/src/withdraw.ts +++ b/packages/taler-wallet-core/src/withdraw.ts @@ -965,7 +965,7 @@ async function processPlanchetGenerate( return getDenomInfo(wex, tx, exchangeBaseUrl, denomPubHash); }, ); - checkDbInvariant(!!denom); + checkDbInvariant(!!denom, `no denom info for ${denomPubHash}`); const r = await wex.cryptoApi.createPlanchet({ denomPub: denom.denomPub, feeWithdraw: Amounts.parseOrThrow(denom.feeWithdraw), @@ -2314,7 +2314,7 @@ export async function getFundingPaytoUris( withdrawalGroupId: string, ): Promise { const withdrawalGroup = await tx.withdrawalGroups.get(withdrawalGroupId); - checkDbInvariant(!!withdrawalGroup); + checkDbInvariant(!!withdrawalGroup, `no withdrawal for ${withdrawalGroupId}`); checkDbInvariant( withdrawalGroup.instructedAmount !== undefined, "can't get funding uri from uninitialized wg", diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts index 838739ad1..daba6aba4 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts @@ -94,9 +94,9 @@ export namespace State { currentAccount: PaytoUri; totalFee: AmountJson; - totalToDeposit: AmountJson; amount: AmountFieldHandler; + totalToDeposit: AmountFieldHandler; account: SelectFieldHandler; cancelHandler: ButtonHandler; depositHandler: ButtonHandler; diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts index 97b2ab517..b674665cf 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts @@ -15,19 +15,18 @@ */ import { - AmountJson, Amounts, - DepositGroupFees, KnownBankAccountsInfo, parsePaytoUri, PaytoUri, stringifyPaytoUri, + TransactionAmountMode } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { useState } from "preact/hooks"; 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 { RecursiveState } from "../../utils/index.js"; import { Props, State } from "./index.js"; @@ -83,8 +82,11 @@ export function useComponentState({ if (hook.hasError) { return { status: "error", - error: alertFromError(i18n, - i18n.str`Could not load balance information`, hook), + error: alertFromError( + i18n, + i18n.str`Could not load balance information`, + hook, + ), }; } const { accounts, balances } = hook.response; @@ -141,21 +143,23 @@ export function useComponentState({ } const firstAccount = accounts[0].uri; const currentAccount = !selectedAccount ? firstAccount : selectedAccount; - - return () => { - // eslint-disable-next-line react-hooks/rules-of-hooks - const [amount, setAmount] = useState( - initialValue ?? ({} as any), + const zero = Amounts.zeroOfCurrency(currency) + return (): State => { + const [instructed, setInstructed] = useState( + {amount: initialValue ?? zero, type: TransactionAmountMode.Raw}, ); - const amountStr = Amounts.stringify(amount); + const amountStr = Amounts.stringify(instructed.amount); const depositPaytoUri = stringifyPaytoUri(currentAccount); - // eslint-disable-next-line react-hooks/rules-of-hooks const hook = useAsyncAsHook(async () => { - const fee = await api.wallet.call(WalletApiOperation.PrepareDeposit, { - amount: amountStr, - depositPaytoUri, - }); + const fee = await api.wallet.call( + WalletApiOperation.ConvertDepositAmount, + { + amount: amountStr, + type: instructed.type, + depositPaytoUri, + }, + ); return { fee }; }, [amountStr, depositPaytoUri]); @@ -183,18 +187,16 @@ export function useComponentState({ const totalFee = fee !== undefined - ? Amounts.sum([fee.fees.wire, fee.fees.coin, fee.fees.refresh]).amount + ? Amounts.sub(fee.effectiveAmount, fee.rawAmount).amount : Amounts.zeroOfCurrency(currency); - const totalToDeposit = - fee !== undefined - ? Amounts.sub(amount, totalFee).amount - : Amounts.zeroOfCurrency(currency); + const totalToDeposit = Amounts.parseOrThrow(fee.rawAmount); + const totalEffective = Amounts.parseOrThrow(fee.effectiveAmount); - const isDirty = amount !== initialValue; + const isDirty = instructed.amount !== initialValue; const amountError = !isDirty ? undefined - : Amounts.cmp(balance, amount) === -1 + : Amounts.cmp(balance, totalEffective) === -1 ? `Too much, your current balance is ${Amounts.stringifyValue(balance)}` : undefined; @@ -207,7 +209,7 @@ export function useComponentState({ if (!currency) return; const depositPaytoUri = stringifyPaytoUri(currentAccount); - const amountStr = Amounts.stringify(amount); + const amountStr = Amounts.stringify(totalEffective); await api.wallet.call(WalletApiOperation.CreateDepositGroup, { amount: amountStr, depositPaytoUri, @@ -220,8 +222,19 @@ export function useComponentState({ error: undefined, currency, amount: { - value: amount, - onInput: pushAlertOnError(async (a) => setAmount(a)), + value: totalEffective, + onInput: pushAlertOnError(async (a) => setInstructed({ + amount: a, + type: TransactionAmountMode.Effective, + })), + error: amountError, + }, + totalToDeposit: { + value: totalToDeposit, + onInput: pushAlertOnError(async (a) => setInstructed({ + amount: a, + type: TransactionAmountMode.Raw, + })), error: amountError, }, onAddAccount: { @@ -244,7 +257,6 @@ export function useComponentState({ onClick: unableToDeposit ? undefined : pushAlertOnError(doSend), }, totalFee, - totalToDeposit, }; }; } @@ -269,7 +281,7 @@ export function createLabelsForBankAccount( ): { [value: string]: string } { const initialList: Record = {}; if (!knownBankAccounts.length) return initialList; - return knownBankAccounts.reduce((prev, cur, i) => { + return knownBankAccounts.reduce((prev, cur) => { prev[stringifyPaytoUri(cur.uri)] = cur.alias; return prev; }, initialList); diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx b/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx index c23f83fdd..0ed62220b 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx @@ -53,7 +53,10 @@ export const WithNoAccountForIBAN = tests.createExample(ReadyView, { onClick: nullFunction, }, totalFee: Amounts.zeroOfCurrency("USD"), - totalToDeposit: Amounts.parseOrThrow("USD:10"), + totalToDeposit: { + onInput:nullFunction, + value: Amounts.parseOrThrow("USD:10"), + }, // onCalculateFee: alwaysReturnFeeToOne, }); @@ -82,7 +85,10 @@ export const WithIBANAccountTypeSelected = tests.createExample(ReadyView, { onClick: nullFunction, }, totalFee: Amounts.zeroOfCurrency("USD"), - totalToDeposit: Amounts.parseOrThrow("USD:10"), + totalToDeposit: { + onInput:nullFunction, + value: Amounts.parseOrThrow("USD:10"), + }, // onCalculateFee: alwaysReturnFeeToOne, }); @@ -111,6 +117,9 @@ export const NewBitcoinAccountTypeSelected = tests.createExample(ReadyView, { onClick: nullFunction, }, totalFee: Amounts.zeroOfCurrency("USD"), - totalToDeposit: Amounts.parseOrThrow("USD:10"), + totalToDeposit: { + onInput:nullFunction, + value: Amounts.parseOrThrow("USD:10"), + }, // onCalculateFee: alwaysReturnFeeToOne, }); diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts index 157cb868a..1144095e1 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts @@ -20,17 +20,16 @@ */ import { + AmountResponse, Amounts, AmountString, - DepositGroupFees, parsePaytoUri, - PrepareDepositResponse, ScopeType, - stringifyPaytoUri, + stringifyPaytoUri } 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 { expect } from "chai"; import { nullFunction } from "../../mui/handlers.js"; import { createWalletApiMock } from "../../test-utils.js"; @@ -38,24 +37,14 @@ import { useComponentState } from "./state.js"; const currency = "EUR"; const amount = `${currency}:0`; -const withoutFee = (): PrepareDepositResponse => ({ - effectiveDepositAmount: `${currency}:5` as AmountString, - totalDepositCost: `${currency}:5` as AmountString, - fees: { - coin: Amounts.stringify(`${currency}:0`), - wire: Amounts.stringify(`${currency}:0`), - refresh: Amounts.stringify(`${currency}:0`), - }, +const withoutFee = (value: number): AmountResponse => ({ + effectiveAmount: `${currency}:${value}` as AmountString, + rawAmount: `${currency}:${value}` as AmountString, }); -const withSomeFee = (): PrepareDepositResponse => ({ - effectiveDepositAmount: `${currency}:5` as AmountString, - totalDepositCost: `${currency}:5` as AmountString, - fees: { - coin: Amounts.stringify(`${currency}:1`), - wire: Amounts.stringify(`${currency}:1`), - refresh: Amounts.stringify(`${currency}:1`), - }, +const withSomeFee = (value: number, fee: number): AmountResponse => ({ + effectiveAmount: `${currency}:${value}` as AmountString, + rawAmount: `${currency}:${value - fee}` as AmountString, }); describe("DepositPage states", () => { @@ -195,9 +184,9 @@ describe("DepositPage states", () => { }, ); handler.addWalletCallResponse( - WalletApiOperation.PrepareDeposit, + WalletApiOperation.ConvertDepositAmount, undefined, - withoutFee(), + withoutFee(0), ); const hookBehavior = await tests.hookBehaveLikeThis( @@ -255,15 +244,15 @@ describe("DepositPage states", () => { }, ); handler.addWalletCallResponse( - WalletApiOperation.PrepareDeposit, + WalletApiOperation.ConvertDepositAmount, undefined, - withoutFee(), + withoutFee(0), ); handler.addWalletCallResponse( - WalletApiOperation.PrepareDeposit, + WalletApiOperation.ConvertDepositAmount, undefined, - withoutFee(), + withoutFee(0), ); const accountSelected = stringifyPaytoUri(ibanPayto.uri); @@ -345,19 +334,19 @@ describe("DepositPage states", () => { }, ); handler.addWalletCallResponse( - WalletApiOperation.PrepareDeposit, + WalletApiOperation.ConvertDepositAmount, undefined, - withoutFee(), + withoutFee(0), ); handler.addWalletCallResponse( - WalletApiOperation.PrepareDeposit, + WalletApiOperation.ConvertDepositAmount, undefined, - withSomeFee(), + withSomeFee(10,3), ); handler.addWalletCallResponse( - WalletApiOperation.PrepareDeposit, + WalletApiOperation.ConvertDepositAmount, undefined, - withSomeFee(), + withSomeFee(10,3), ); const accountSelected = stringifyPaytoUri(ibanPayto.uri); @@ -404,7 +393,7 @@ describe("DepositPage states", () => { expect(state.account.value).eq(accountSelected); expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:10")); expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)); - expect(state.totalToDeposit).deep.eq( + expect(state.totalToDeposit.value).deep.eq( Amounts.parseOrThrow(`${currency}:7`), ); expect(state.depositHandler.onClick).not.undefined; @@ -416,7 +405,7 @@ describe("DepositPage states", () => { expect(state.account.value).eq(accountSelected); expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:10")); expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)); - expect(state.totalToDeposit).deep.eq( + expect(state.totalToDeposit.value).deep.eq( Amounts.parseOrThrow(`${currency}:7`), ); expect(state.depositHandler.onClick).not.undefined; diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx b/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx index 37f1c252d..b3607ebba 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx @@ -26,7 +26,7 @@ import { Grid } from "../../mui/Grid.js"; import { State } from "./index.js"; export function AmountOrCurrencyErrorView( - p: State.AmountOrCurrencyError, + _p: State.AmountOrCurrencyError, ): VNode { const { i18n } = useTranslationContext(); @@ -156,12 +156,7 @@ export function ReadyView(state: State.Ready): VNode { /> - + @@ -180,7 +175,7 @@ export function ReadyView(state: State.Ready): VNode { ) : ( diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx b/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx index 7b80977f3..b995a44d0 100644 --- a/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx +++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx @@ -130,7 +130,6 @@ export function ReadyView({ ))}
- --- {uri.value} ---