From 60a4640a35be584ee004de5e362f21ed03fa239e Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 30 May 2024 13:16:15 -0300 Subject: working #8882 --- .../test-timetravel-autorefresh.ts | 10 +- .../test-withdrawal-bank-integrated.ts | 21 +- .../src/integrationtests/test-withdrawal-fees.ts | 10 +- packages/taler-util/src/transactions-types.ts | 2 +- packages/taler-util/src/wallet-types.ts | 4 +- packages/taler-wallet-core/src/balance.ts | 12 + packages/taler-wallet-core/src/db.ts | 4 +- packages/taler-wallet-core/src/transactions.ts | 51 +++- packages/taler-wallet-core/src/wallet.ts | 5 +- packages/taler-wallet-core/src/withdraw.ts | 301 ++++++++++++--------- .../src/components/HistoryItem.tsx | 4 +- .../src/cta/Withdraw/state.ts | 33 +-- .../src/cta/Withdraw/test.ts | 5 +- 13 files changed, 277 insertions(+), 185 deletions(-) (limited to 'packages') diff --git a/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts b/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts index f3e3802e5..046bd5aed 100644 --- a/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts +++ b/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts @@ -24,6 +24,7 @@ import { NotificationType, PreparePayResultType, TalerCorebankApiClient, + j2s, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { makeNoFeeCoinConfig } from "../harness/denomStructures.js"; @@ -149,12 +150,15 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) { }); t.logStep("wait"); await wres.withdrawalFinishedCond; - const exchangeUpdated1Cond = walletClient.waitForNotificationCond( (x) => - x.type === NotificationType.ExchangeStateTransition && - x.exchangeBaseUrl === exchange.baseUrl, + { + t.logStep(`EXCHANGE UPDATE, ${j2s(x)}`) + return x.type === NotificationType.ExchangeStateTransition && + x.exchangeBaseUrl === exchange.baseUrl + } ); + t.logStep("waiting tx"); await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); { diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts index a13095883..3ec2a3bcd 100644 --- a/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts +++ b/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts @@ -46,7 +46,7 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) { "TESTKUDOS:10", ); - // Hand it to the wallet + t.logStep("Hand it to the wallet") const r1 = await walletClient.client.call( WalletApiOperation.GetWithdrawalDetailsForUri, @@ -55,7 +55,7 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) { }, ); - // Withdraw + t.logStep("Withdraw") const r2 = await walletClient.client.call( WalletApiOperation.AcceptBankIntegratedWithdrawal, @@ -65,6 +65,7 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) { }, ); + t.logStep("wait confirmed") const withdrawalBankConfirmedCond = walletClient.waitForNotificationCond( (x) => { return ( @@ -76,6 +77,7 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) { }, ); + t.logStep("wait finished") const withdrawalFinishedCond = walletClient.waitForNotificationCond((x) => { return ( x.type === NotificationType.TransactionStateTransition && @@ -84,6 +86,7 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) { ); }); + t.logStep("wait withdraw coins") const withdrawalReserveReadyCond = walletClient.waitForNotificationCond( (x) => { return ( @@ -95,7 +98,7 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) { }, ); - // Do it twice to check idempotency + t.logStep("Do it twice to check idempotency") const r3 = await walletClient.client.call( WalletApiOperation.AcceptBankIntegratedWithdrawal, { @@ -104,9 +107,10 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) { }, ); + t.logStep("stop wirewatch") await exchange.stopWirewatch(); - // Check status before withdrawal is confirmed by bank. + t.logStep("Check status before withdrawal is confirmed by bank.") { const txn = await walletClient.client.call( WalletApiOperation.GetTransactions, @@ -122,7 +126,7 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) { t.assertTrue(tx0.withdrawalDetails.reserveIsReady === false); } - // Confirm it + t.logStep("Confirm it") await bankClient.confirmWithdrawalOperation(user.username, { withdrawalOperationId: wop.withdrawal_id, @@ -132,6 +136,7 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) { // Check status after withdrawal is confirmed by bank, // but before funds are wired to the exchange. + t.logStep("Check status after withdrawal") { const txn = await walletClient.client.call( WalletApiOperation.GetTransactions, @@ -147,11 +152,13 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) { t.assertTrue(tx0.withdrawalDetails.reserveIsReady === false); } + t.logStep("start wirewatch") await exchange.startWirewatch(); + t.logStep("wait reserve") await withdrawalReserveReadyCond; - // Check status after funds were wired. + t.logStep("Check status after funds were wired.") { const txn = await walletClient.client.call( WalletApiOperation.GetTransactions, @@ -169,7 +176,7 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) { await withdrawalFinishedCond; - // Check balance + t.logStep("Check balance") const balResp = await walletClient.client.call( WalletApiOperation.GetBalances, diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts index 8a2268231..0657d2da7 100644 --- a/packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts +++ b/packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts @@ -138,7 +138,7 @@ export async function runWithdrawalFeesTest(t: GlobalTestState) { bankClient.setAuth(user); const wop = await bankClient.createWithdrawalOperation(user.username, amount); - // Hand it to the wallet + t.logStep("Hand it to the wallet") const details = await wallet.client.call( WalletApiOperation.GetWithdrawalDetailsForUri, @@ -165,23 +165,25 @@ export async function runWithdrawalFeesTest(t: GlobalTestState) { t.assertAmountEquals(amountDetails.amountEffective, "TESTKUDOS:5"); t.assertAmountEquals(amountDetails.amountRaw, "TESTKUDOS:7.5"); + t.logStep("Complete all pending operations") + await wallet.runPending(); - // Withdraw (AKA select) + t.logStep("Withdraw (AKA select)") await wallet.client.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, { exchangeBaseUrl: exchange.baseUrl, talerWithdrawUri: wop.taler_withdraw_uri, }); - // Confirm it + t.logStep("Confirm it") await bankClient.confirmWithdrawalOperation(user.username, { withdrawalOperationId: wop.withdrawal_id, }); await wallet.runUntilDone(); - // Check balance + t.logStep("Check balance") const balResp = await wallet.client.call(WalletApiOperation.GetBalances, {}); console.log(j2s(balResp)); diff --git a/packages/taler-util/src/transactions-types.ts b/packages/taler-util/src/transactions-types.ts index cee3de9fa..db2133944 100644 --- a/packages/taler-util/src/transactions-types.ts +++ b/packages/taler-util/src/transactions-types.ts @@ -324,7 +324,7 @@ export interface TransactionWithdrawal extends TransactionCommon { /** * Exchange of the withdrawal. */ - exchangeBaseUrl: string; + exchangeBaseUrl: string | undefined; /** * Amount that got subtracted from the reserve balance. diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts index a7cd65fa2..66b1e9769 100644 --- a/packages/taler-util/src/wallet-types.ts +++ b/packages/taler-util/src/wallet-types.ts @@ -1850,18 +1850,16 @@ export interface GetWithdrawalDetailsForAmountRequest { export interface PrepareBankIntegratedWithdrawalRequest { talerWithdrawUri: string; - selectedExchange?: string; } export const codecForPrepareBankIntegratedWithdrawalRequest = (): Codec => buildCodecForObject() .property("talerWithdrawUri", codecForString()) - .property("selectedExchange", codecOptional(codecForString())) .build("PrepareBankIntegratedWithdrawalRequest"); export interface PrepareBankIntegratedWithdrawalResponse { - transactionId?: string; + transactionId: TransactionIdStr; info: WithdrawUriInfoResponse; } diff --git a/packages/taler-wallet-core/src/balance.ts b/packages/taler-wallet-core/src/balance.ts index 76e604324..4f06e3756 100644 --- a/packages/taler-wallet-core/src/balance.ts +++ b/packages/taler-wallet-core/src/balance.ts @@ -379,6 +379,10 @@ export async function getBalancesInsideTransaction( wg.denomsSel !== undefined, "wg in kyc state should have been initialized", ); + checkDbInvariant( + wg.exchangeBaseUrl !== undefined, + "wg in kyc state should have been initialized", + ); const currency = Amounts.currencyOf(wg.denomsSel.totalCoinValue); await balanceStore.setFlagIncomingKyc(currency, wg.exchangeBaseUrl); break; @@ -389,6 +393,10 @@ export async function getBalancesInsideTransaction( wg.denomsSel !== undefined, "wg in aml state should have been initialized", ); + checkDbInvariant( + wg.exchangeBaseUrl !== undefined, + "wg in kyc state should have been initialized", + ); const currency = Amounts.currencyOf(wg.denomsSel.totalCoinValue); await balanceStore.setFlagIncomingAml(currency, wg.exchangeBaseUrl); break; @@ -408,6 +416,10 @@ export async function getBalancesInsideTransaction( wg.denomsSel !== undefined, "wg in confirmed state should have been initialized", ); + checkDbInvariant( + wg.exchangeBaseUrl !== undefined, + "wg in kyc state should have been initialized", + ); const currency = Amounts.currencyOf(wg.denomsSel.totalCoinValue); await balanceStore.setFlagIncomingConfirmation( currency, diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index ad9b4f1cb..4ec83a783 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -395,6 +395,8 @@ export interface ReserveBankInfo { timestampBankConfirmed: DbPreciseTimestamp | undefined; wireTypes: string[] | undefined; + + currency: string | undefined; } /** @@ -1531,7 +1533,7 @@ export interface WithdrawalGroupRecord { * The exchange base URL that we're withdrawing from. * (Redundantly stored, as the reserve record also has this info.) */ - exchangeBaseUrl: string; + exchangeBaseUrl?: string; /** * When was the withdrawal operation started started? diff --git a/packages/taler-wallet-core/src/transactions.ts b/packages/taler-wallet-core/src/transactions.ts index 72aff319a..bcf3fcaf6 100644 --- a/packages/taler-wallet-core/src/transactions.ts +++ b/packages/taler-wallet-core/src/transactions.ts @@ -243,11 +243,14 @@ export async function getTransactionById( const opId = TaskIdentifiers.forWithdrawal(withdrawalGroupRecord); const ort = await tx.operationRetries.get(opId); - const exchangeDetails = await getExchangeWireDetailsInTx( - tx, - withdrawalGroupRecord.exchangeBaseUrl, - ); - if (!exchangeDetails) throw Error("not exchange details"); + const exchangeDetails = + withdrawalGroupRecord.exchangeBaseUrl === undefined + ? undefined + : await getExchangeWireDetailsInTx( + tx, + withdrawalGroupRecord.exchangeBaseUrl, + ); + // if (!exchangeDetails) throw Error("not exchange details"); if ( withdrawalGroupRecord.wgInfo.withdrawalType === @@ -259,7 +262,10 @@ export async function getTransactionById( ort, ); } - + checkDbInvariant( + exchangeDetails !== undefined, + "manual withdrawal without exchange", + ); return buildTransactionForManualWithdraw( withdrawalGroupRecord, exchangeDetails, @@ -404,7 +410,10 @@ 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, `no contract terms for p2p push ${parsedTx.pursePub}`); + checkDbInvariant( + !!ct, + `no contract terms for p2p push ${parsedTx.pursePub}`, + ); return buildTransactionForPushPaymentDebit( debit, ct.contractTermsRaw, @@ -428,7 +437,10 @@ 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, `no contract terms for p2p push ${peerPushCreditId}`); + checkDbInvariant( + !!ct, + `no contract terms for p2p push ${peerPushCreditId}`, + ); let wg: WithdrawalGroupRecord | undefined = undefined; let wgOrt: OperationRetryRecord | undefined = undefined; @@ -593,6 +605,7 @@ function buildTransactionForPeerPullCredit( const txState = computePeerPullCreditTransactionState(pullCredit); checkDbInvariant(wsr.instructedAmount !== undefined, "wg uninitialized"); checkDbInvariant(wsr.denomsSel !== undefined, "wg uninitialized"); + checkDbInvariant(wsr.exchangeBaseUrl !== undefined, "wg uninitialized"); return { type: TransactionType.PeerPullCredit, txState, @@ -667,6 +680,7 @@ function buildTransactionForPeerPushCredit( } checkDbInvariant(wg.instructedAmount !== undefined, "wg uninitialized"); checkDbInvariant(wg.denomsSel !== undefined, "wg uninitialized"); + checkDbInvariant(wg.exchangeBaseUrl !== undefined, "wg uninitialized"); const txState = computePeerPushCreditTransactionState(pushInc); return { @@ -719,15 +733,17 @@ function buildTransactionForPeerPushCredit( function buildTransactionForBankIntegratedWithdraw( wg: WithdrawalGroupRecord, - exchangeDetails: ExchangeWireDetails, + exchangeDetails: ExchangeWireDetails | undefined, ort?: OperationRetryRecord, ): TransactionWithdrawal { if (wg.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) { throw Error(""); } + checkDbInvariant(wg.wgInfo.bankInfo.currency !== undefined, "wg uninitialized"); const txState = computeWithdrawalTransactionStatus(wg); + const zero = Amounts.stringify( - Amounts.zeroOfCurrency(exchangeDetails.currency), + Amounts.zeroOfCurrency(wg.wgInfo.bankInfo.currency), ); return { type: TransactionType.Withdrawal, @@ -784,6 +800,7 @@ function buildTransactionForManualWithdraw( checkDbInvariant(wg.instructedAmount !== undefined, "wg uninitialized"); checkDbInvariant(wg.denomsSel !== undefined, "wg uninitialized"); + checkDbInvariant(wg.exchangeBaseUrl !== undefined, "wg uninitialized"); const exchangePaytoUris = augmentPaytoUrisForWithdrawal( plainPaytoUris, wg.reservePub, @@ -1034,8 +1051,14 @@ function buildTransactionForPurchase( })); const timestamp = purchaseRecord.timestampAccept; - checkDbInvariant(!!timestamp, `purchase ${purchaseRecord.orderId} without accepted time`); - checkDbInvariant(!!purchaseRecord.payInfo, `purchase ${purchaseRecord.orderId} without payinfo`); + checkDbInvariant( + !!timestamp, + `purchase ${purchaseRecord.orderId} without accepted time`, + ); + checkDbInvariant( + !!purchaseRecord.payInfo, + `purchase ${purchaseRecord.orderId} without payinfo`, + ); const txState = computePayMerchantTransactionState(purchaseRecord); return { @@ -1089,6 +1112,10 @@ export async function getWithdrawalTransactionByUri( if (!withdrawalGroupRecord) { return undefined; } + if (withdrawalGroupRecord.exchangeBaseUrl === undefined) { + // prepared and unconfirmed withdrawals are hidden + return undefined; + } const opId = TaskIdentifiers.forWithdrawal(withdrawalGroupRecord); const ort = await tx.operationRetries.get(opId); diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index d98106d1f..f1d53b7d5 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -1010,10 +1010,7 @@ async function dispatchRequestInternal( case WalletApiOperation.PrepareBankIntegratedWithdrawal: { const req = codecForPrepareBankIntegratedWithdrawalRequest().decode(payload); - return prepareBankIntegratedWithdrawal(wex, { - talerWithdrawUri: req.talerWithdrawUri, - selectedExchange: req.selectedExchange, - }); + return prepareBankIntegratedWithdrawal(wex, req); } case WalletApiOperation.GetExchangeTos: { const req = codecForGetExchangeTosRequest().decode(payload); diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts index 24e2861e1..0e7ed144c 100644 --- a/packages/taler-wallet-core/src/withdraw.ts +++ b/packages/taler-wallet-core/src/withdraw.ts @@ -344,9 +344,11 @@ export class WithdrawTransactionContext implements TransactionContext { "exchanges" as const, "exchangeDetails" as const, ]; - let stores = opts.extraStores + const stores = opts.extraStores ? [...baseStores, ...opts.extraStores] : baseStores; + + let errorThrown: Error | undefined; const transitionInfo = await this.wex.db.runReadWriteTx( { storeNames: stores }, async (tx) => { @@ -359,7 +361,17 @@ export class WithdrawTransactionContext implements TransactionContext { major: TransactionMajorState.None, }; } - const res = await f(wgRec, tx); + let res: TransitionResult | undefined; + try { + res = await f(wgRec, tx); + } catch (error) { + if (error instanceof Error) { + errorThrown = error; + } + return undefined; + } + + // const res = await f(wgRec, tx); switch (res.type) { case TransitionResultType.Transition: { await tx.withdrawalGroups.put(res.rec); @@ -384,6 +396,9 @@ export class WithdrawTransactionContext implements TransactionContext { } }, ); + if (errorThrown) { + throw errorThrown; + } notifyTransition(this.wex, this.transactionId, transitionInfo); return transitionInfo; } @@ -929,6 +944,10 @@ async function processPlanchetGenerate( withdrawalGroup.denomsSel !== undefined, "can't process uninitialized exchange", ); + checkDbInvariant( + withdrawalGroup.exchangeBaseUrl !== undefined, + "can't get funding uri from uninitialized wg", + ); const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl; let planchet = await wex.db.runReadOnlyTx( { storeNames: ["planchets"] }, @@ -1133,6 +1152,10 @@ async function processPlanchetExchangeBatchRequest( logger.info( `processing planchet exchange batch request ${withdrawalGroup.withdrawalGroupId}, start=${args.coinStartIndex}, len=${args.batchSize}`, ); + checkDbInvariant( + withdrawalGroup.exchangeBaseUrl !== undefined, + "can't get funding uri from uninitialized wg", + ); const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl; const batchReq: ExchangeBatchWithdrawRequest = { planchets: [] }; @@ -1268,6 +1291,10 @@ async function processPlanchetVerifyAndStoreCoin( resp: ExchangeWithdrawResponse, ): Promise { const withdrawalGroup = wgContext.wgRecord; + checkDbInvariant( + withdrawalGroup.exchangeBaseUrl !== undefined, + "can't get funding uri from uninitialized wg", + ); const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl; logger.trace(`checking and storing planchet idx=${coinIdx}`); @@ -1516,6 +1543,10 @@ async function processQueryReserve( if (withdrawalGroup.status !== WithdrawalGroupStatus.PendingQueryingStatus) { return TaskRunResult.backoff(); } + checkDbInvariant( + withdrawalGroup.exchangeBaseUrl !== undefined, + "can't get funding uri from uninitialized wg", + ); checkDbInvariant( withdrawalGroup.denomsSel !== undefined, "can't process uninitialized exchange", @@ -1751,6 +1782,10 @@ async function redenominateWithdrawal( if (!wg) { return; } + checkDbInvariant( + wg.exchangeBaseUrl !== undefined, + "can't get funding uri from uninitialized wg", + ); checkDbInvariant( wg.denomsSel !== undefined, "can't process uninitialized exchange", @@ -1894,6 +1929,10 @@ async function processWithdrawalGroupPendingReady( withdrawalGroup.denomsSel !== undefined, "can't process uninitialized exchange", ); + checkDbInvariant( + withdrawalGroup.exchangeBaseUrl !== undefined, + "can't get funding uri from uninitialized wg", + ); const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl; await fetchFreshExchange(wex, withdrawalGroup.exchangeBaseUrl); @@ -2320,6 +2359,10 @@ export async function getFundingPaytoUris( ): Promise { const withdrawalGroup = await tx.withdrawalGroups.get(withdrawalGroupId); checkDbInvariant(!!withdrawalGroup, `no withdrawal for ${withdrawalGroupId}`); + checkDbInvariant( + withdrawalGroup.exchangeBaseUrl !== undefined, + "can't get funding uri from uninitialized wg", + ); checkDbInvariant( withdrawalGroup.instructedAmount !== undefined, "can't get funding uri from uninitialized wg", @@ -2675,7 +2718,7 @@ export async function internalPrepareCreateWithdrawalGroup( args: { reserveStatus: WithdrawalGroupStatus; amount?: AmountJson; - exchangeBaseUrl: string; + exchangeBaseUrl: string | undefined; forcedWithdrawalGroupId?: string; forcedDenomSel?: ForcedDenomSel; reserveKeyPair?: EddsaKeypair; @@ -2716,7 +2759,7 @@ export async function internalPrepareCreateWithdrawalGroup( let initialDenomSel: DenomSelectionState | undefined; const denomSelUid = encodeCrock(getRandomBytes(16)); - if (amount !== undefined) { + if (amount !== undefined && exchangeBaseUrl !== undefined) { initialDenomSel = await getInitialDenomsSelection( wex, exchangeBaseUrl, @@ -2747,7 +2790,9 @@ export async function internalPrepareCreateWithdrawalGroup( wgInfo: args.wgInfo, }; - await fetchFreshExchange(wex, exchangeBaseUrl); + if (exchangeBaseUrl !== undefined) { + await fetchFreshExchange(wex, exchangeBaseUrl); + } const transactionId = constructTransactionIdentifier({ tag: TransactionType.Withdrawal, @@ -2757,12 +2802,13 @@ export async function internalPrepareCreateWithdrawalGroup( return { withdrawalGroup, transactionId, - creationInfo: !amount - ? undefined - : { - amount, - canonExchange: exchangeBaseUrl, - }, + creationInfo: + !amount || !exchangeBaseUrl + ? undefined + : { + amount, + canonExchange: exchangeBaseUrl, + }, }; } @@ -2792,8 +2838,8 @@ export async function internalPerformCreateWithdrawalGroup( if (existingWg) { return { withdrawalGroup: existingWg, - exchangeNotif: undefined, transitionInfo: undefined, + exchangeNotif: undefined, }; } await tx.withdrawalGroups.add(withdrawalGroup); @@ -2809,7 +2855,21 @@ export async function internalPerformCreateWithdrawalGroup( exchangeNotif: undefined, }; } - const exchange = await tx.exchanges.get(prep.creationInfo.canonExchange); + return internalPerformExchangeWasUsed( + wex, + tx, + prep.creationInfo.canonExchange, + withdrawalGroup, + ); +} + +export async function internalPerformExchangeWasUsed( + wex: WalletExecutionContext, + tx: WalletDbReadWriteTransaction<["exchanges"]>, + canonExchange: string, + withdrawalGroup: WithdrawalGroupRecord, +): Promise { + const exchange = await tx.exchanges.get(canonExchange); if (exchange) { exchange.lastWithdrawal = timestampPreciseToDb(TalerPreciseTimestamp.now()); await tx.exchanges.put(exchange); @@ -2825,11 +2885,7 @@ export async function internalPerformCreateWithdrawalGroup( newTxState, }; - const exchangeUsedRes = await markExchangeUsed( - wex, - tx, - prep.creationInfo.canonExchange, - ); + const exchangeUsedRes = await markExchangeUsed(wex, tx, canonExchange); const ctx = new WithdrawTransactionContext( wex, @@ -2857,7 +2913,7 @@ export async function internalCreateWithdrawalGroup( wex: WalletExecutionContext, args: { reserveStatus: WithdrawalGroupStatus; - exchangeBaseUrl: string; + exchangeBaseUrl: string | undefined; amount?: AmountJson; forcedWithdrawalGroupId?: string; forcedDenomSel?: ForcedDenomSel; @@ -2903,7 +2959,6 @@ export async function prepareBankIntegratedWithdrawal( wex: WalletExecutionContext, req: { talerWithdrawUri: string; - selectedExchange?: string; }, ): Promise { const existingWithdrawalGroup = await wex.db.runReadOnlyTx( @@ -2932,12 +2987,6 @@ export async function prepareBankIntegratedWithdrawal( const info = await getWithdrawalDetailsForUri(wex, req.talerWithdrawUri); - const exchangeBaseUrl = - req.selectedExchange ?? withdrawInfo.suggestedExchange; - if (!exchangeBaseUrl) { - return { info }; - } - /** * Withdrawal group without exchange and amount * this is an special case when the user haven't yet @@ -2946,7 +2995,7 @@ export async function prepareBankIntegratedWithdrawal( * same URI */ const withdrawalGroup = await internalCreateWithdrawalGroup(wex, { - exchangeBaseUrl, + exchangeBaseUrl: undefined, wgInfo: { withdrawalType: WithdrawalRecordType.BankIntegrated, bankInfo: { @@ -2955,6 +3004,7 @@ export async function prepareBankIntegratedWithdrawal( timestampBankConfirmed: undefined, timestampReserveInfoPosted: undefined, wireTypes: withdrawInfo.wireTypes, + currency: withdrawInfo.currency, }, }, reserveStatus: WithdrawalGroupStatus.DialogProposed, @@ -2977,6 +3027,9 @@ export async function confirmWithdrawal( req: ConfirmWithdrawalRequest, ): Promise { const parsedTx = parseTransactionIdentifier(req.transactionId); + const selectedExchange = req.exchangeBaseUrl; + const instructedAmount = Amounts.parseOrThrow(req.amount); + if (parsedTx?.tag !== TransactionType.Withdrawal) { throw Error("invalid withdrawal transaction ID"); } @@ -2998,7 +3051,6 @@ export async function confirmWithdrawal( throw Error("not a bank integrated withdrawal"); } - const selectedExchange = req.exchangeBaseUrl; const exchange = await fetchFreshExchange(wex, selectedExchange); const talerWithdrawUri = withdrawalGroup.wgInfo.bankInfo.talerWithdrawUri; @@ -3006,30 +3058,36 @@ export async function confirmWithdrawal( /** * The only reason this could be undefined is because it is an old wallet - * database before adding the wireType field was added + * database before adding the prepareWithdrawal feature */ - let wtypes: string[]; - if (withdrawalGroup.wgInfo.bankInfo.wireTypes === undefined) { + let bankWireTypes: string[]; + let bankCurrency: string; + if ( + withdrawalGroup.wgInfo.bankInfo.wireTypes === undefined || + withdrawalGroup.wgInfo.bankInfo.currency === undefined + ) { const withdrawInfo = await getBankWithdrawalInfo( wex.http, talerWithdrawUri, ); - wtypes = withdrawInfo.wireTypes; + bankWireTypes = withdrawInfo.wireTypes; + bankCurrency = withdrawInfo.currency; } else { - wtypes = withdrawalGroup.wgInfo.bankInfo.wireTypes; + bankWireTypes = withdrawalGroup.wgInfo.bankInfo.wireTypes; + bankCurrency = withdrawalGroup.wgInfo.bankInfo.currency; } const exchangePaytoUri = await getExchangePaytoUri( wex, selectedExchange, - wtypes, + bankWireTypes, ); const withdrawalAccountList = await fetchWithdrawalAccountInfo( wex, { exchange, - instructedAmount: Amounts.parseOrThrow(req.amount), + instructedAmount, }, wex.cancellationToken, ); @@ -3040,23 +3098,34 @@ export async function confirmWithdrawal( ); const initalDenoms = await getInitialDenomsSelection( wex, - req.exchangeBaseUrl, - Amounts.parseOrThrow(req.amount), + exchange.exchangeBaseUrl, + instructedAmount, req.forcedDenomSel, ); + let pending = false; await ctx.transition({}, async (rec) => { if (!rec) { return TransitionResult.stay(); } switch (rec.status) { + case WithdrawalGroupStatus.PendingWaitConfirmBank: { + pending = true; + return TransitionResult.stay(); + } + case WithdrawalGroupStatus.AbortedOtherWallet: { + throw TalerError.fromDetail( + TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK, + {}, + ); + } case WithdrawalGroupStatus.DialogProposed: { - rec.exchangeBaseUrl = req.exchangeBaseUrl; + rec.exchangeBaseUrl = exchange.exchangeBaseUrl; rec.instructedAmount = req.amount; + rec.restrictAge = req.restrictAge; rec.denomsSel = initalDenoms; rec.rawWithdrawalAmount = initalDenoms.totalWithdrawCost; rec.effectiveWithdrawalAmount = initalDenoms.totalCoinValue; - rec.restrictAge = req.restrictAge; rec.wgInfo = { withdrawalType: WithdrawalRecordType.BankIntegrated, @@ -3067,19 +3136,50 @@ export async function confirmWithdrawal( confirmUrl: confirmUrl, timestampBankConfirmed: undefined, timestampReserveInfoPosted: undefined, - wireTypes: wtypes, + wireTypes: bankWireTypes, + currency: bankCurrency, }, }; - + pending = true; rec.status = WithdrawalGroupStatus.PendingRegisteringBank; return TransitionResult.transition(rec); } - default: - throw Error("unable to confirm withdrawal in current state"); + default: { + throw Error( + `unable to confirm withdrawal in current state: ${rec.status}`, + ); + } } }); await wex.taskScheduler.resetTaskRetries(ctx.taskId); + + wex.ws.notify({ + type: NotificationType.BalanceChange, + hintTransactionId: ctx.transactionId, + }); + + const res = await wex.db.runReadWriteTx( + { + storeNames: ["exchanges"], + }, + async (tx) => { + const r = await internalPerformExchangeWasUsed( + wex, + tx, + exchange.exchangeBaseUrl, + withdrawalGroup, + ); + return r; + }, + ); + if (res.exchangeNotif) { + wex.ws.notify(res.exchangeNotif); + } + + if (pending) { + await waitWithdrawalRegistered(wex, ctx); + } } /** @@ -3104,112 +3204,57 @@ export async function acceptWithdrawalFromUri( ): Promise { const selectedExchange = req.selectedExchange; logger.info( - `accepting withdrawal via ${req.talerWithdrawUri}, canonicalized selected exchange ${selectedExchange}`, - ); - const existingWithdrawalGroup = await wex.db.runReadOnlyTx( - { storeNames: ["withdrawalGroups"] }, - async (tx) => { - return await tx.withdrawalGroups.indexes.byTalerWithdrawUri.get( - req.talerWithdrawUri, - ); - }, + `preparing withdrawal via ${req.talerWithdrawUri}, canonicalized selected exchange ${selectedExchange}`, ); - if (existingWithdrawalGroup) { - let url: string | undefined; - if ( - existingWithdrawalGroup.wgInfo.withdrawalType === - WithdrawalRecordType.BankIntegrated - ) { - url = existingWithdrawalGroup.wgInfo.bankInfo.confirmUrl; - } - return { - reservePub: existingWithdrawalGroup.reservePub, - confirmTransferUrl: url, - transactionId: constructTransactionIdentifier({ - tag: TransactionType.Withdrawal, - withdrawalGroupId: existingWithdrawalGroup.withdrawalGroupId, - }), - }; - } - - const exchange = await fetchFreshExchange(wex, selectedExchange); - const withdrawInfo = await getBankWithdrawalInfo( - wex.http, - req.talerWithdrawUri, - ); - const exchangePaytoUri = await getExchangePaytoUri( - wex, - selectedExchange, - withdrawInfo.wireTypes, - ); + const p = await prepareBankIntegratedWithdrawal(wex, { + talerWithdrawUri: req.talerWithdrawUri, + }); - let amount: AmountJson; - if (withdrawInfo.amount == null) { + let amount: AmountString; + if (p.info.amount == null) { if (req.amount == null) { throw Error( "amount required, as withdrawal operation has flexible amount", ); } - amount = Amounts.parseOrThrow(req.amount); + amount = req.amount as AmountString; } else { - if ( - req.amount != null && - Amounts.cmp(req.amount, withdrawInfo.amount) != 0 - ) { + if (req.amount != null && Amounts.cmp(req.amount, p.info.amount) != 0) { throw Error( "mismatched amount, amount is fixed by bank but client provided different amount", ); } - amount = withdrawInfo.amount; + amount = p.info.amount; } - const withdrawalAccountList = await fetchWithdrawalAccountInfo( - wex, - { - exchange, - instructedAmount: amount, - }, - CancellationToken.CONTINUE, - ); - - const withdrawalGroup = await internalCreateWithdrawalGroup(wex, { - amount, - exchangeBaseUrl: req.selectedExchange, - wgInfo: { - withdrawalType: WithdrawalRecordType.BankIntegrated, - exchangeCreditAccounts: withdrawalAccountList, - bankInfo: { - exchangePaytoUri, - talerWithdrawUri: req.talerWithdrawUri, - confirmUrl: withdrawInfo.confirmTransferUrl, - timestampBankConfirmed: undefined, - timestampReserveInfoPosted: undefined, - wireTypes: withdrawInfo.wireTypes, - }, - }, + logger.info(`confirming withdrawal with tx ${p.transactionId}`); + await confirmWithdrawal(wex, { + amount: Amounts.stringify(amount), + exchangeBaseUrl: selectedExchange, + transactionId: p.transactionId, restrictAge: req.restrictAge, forcedDenomSel: req.forcedDenomSel, - reserveStatus: WithdrawalGroupStatus.PendingRegisteringBank, - }); - - const withdrawalGroupId = withdrawalGroup.withdrawalGroupId; - - const ctx = new WithdrawTransactionContext(wex, withdrawalGroupId); - - wex.ws.notify({ - type: NotificationType.BalanceChange, - hintTransactionId: ctx.transactionId, }); - wex.taskScheduler.startShepherdTask(ctx.taskId); + const newWithdrawralGroup = await wex.db.runReadOnlyTx( + { storeNames: ["withdrawalGroups"] }, + async (tx) => { + return await tx.withdrawalGroups.indexes.byTalerWithdrawUri.get( + req.talerWithdrawUri, + ); + }, + ); - await waitWithdrawalRegistered(wex, ctx); + checkDbInvariant( + newWithdrawralGroup !== undefined, + "withdrawal don't exist after confirm", + ); return { - reservePub: withdrawalGroup.reservePub, - confirmTransferUrl: withdrawInfo.confirmTransferUrl, - transactionId: ctx.transactionId, + reservePub: newWithdrawralGroup.reservePub, + confirmTransferUrl: p.info.confirmTransferUrl, + transactionId: p.transactionId, }; } @@ -3434,7 +3479,7 @@ export async function createManualWithdrawal( ); const withdrawalGroup = await internalCreateWithdrawalGroup(wex, { - amount: Amounts.jsonifyAmount(req.amount), + amount: amount, wgInfo: { withdrawalType: WithdrawalRecordType.BankManual, exchangeCreditAccounts: withdrawalAccountsList, diff --git a/packages/taler-wallet-webextension/src/components/HistoryItem.tsx b/packages/taler-wallet-webextension/src/components/HistoryItem.tsx index 9be9326b2..8e48a2e9f 100644 --- a/packages/taler-wallet-webextension/src/components/HistoryItem.tsx +++ b/packages/taler-wallet-webextension/src/components/HistoryItem.tsx @@ -26,7 +26,7 @@ import { DenomLossEventType, parsePaytoUri, } from "@gnu-taler/taler-util"; -import { h, VNode } from "preact"; +import { Fragment, h, VNode } from "preact"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Avatar } from "../mui/Avatar.js"; import { Pages } from "../NavigationBar.js"; @@ -49,6 +49,8 @@ export function HistoryItem(props: { tx: Transaction }): VNode { */ switch (tx.type) { case TransactionType.Withdrawal: + //withdrawal that has not been confirmed are hidden + if (!tx.exchangeBaseUrl) return return ( { - if (!txId) { - throw Error("can't confirm transaction"); - } const res = await api.wallet.call(WalletApiOperation.ConfirmWithdrawal, { exchangeBaseUrl: exchange, amount, @@ -370,15 +364,16 @@ function exchangeSelectionState( onExchangeUpdated(current); } }, [current]); - - const safeAmount = !infoAmount ? Amounts.zeroOfCurrency(currency) : infoAmount - const [choosenAmount, setChoosenAmount] = useState(safeAmount) + + const safeAmount = !infoAmount + ? Amounts.zeroOfCurrency(currency) + : infoAmount; + const [choosenAmount, setChoosenAmount] = useState(safeAmount); if (selectedExchange.status !== "ready") { return selectedExchange; } - return useCallback((): | State.Success | State.LoadingUriError @@ -520,8 +515,8 @@ function exchangeSelectionState( amount: { value: choosenAmount, onInput: pushAlertOnError(async (v) => { - setChoosenAmount(v) - }) + setChoosenAmount(v); + }), }, talerWithdrawUri, ageRestriction, diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts index 785fab996..5bbf5f6c8 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts +++ b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts @@ -26,6 +26,7 @@ import { ExchangeListItem, ExchangeTosStatus, ScopeType, + TransactionIdStr, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { expect } from "chai"; @@ -111,7 +112,7 @@ describe("Withdraw CTA states", () => { WalletApiOperation.PrepareBankIntegratedWithdrawal, undefined, { - transactionId: "123", + transactionId: "123" as TransactionIdStr, info: { status: "pending", operationId: "123", @@ -153,7 +154,7 @@ describe("Withdraw CTA states", () => { WalletApiOperation.PrepareBankIntegratedWithdrawal, undefined, { - transactionId: "123", + transactionId: "123" as TransactionIdStr, info: { status: "pending", operationId: "123", -- cgit v1.2.3