taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 6d290c2feed8543a83d2679ed1cba53bb636b29b
parent 26628693c2899098539dd7c7c7ff52b90b656a00
Author: Sebastian <sebasjm@gmail.com>
Date:   Thu, 16 May 2024 15:45:07 -0300

work in progress for #8856

Diffstat:
Mpackages/taler-harness/src/integrationtests/test-withdrawal-handover.ts | 17++++++++++++-----
Mpackages/taler-util/src/wallet-types.ts | 16+++++++++-------
Mpackages/taler-wallet-core/src/balance.ts | 64++++++++++++++++++++++++++++++++++++++++++----------------------
Mpackages/taler-wallet-core/src/db.ts | 16+++++++++-------
Mpackages/taler-wallet-core/src/transactions.ts | 114++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mpackages/taler-wallet-core/src/wallet.ts | 37+++++++++----------------------------
Mpackages/taler-wallet-core/src/withdraw.ts | 323+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
7 files changed, 375 insertions(+), 212 deletions(-)

diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-handover.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-handover.ts @@ -47,9 +47,10 @@ export async function runWithdrawalHandoverTest(t: GlobalTestState) { const user = await bankClient.createRandomBankUser(); const userBankClient = new TalerCorebankApiClient(bankClient.baseUrl); userBankClient.setAuth(user); + const amount = "TESTKUDOS:10" const wop = await userBankClient.createWithdrawalOperation( user.username, - "TESTKUDOS:10", + amount, ); const checkResp = await walletClient.call( @@ -64,7 +65,7 @@ export async function runWithdrawalHandoverTest(t: GlobalTestState) { const prepareResp = await walletClient.call( WalletApiOperation.PrepareBankIntegratedWithdrawal, { - exchangeBaseUrl: checkResp.defaultExchangeBaseUrl, + // exchangeBaseUrl: checkResp.defaultExchangeBaseUrl, talerWithdrawUri: wop.taler_withdraw_uri, }, ); @@ -78,6 +79,8 @@ export async function runWithdrawalHandoverTest(t: GlobalTestState) { await walletClient.call(WalletApiOperation.ConfirmWithdrawal, { transactionId: prepareResp.transactionId, + amount, + exchangeBaseUrl: checkResp.defaultExchangeBaseUrl, }); await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { @@ -113,9 +116,11 @@ export async function runWithdrawalHandoverTest(t: GlobalTestState) { const user = await bankClient.createRandomBankUser(); const userBankClient = new TalerCorebankApiClient(bankClient.baseUrl); userBankClient.setAuth(user); + const amount = "TESTKUDOS:10"; + const wop = await userBankClient.createWithdrawalOperation( user.username, - "TESTKUDOS:10", + amount, ); const checkResp = await walletClient.call( @@ -130,7 +135,7 @@ export async function runWithdrawalHandoverTest(t: GlobalTestState) { const prepareRespW1 = await walletClient.call( WalletApiOperation.PrepareBankIntegratedWithdrawal, { - exchangeBaseUrl: checkResp.defaultExchangeBaseUrl, + // exchangeBaseUrl: checkResp.defaultExchangeBaseUrl, talerWithdrawUri: wop.taler_withdraw_uri, }, ); @@ -138,13 +143,15 @@ export async function runWithdrawalHandoverTest(t: GlobalTestState) { const prepareRespW2 = await w2.walletClient.call( WalletApiOperation.PrepareBankIntegratedWithdrawal, { - exchangeBaseUrl: checkResp.defaultExchangeBaseUrl, + // exchangeBaseUrl: checkResp.defaultExchangeBaseUrl, talerWithdrawUri: wop.taler_withdraw_uri, }, ); await w2.walletClient.call(WalletApiOperation.ConfirmWithdrawal, { transactionId: prepareRespW2.transactionId, + amount, + exchangeBaseUrl: checkResp.defaultExchangeBaseUrl, }); await w2.walletClient.call(WalletApiOperation.TestingWaitTransactionState, { diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts @@ -526,7 +526,7 @@ export interface WalletRunConfig { /** * Start processing tasks only when explicitly required, even after * init has been called. - * + * * Useful when the wallet is started to make single read-only request, * as otherwise wallet-core starts making network request and process * unrelated pending tasks. @@ -1845,18 +1845,12 @@ export interface GetWithdrawalDetailsForAmountRequest { export interface PrepareBankIntegratedWithdrawalRequest { talerWithdrawUri: string; - exchangeBaseUrl: string; - forcedDenomSel?: ForcedDenomSel; - restrictAge?: number; } export const codecForPrepareBankIntegratedWithdrawalRequest = (): Codec<PrepareBankIntegratedWithdrawalRequest> => buildCodecForObject<PrepareBankIntegratedWithdrawalRequest>() - .property("exchangeBaseUrl", codecForCanonBaseUrl()) .property("talerWithdrawUri", codecForString()) - .property("forcedDenomSel", codecForAny()) - .property("restrictAge", codecOptional(codecForNumber())) .build("PrepareBankIntegratedWithdrawalRequest"); export interface PrepareBankIntegratedWithdrawalResponse { @@ -1865,12 +1859,20 @@ export interface PrepareBankIntegratedWithdrawalResponse { export interface ConfirmWithdrawalRequest { transactionId: string; + exchangeBaseUrl: string; + amount: AmountString; + forcedDenomSel?: ForcedDenomSel; + restrictAge?: number; } export const codecForConfirmWithdrawalRequestRequest = (): Codec<ConfirmWithdrawalRequest> => buildCodecForObject<ConfirmWithdrawalRequest>() .property("transactionId", codecForString()) + .property("amount", codecForAmountString()) + .property("exchangeBaseUrl", codecForCanonBaseUrl()) + .property("forcedDenomSel", codecForAny()) + .property("restrictAge", codecOptional(codecForNumber())) .build("ConfirmWithdrawalRequest"); export interface AcceptBankIntegratedWithdrawalRequest { diff --git a/packages/taler-wallet-core/src/balance.ts b/packages/taler-wallet-core/src/balance.ts @@ -57,6 +57,7 @@ import { assertUnreachable, BalanceFlag, BalancesResponse, + checkDbInvariant, GetBalanceDetailRequest, j2s, Logger, @@ -350,9 +351,8 @@ export async function getBalancesInsideTransaction( await tx.withdrawalGroups.indexes.byStatus .iter(keyRangeActive) - .forEachAsync(async (wgRecord) => { - const currency = Amounts.currencyOf(wgRecord.denomsSel.totalCoinValue); - switch (wgRecord.status) { + .forEachAsync(async (wg) => { + switch (wg.status) { case WithdrawalGroupStatus.AbortedBank: case WithdrawalGroupStatus.AbortedExchange: case WithdrawalGroupStatus.FailedAbortingBank: @@ -374,34 +374,54 @@ export async function getBalancesInsideTransaction( // Pending, but no special flag. break; case WithdrawalGroupStatus.SuspendedKyc: - case WithdrawalGroupStatus.PendingKyc: - await balanceStore.setFlagIncomingKyc( - currency, - wgRecord.exchangeBaseUrl, - ); + case WithdrawalGroupStatus.PendingKyc: { + checkDbInvariant(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; + } case WithdrawalGroupStatus.PendingAml: - case WithdrawalGroupStatus.SuspendedAml: - await balanceStore.setFlagIncomingAml( - currency, - wgRecord.exchangeBaseUrl, - ); + case WithdrawalGroupStatus.SuspendedAml: { + checkDbInvariant(wg.denomsSel !== undefined, "wg in aml state should have been initialized") + checkDbInvariant(wg.exchangeBaseUrl !== undefined, "wg in aml state should have been initialized") + const currency = Amounts.currencyOf(wg.denomsSel.totalCoinValue); + await balanceStore.setFlagIncomingAml(currency, wg.exchangeBaseUrl); + break; + } + case WithdrawalGroupStatus.PendingRegisteringBank: { + if (wg.denomsSel && wg.exchangeBaseUrl) { + const currency = Amounts.currencyOf(wg.denomsSel.totalCoinValue); + await balanceStore.setFlagIncomingConfirmation( + currency, + wg.exchangeBaseUrl, + ); + } break; - case WithdrawalGroupStatus.PendingRegisteringBank: - case WithdrawalGroupStatus.PendingWaitConfirmBank: + } + case WithdrawalGroupStatus.PendingWaitConfirmBank: { + checkDbInvariant(wg.denomsSel !== undefined, "wg in confirmed state should have been initialized") + checkDbInvariant(wg.exchangeBaseUrl !== undefined, "wg in confirmed state should have been initialized") + const currency = Amounts.currencyOf(wg.denomsSel.totalCoinValue); await balanceStore.setFlagIncomingConfirmation( currency, - wgRecord.exchangeBaseUrl, + wg.exchangeBaseUrl, ); break; + } default: - assertUnreachable(wgRecord.status); + assertUnreachable(wg.status); } - await balanceStore.addPendingIncoming( - currency, - wgRecord.exchangeBaseUrl, - wgRecord.denomsSel.totalCoinValue, - ); + if (wg.denomsSel && wg.exchangeBaseUrl) { + // only inform pending incoming if amount and exchange has been selected + const currency = Amounts.currencyOf(wg.denomsSel.totalCoinValue); + await balanceStore.addPendingIncoming( + currency, + wg.exchangeBaseUrl, + wg.denomsSel.totalCoinValue, + ); + } + }); await tx.peerPushDebit.indexes.byStatus diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts @@ -376,7 +376,7 @@ export interface ReserveBankInfo { /** * Exchange payto URI that the bank will use to fund the reserve. */ - exchangePaytoUri: string; + exchangePaytoUri?: string; /** * Time when the information about this reserve was posted to the bank. @@ -1528,7 +1528,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? @@ -1562,7 +1562,7 @@ export interface WithdrawalGroupRecord { /** * Amount that was sent by the user to fund the reserve. */ - instructedAmount: AmountString; + instructedAmount?: AmountString; /** * Amount that was observed when querying the reserve that @@ -1579,7 +1579,7 @@ export interface WithdrawalGroupRecord { * (Initial amount confirmed by the user, might differ with denomSel * on reselection.) */ - rawWithdrawalAmount: AmountString; + rawWithdrawalAmount?: AmountString; /** * Amount that will be added to the balance when the withdrawal succeeds. @@ -1587,12 +1587,12 @@ export interface WithdrawalGroupRecord { * (Initial amount confirmed by the user, might differ with denomSel * on reselection.) */ - effectiveWithdrawalAmount: AmountString; + effectiveWithdrawalAmount?: AmountString; /** * Denominations selected for withdrawal. */ - denomsSel: DenomSelectionState; + denomsSel?: DenomSelectionState; /** * UID of the denomination selection. @@ -2677,7 +2677,9 @@ export const WalletStoresV1 = { describeContents<BankWithdrawUriRecord>({ keyPath: "talerWithdrawUri", }), - {}, + { + byGroup: describeIndex("byGroup", "withdrawalGroupId"), + }, ), backupProviders: describeStore( "backupProviders", diff --git a/packages/taler-wallet-core/src/transactions.ts b/packages/taler-wallet-core/src/transactions.ts @@ -252,6 +252,10 @@ export async function getTransactionById( ort, ); } + checkDbInvariant( + withdrawalGroupRecord.exchangeBaseUrl !== undefined, + "manual withdraw should have exchange url", + ); const exchangeDetails = await getExchangeWireDetailsInTx( tx, withdrawalGroupRecord.exchangeBaseUrl, @@ -589,6 +593,9 @@ function buildTransactionForPeerPullCredit( ); }); const txState = computePeerPullCreditTransactionState(pullCredit); + checkDbInvariant(wsr.instructedAmount !== undefined, "wg unitialized"); + checkDbInvariant(wsr.denomsSel !== undefined, "wg unitialized"); + checkDbInvariant(wsr.exchangeBaseUrl !== undefined, "wg unitialized"); return { type: TransactionType.PeerPullCredit, txState, @@ -654,13 +661,16 @@ function buildTransactionForPeerPushCredit( pushInc: PeerPushPaymentIncomingRecord, pushOrt: OperationRetryRecord | undefined, peerContractTerms: PeerContractTerms, - wsr: WithdrawalGroupRecord | undefined, + wg: WithdrawalGroupRecord | undefined, wsrOrt: OperationRetryRecord | undefined, ): Transaction { - if (wsr) { - if (wsr.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPushCredit) { + if (wg) { + if (wg.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPushCredit) { throw Error("invalid withdrawal group type for push payment credit"); } + checkDbInvariant(wg.instructedAmount !== undefined, "wg unitialized"); + checkDbInvariant(wg.denomsSel !== undefined, "wg unitialized"); + checkDbInvariant(wg.exchangeBaseUrl !== undefined, "wg unitialized"); const txState = computePeerPushCreditTransactionState(pushInc); return { @@ -668,15 +678,15 @@ function buildTransactionForPeerPushCredit( txState, txActions: computePeerPushCreditTransactionActions(pushInc), amountEffective: isUnsuccessfulTransaction(txState) - ? Amounts.stringify(Amounts.zeroOfAmount(wsr.instructedAmount)) - : Amounts.stringify(wsr.denomsSel.totalCoinValue), - amountRaw: Amounts.stringify(wsr.instructedAmount), - exchangeBaseUrl: wsr.exchangeBaseUrl, + ? Amounts.stringify(Amounts.zeroOfAmount(wg.instructedAmount)) + : Amounts.stringify(wg.denomsSel.totalCoinValue), + amountRaw: Amounts.stringify(wg.instructedAmount), + exchangeBaseUrl: wg.exchangeBaseUrl, info: { expiration: peerContractTerms.purse_expiration, summary: peerContractTerms.summary, }, - timestamp: timestampPreciseFromDb(wsr.timestampStart), + timestamp: timestampPreciseFromDb(wg.timestampStart), transactionId: constructTransactionIdentifier({ tag: TransactionType.PeerPushCredit, peerPushCreditId: pushInc.peerPushCreditId, @@ -712,37 +722,40 @@ function buildTransactionForPeerPushCredit( } function buildTransactionForBankIntegratedWithdraw( - wgRecord: WithdrawalGroupRecord, + wg: WithdrawalGroupRecord, ort?: OperationRetryRecord, ): TransactionWithdrawal { - if (wgRecord.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) + if (wg.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) throw Error(""); - const txState = computeWithdrawalTransactionStatus(wgRecord); + const txState = computeWithdrawalTransactionStatus(wg); + checkDbInvariant(wg.instructedAmount !== undefined, "wg unitialized"); + checkDbInvariant(wg.denomsSel !== undefined, "wg unitialized"); + checkDbInvariant(wg.exchangeBaseUrl !== undefined, "wg unitialized"); return { type: TransactionType.Withdrawal, txState, - txActions: computeWithdrawalTransactionActions(wgRecord), + txActions: computeWithdrawalTransactionActions(wg), amountEffective: isUnsuccessfulTransaction(txState) - ? Amounts.stringify(Amounts.zeroOfAmount(wgRecord.instructedAmount)) - : Amounts.stringify(wgRecord.denomsSel.totalCoinValue), - amountRaw: Amounts.stringify(wgRecord.instructedAmount), + ? Amounts.stringify(Amounts.zeroOfAmount(wg.instructedAmount)) + : Amounts.stringify(wg.denomsSel.totalCoinValue), + amountRaw: Amounts.stringify(wg.instructedAmount), withdrawalDetails: { type: WithdrawalType.TalerBankIntegrationApi, - confirmed: wgRecord.wgInfo.bankInfo.timestampBankConfirmed ? true : false, - exchangeCreditAccountDetails: wgRecord.wgInfo.exchangeCreditAccounts, - reservePub: wgRecord.reservePub, - bankConfirmationUrl: wgRecord.wgInfo.bankInfo.confirmUrl, + confirmed: wg.wgInfo.bankInfo.timestampBankConfirmed ? true : false, + exchangeCreditAccountDetails: wg.wgInfo.exchangeCreditAccounts, + reservePub: wg.reservePub, + bankConfirmationUrl: wg.wgInfo.bankInfo.confirmUrl, reserveIsReady: - wgRecord.status === WithdrawalGroupStatus.Done || - wgRecord.status === WithdrawalGroupStatus.PendingReady, + wg.status === WithdrawalGroupStatus.Done || + wg.status === WithdrawalGroupStatus.PendingReady, }, - kycUrl: wgRecord.kycUrl, - exchangeBaseUrl: wgRecord.exchangeBaseUrl, - timestamp: timestampPreciseFromDb(wgRecord.timestampStart), + kycUrl: wg.kycUrl, + exchangeBaseUrl: wg.exchangeBaseUrl, + timestamp: timestampPreciseFromDb(wg.timestampStart), transactionId: constructTransactionIdentifier({ tag: TransactionType.Withdrawal, - withdrawalGroupId: wgRecord.withdrawalGroupId, + withdrawalGroupId: wg.withdrawalGroupId, }), ...(ort?.lastError ? { error: ort.lastError } : {}), }; @@ -759,50 +772,50 @@ export function isUnsuccessfulTransaction(state: TransactionState): boolean { } function buildTransactionForManualWithdraw( - withdrawalGroup: WithdrawalGroupRecord, + wg: WithdrawalGroupRecord, exchangeDetails: ExchangeWireDetails, ort?: OperationRetryRecord, ): TransactionWithdrawal { - if (withdrawalGroup.wgInfo.withdrawalType !== WithdrawalRecordType.BankManual) + if (wg.wgInfo.withdrawalType !== WithdrawalRecordType.BankManual) throw Error(""); const plainPaytoUris = exchangeDetails.wireInfo?.accounts.map((x) => x.payto_uri) ?? []; + checkDbInvariant(wg.instructedAmount !== undefined, "wg unitialized"); + checkDbInvariant(wg.denomsSel !== undefined, "wg unitialized"); + checkDbInvariant(wg.exchangeBaseUrl !== undefined, "wg unitialized"); const exchangePaytoUris = augmentPaytoUrisForWithdrawal( plainPaytoUris, - withdrawalGroup.reservePub, - withdrawalGroup.instructedAmount, + wg.reservePub, + wg.instructedAmount, ); - const txState = computeWithdrawalTransactionStatus(withdrawalGroup); + const txState = computeWithdrawalTransactionStatus(wg); return { type: TransactionType.Withdrawal, txState, - txActions: computeWithdrawalTransactionActions(withdrawalGroup), + txActions: computeWithdrawalTransactionActions(wg), amountEffective: isUnsuccessfulTransaction(txState) - ? Amounts.stringify( - Amounts.zeroOfAmount(withdrawalGroup.instructedAmount), - ) - : Amounts.stringify(withdrawalGroup.denomsSel.totalCoinValue), - amountRaw: Amounts.stringify(withdrawalGroup.instructedAmount), + ? Amounts.stringify(Amounts.zeroOfAmount(wg.instructedAmount)) + : Amounts.stringify(wg.denomsSel.totalCoinValue), + amountRaw: Amounts.stringify(wg.instructedAmount), withdrawalDetails: { type: WithdrawalType.ManualTransfer, - reservePub: withdrawalGroup.reservePub, + reservePub: wg.reservePub, exchangePaytoUris, - exchangeCreditAccountDetails: - withdrawalGroup.wgInfo.exchangeCreditAccounts, + exchangeCreditAccountDetails: wg.wgInfo.exchangeCreditAccounts, reserveIsReady: - withdrawalGroup.status === WithdrawalGroupStatus.Done || - withdrawalGroup.status === WithdrawalGroupStatus.PendingReady, + wg.status === WithdrawalGroupStatus.Done || + wg.status === WithdrawalGroupStatus.PendingReady, }, - kycUrl: withdrawalGroup.kycUrl, - exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl, - timestamp: timestampPreciseFromDb(withdrawalGroup.timestampStart), + kycUrl: wg.kycUrl, + exchangeBaseUrl: wg.exchangeBaseUrl, + timestamp: timestampPreciseFromDb(wg.timestampStart), transactionId: constructTransactionIdentifier({ tag: TransactionType.Withdrawal, - withdrawalGroupId: withdrawalGroup.withdrawalGroupId, + withdrawalGroupId: wg.withdrawalGroupId, }), ...(ort?.lastError ? { error: ort.lastError } : {}), }; @@ -1090,6 +1103,10 @@ export async function getWithdrawalTransactionByUri( ort, ); } + checkDbInvariant( + withdrawalGroupRecord.exchangeBaseUrl !== undefined, + "manual withdraw should have exchange url", + ); const exchangeDetails = await getExchangeWireDetailsInTx( tx, withdrawalGroupRecord.exchangeBaseUrl, @@ -1337,6 +1354,13 @@ export async function getTransactions( }); await iterRecordsForWithdrawal(tx, filter, async (wsr) => { + if ( + wsr.rawWithdrawalAmount === undefined || + wsr.exchangeBaseUrl == undefined + ) { + // skip prepared withdrawals which has not been confirmed + return; + } const exchangesInTx = [wsr.exchangeBaseUrl]; if ( shouldSkipCurrency( diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts @@ -84,7 +84,6 @@ import { codecForAny, codecForApplyDevExperiment, codecForCanonicalizeBaseUrlRequest, - codecForCheckPayTemplateRequest, codecForCheckPeerPullPaymentRequest, codecForCheckPeerPushDebitRequest, codecForConfirmPayRequest, @@ -107,7 +106,6 @@ import { codecForGetExchangeTosRequest, codecForGetWithdrawalDetailsForAmountRequest, codecForGetWithdrawalDetailsForUri, - codecForHintNetworkAvailabilityRequest, codecForImportDbRequest, codecForInitRequest, codecForInitiatePeerPullPaymentRequest, @@ -230,7 +228,6 @@ import { observeTalerCrypto, } from "./observable-wrappers.js"; import { - checkPayForTemplate, confirmPay, getContractTermsDetails, preparePayForTemplate, @@ -288,7 +285,6 @@ import { getWithdrawalTransactionByUri, parseTransactionIdentifier, resumeTransaction, - retryAll, retryTransaction, suspendTransaction, } from "./transactions.js"; @@ -746,6 +742,7 @@ async function dispatchRequestInternal( innerError: getErrorDetailFromException(e), }); } + wex.ws.initWithConfig(applyRunConfigDefaults(req.config)); if (wex.ws.config.testing.skipDefaults) { @@ -758,11 +755,8 @@ async function dispatchRequestInternal( versionInfo: getVersion(wex), }; - if (req.config?.lazyTaskLoop) { - logger.trace("lazily starting task loop"); - } else { - await wex.taskScheduler.ensureRunning(); - } + // After initialization, task loop should run. + await wex.taskScheduler.ensureRunning(); wex.ws.initCalled = true; return resp; @@ -1006,16 +1000,13 @@ async function dispatchRequestInternal( } case WalletApiOperation.ConfirmWithdrawal: { const req = codecForConfirmWithdrawalRequestRequest().decode(payload); - return confirmWithdrawal(wex, req.transactionId); + return confirmWithdrawal(wex, req); } case WalletApiOperation.PrepareBankIntegratedWithdrawal: { const req = codecForPrepareBankIntegratedWithdrawalRequest().decode(payload); return prepareBankIntegratedWithdrawal(wex, { - selectedExchange: req.exchangeBaseUrl, talerWithdrawUri: req.talerWithdrawUri, - forcedDenomSel: req.forcedDenomSel, - restrictAge: req.restrictAge, }); } case WalletApiOperation.GetExchangeTos: { @@ -1054,10 +1045,6 @@ async function dispatchRequestInternal( const req = codecForPrepareWithdrawExchangeRequest().decode(payload); return handlePrepareWithdrawExchange(wex, req); } - case WalletApiOperation.CheckPayForTemplate: { - const req = codecForCheckPayTemplateRequest().decode(payload); - return await checkPayForTemplate(wex, req); - } case WalletApiOperation.PreparePayForUri: { const req = codecForPreparePayRequest().decode(payload); return await preparePayForUri(wex, req.talerPayUri); @@ -1242,16 +1229,10 @@ async function dispatchRequestInternal( await loadBackupRecovery(wex, req); return {}; } - case WalletApiOperation.HintNetworkAvailability: { - const req = codecForHintNetworkAvailabilityRequest().decode(payload); - if (req.isNetworkAvailable) { - await retryAll(wex); - } else { - // We're not doing anything right now, but we could stop showing - // certain errors! - } - return {}; - } + // case WalletApiOperation.GetPlanForOperation: { + // const req = codecForGetPlanForOperationRequest().decode(payload); + // return await getPlanForOperation(ws, req); + // } case WalletApiOperation.ConvertDepositAmount: { const req = codecForConvertAmountRequest.decode(payload); return await convertDepositAmount(wex, req); @@ -1846,7 +1827,7 @@ class WalletDbTriggerSpec implements TriggerSpec { if (info.mode !== "readwrite") { return; } - logger.trace( + logger.info( `in after commit callback for readwrite, modified ${j2s([ ...info.modifiedStores, ])}`, diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts @@ -36,6 +36,7 @@ import { BankWithdrawDetails, CancellationToken, CoinStatus, + ConfirmWithdrawalRequest, CurrencySpecification, DenomKeyType, DenomSelItem, @@ -194,6 +195,15 @@ async function updateWithdrawalTransaction( let transactionItem: Transaction; + if ( + !wgRecord.instructedAmount || + !wgRecord.denomsSel || + !wgRecord.exchangeBaseUrl + ) { + // withdrawal group is in preparation, nothing to update + return; + } + if (wgRecord.wgInfo.withdrawalType === WithdrawalRecordType.BankIntegrated) { const txState = computeWithdrawalTransactionStatus(wgRecord); transactionItem = { @@ -224,6 +234,18 @@ async function updateWithdrawalTransaction( } else if ( wgRecord.wgInfo.withdrawalType === WithdrawalRecordType.BankManual ) { + checkDbInvariant( + wgRecord.exchangeBaseUrl !== undefined, + "manual withdrawal without exchange can't be created", + ); + checkDbInvariant( + wgRecord.instructedAmount !== undefined, + "manual withdrawal without amount can't be created", + ); + checkDbInvariant( + wgRecord.denomsSel !== undefined, + "manual withdrawal without denoms can't be created", + ); const exchangeDetails = await getExchangeWireDetailsInTx( tx, wgRecord.exchangeBaseUrl, @@ -895,6 +917,15 @@ async function processPlanchetGenerate( withdrawalGroup: WithdrawalGroupRecord, coinIdx: number, ): Promise<void> { + checkDbInvariant( + withdrawalGroup.exchangeBaseUrl !== undefined, + "can't process unitialized exchange", + ); + checkDbInvariant( + withdrawalGroup.denomsSel !== undefined, + "can't process unitialized exchange", + ); + const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl; let planchet = await wex.db.runReadOnlyTx( { storeNames: ["planchets"] }, async (tx) => { @@ -932,12 +963,7 @@ async function processPlanchetGenerate( const denom = await wex.db.runReadOnlyTx( { storeNames: ["denominations"] }, async (tx) => { - return getDenomInfo( - wex, - tx, - withdrawalGroup.exchangeBaseUrl, - denomPubHash, - ); + return getDenomInfo(wex, tx, exchangeBaseUrl, denomPubHash); }, ); checkDbInvariant(!!denom); @@ -1058,7 +1084,7 @@ async function handleKycRequired( return TransitionResult.stay(); } for (let i = startIdx; i < requestCoinIdxs.length; i++) { - let planchet = await tx.planchets.indexes.byGroupAndIndex.get([ + const planchet = await tx.planchets.indexes.byGroupAndIndex.get([ withdrawalGroup.withdrawalGroupId, requestCoinIdxs[i], ]); @@ -1103,6 +1129,11 @@ async function processPlanchetExchangeBatchRequest( logger.info( `processing planchet exchange batch request ${withdrawalGroup.withdrawalGroupId}, start=${args.coinStartIndex}, len=${args.batchSize}`, ); + checkDbInvariant( + withdrawalGroup.exchangeBaseUrl !== undefined, + "can't process unitialized exchange", + ); + const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl; const batchReq: ExchangeBatchWithdrawRequest = { planchets: [] }; // Indices of coins that are included in the batch request @@ -1117,7 +1148,7 @@ async function processPlanchetExchangeBatchRequest( coinIdx < wgContext.numPlanchets; coinIdx++ ) { - let planchet = await tx.planchets.indexes.byGroupAndIndex.get([ + const planchet = await tx.planchets.indexes.byGroupAndIndex.get([ withdrawalGroup.withdrawalGroupId, coinIdx, ]); @@ -1134,7 +1165,7 @@ async function processPlanchetExchangeBatchRequest( const denom = await getDenomInfo( wex, tx, - withdrawalGroup.exchangeBaseUrl, + exchangeBaseUrl, planchet.denomPubHash, ); @@ -1168,7 +1199,7 @@ async function processPlanchetExchangeBatchRequest( ): Promise<void> { logger.trace(`withdrawal request failed: ${j2s(errDetail)}`); await wex.db.runReadWriteTx({ storeNames: ["planchets"] }, async (tx) => { - let planchet = await tx.planchets.indexes.byGroupAndIndex.get([ + const planchet = await tx.planchets.indexes.byGroupAndIndex.get([ withdrawalGroup.withdrawalGroupId, coinIdx, ]); @@ -1237,11 +1268,17 @@ async function processPlanchetVerifyAndStoreCoin( resp: ExchangeWithdrawResponse, ): Promise<void> { const withdrawalGroup = wgContext.wgRecord; + checkDbInvariant( + withdrawalGroup.exchangeBaseUrl !== undefined, + "can't process unitialized exchange", + ); + const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl; + logger.trace(`checking and storing planchet idx=${coinIdx}`); const d = await wex.db.runReadOnlyTx( { storeNames: ["planchets", "denominations"] }, async (tx) => { - let planchet = await tx.planchets.indexes.byGroupAndIndex.get([ + const planchet = await tx.planchets.indexes.byGroupAndIndex.get([ withdrawalGroup.withdrawalGroupId, coinIdx, ]); @@ -1255,7 +1292,7 @@ async function processPlanchetVerifyAndStoreCoin( const denomInfo = await getDenomInfo( wex, tx, - withdrawalGroup.exchangeBaseUrl, + exchangeBaseUrl, planchet.denomPubHash, ); if (!denomInfo) { @@ -1264,7 +1301,7 @@ async function processPlanchetVerifyAndStoreCoin( return { planchet, denomInfo, - exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl, + exchangeBaseUrl: exchangeBaseUrl, }; }, ); @@ -1285,7 +1322,7 @@ async function processPlanchetVerifyAndStoreCoin( throw Error(`cipher (${planchetDenomPub.cipher}) not supported`); } - let evSig = resp.ev_sig; + const evSig = resp.ev_sig; if (!(evSig.cipher === DenomKeyType.Rsa)) { throw Error("unsupported cipher"); } @@ -1304,7 +1341,7 @@ async function processPlanchetVerifyAndStoreCoin( if (!isValid) { await wex.db.runReadWriteTx({ storeNames: ["planchets"] }, async (tx) => { - let planchet = await tx.planchets.indexes.byGroupAndIndex.get([ + const planchet = await tx.planchets.indexes.byGroupAndIndex.get([ withdrawalGroup.withdrawalGroupId, coinIdx, ]); @@ -1483,6 +1520,19 @@ async function processQueryReserve( if (withdrawalGroup.status !== WithdrawalGroupStatus.PendingQueryingStatus) { return TaskRunResult.backoff(); } + checkDbInvariant( + withdrawalGroup.exchangeBaseUrl !== undefined, + "can't process unitialized exchange", + ); + checkDbInvariant( + withdrawalGroup.denomsSel !== undefined, + "can't process unitialized exchange", + ); + checkDbInvariant( + withdrawalGroup.instructedAmount !== undefined, + "can't process unitialized exchange", + ); + const reservePub = withdrawalGroup.reservePub; const reserveUrl = new URL( @@ -1705,6 +1755,14 @@ async function redenominateWithdrawal( if (!wg) { return; } + checkDbInvariant( + wg.exchangeBaseUrl !== undefined, + "can't process unitialized exchange", + ); + checkDbInvariant( + wg.denomsSel !== undefined, + "can't process unitialized exchange", + ); const currency = Amounts.currencyOf(wg.denomsSel.totalWithdrawCost); const exchangeBaseUrl = wg.exchangeBaseUrl; @@ -1721,13 +1779,13 @@ async function redenominateWithdrawal( logger.trace(`old denom sel: ${j2s(oldSel)}`); } - let zero = Amount.zeroOfCurrency(currency); + const zero = Amount.zeroOfCurrency(currency); let amountRemaining = zero; let prevTotalCoinValue = zero; let prevTotalWithdrawalCost = zero; let prevHasDenomWithAgeRestriction = false; let prevEarliestDepositExpiration = AbsoluteTime.never(); - let prevDenoms: DenomSelItem[] = []; + const prevDenoms: DenomSelItem[] = []; let coinIndex = 0; for (let i = 0; i < oldSel.selectedDenoms.length; i++) { const sel = wg.denomsSel.selectedDenoms[i]; @@ -1739,7 +1797,7 @@ async function redenominateWithdrawal( throw Error("denom in use but not not found"); } // FIXME: Also check planchet if there was a different error or planchet already withdrawn - let denomOkay = isWithdrawableDenom( + const denomOkay = isWithdrawableDenom( denom, wex.ws.config.testing.denomselAllowLate, ); @@ -1840,8 +1898,15 @@ async function processWithdrawalGroupPendingReady( const { withdrawalGroupId } = withdrawalGroup; const ctx = new WithdrawTransactionContext(wex, withdrawalGroupId); + checkDbInvariant( + withdrawalGroup.exchangeBaseUrl !== undefined, + "can't process unitialized exchange", + ); + checkDbInvariant( + withdrawalGroup.denomsSel !== undefined, + "can't process unitialized exchange", + ); const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl; - await fetchFreshExchange(wex, withdrawalGroup.exchangeBaseUrl); if (withdrawalGroup.denomsSel.selectedDenoms.length === 0) { @@ -1943,7 +2008,6 @@ async function processWithdrawalGroupPendingReady( const errorsPerCoin: Record<number, TalerErrorDetail> = {}; let numPlanchetErrors = 0; let numActive = 0; - let numDone = 0; const maxReportedErrors = 5; const res = await ctx.transition( @@ -1964,7 +2028,6 @@ async function processWithdrawalGroupPendingReady( numActive++; break; case PlanchetStatus.WithdrawalDone: - numDone++; break; } if (x.lastError) { @@ -2269,6 +2332,14 @@ export async function getFundingPaytoUris( ): Promise<string[]> { const withdrawalGroup = await tx.withdrawalGroups.get(withdrawalGroupId); checkDbInvariant(!!withdrawalGroup); + checkDbInvariant( + withdrawalGroup.exchangeBaseUrl !== undefined, + "can't get funding uri from uninitialized wg", + ); + checkDbInvariant( + withdrawalGroup.instructedAmount !== undefined, + "can't get funding uri from uninitialized wg", + ); const exchangeDetails = await getExchangeWireDetailsInTx( tx, withdrawalGroup.exchangeBaseUrl, @@ -2579,12 +2650,41 @@ export interface PrepareCreateWithdrawalGroupResult { }; } +async function getInitialDenomsSelection( + wex: WalletExecutionContext, + exchange: string, + amount: AmountJson, + forcedDenoms: ForcedDenomSel | undefined, +): Promise<DenomSelectionState> { + const currency = Amounts.currencyOf(amount); + await updateWithdrawalDenoms(wex, exchange); + const denoms = await getCandidateWithdrawalDenoms(wex, exchange, currency); + + if (forcedDenoms) { + logger.warn("using forced denom selection"); + const initialDenomSel = selectForcedWithdrawalDenominations( + amount, + denoms, + forcedDenoms, + wex.ws.config.testing.denomselAllowLate, + ); + return initialDenomSel; + } else { + const initialDenomSel = selectWithdrawalDenominations( + amount, + denoms, + wex.ws.config.testing.denomselAllowLate, + ); + return initialDenomSel; + } +} + export async function internalPrepareCreateWithdrawalGroup( wex: WalletExecutionContext, args: { reserveStatus: WithdrawalGroupStatus; - amount: AmountJson; - exchangeBaseUrl: string; + amount?: AmountJson; + exchangeBaseUrl?: string; forcedWithdrawalGroupId?: string; forcedDenomSel?: ForcedDenomSel; reserveKeyPair?: EddsaKeypair; @@ -2598,9 +2698,8 @@ export async function internalPrepareCreateWithdrawalGroup( const secretSeed = encodeCrock(getRandomBytes(32)); const exchangeBaseUrl = args.exchangeBaseUrl; const amount = args.amount; - const currency = Amounts.currencyOf(amount); - let withdrawalGroupId; + let withdrawalGroupId: string; if (args.forcedWithdrawalGroupId) { withdrawalGroupId = args.forcedWithdrawalGroupId; @@ -2623,39 +2722,37 @@ export async function internalPrepareCreateWithdrawalGroup( withdrawalGroupId = encodeCrock(getRandomBytes(32)); } - await updateWithdrawalDenoms(wex, exchangeBaseUrl); - const denoms = await getCandidateWithdrawalDenoms( - wex, - exchangeBaseUrl, - currency, - ); - - let initialDenomSel: DenomSelectionState; + let initialDenomSel: DenomSelectionState | undefined; const denomSelUid = encodeCrock(getRandomBytes(16)); - if (args.forcedDenomSel) { - logger.warn("using forced denom selection"); - initialDenomSel = selectForcedWithdrawalDenominations( - amount, - denoms, + + const creationInfo = + exchangeBaseUrl !== undefined && amount !== undefined + ? { + canonExchange: exchangeBaseUrl, + amount, + } + : undefined; + + if (creationInfo) { + initialDenomSel = await getInitialDenomsSelection( + wex, + creationInfo.canonExchange, + creationInfo.amount, args.forcedDenomSel, - wex.ws.config.testing.denomselAllowLate, - ); - } else { - initialDenomSel = selectWithdrawalDenominations( - amount, - denoms, - wex.ws.config.testing.denomselAllowLate, ); } const withdrawalGroup: WithdrawalGroupRecord = { denomSelUid, + // next fields will be undefined if exchange or amount is not specified denomsSel: initialDenomSel, exchangeBaseUrl: exchangeBaseUrl, - instructedAmount: Amounts.stringify(amount), + instructedAmount: + amount === undefined ? undefined : Amounts.stringify(amount), + rawWithdrawalAmount: initialDenomSel?.totalWithdrawCost, + effectiveWithdrawalAmount: initialDenomSel?.totalCoinValue, + // end of optional fields timestampStart: timestampPreciseToDb(now), - rawWithdrawalAmount: initialDenomSel.totalWithdrawCost, - effectiveWithdrawalAmount: initialDenomSel.totalCoinValue, secretSeed, reservePriv: reserveKeyPair.priv, reservePub: reserveKeyPair.pub, @@ -2667,7 +2764,10 @@ export async function internalPrepareCreateWithdrawalGroup( wgInfo: args.wgInfo, }; - await fetchFreshExchange(wex, exchangeBaseUrl); + if (creationInfo) { + await fetchFreshExchange(wex, creationInfo.canonExchange); + } + const transactionId = constructTransactionIdentifier({ tag: TransactionType.Withdrawal, withdrawalGroupId: withdrawalGroup.withdrawalGroupId, @@ -2676,10 +2776,7 @@ export async function internalPrepareCreateWithdrawalGroup( return { withdrawalGroup, transactionId, - creationInfo: { - canonExchange: exchangeBaseUrl, - amount, - }, + creationInfo, }; } @@ -2703,13 +2800,6 @@ export async function internalPerformCreateWithdrawalGroup( prep: PrepareCreateWithdrawalGroupResult, ): Promise<PerformCreateWithdrawalGroupResult> { const { withdrawalGroup } = prep; - if (!prep.creationInfo) { - return { - withdrawalGroup, - transitionInfo: undefined, - exchangeNotif: undefined, - }; - } const existingWg = await tx.withdrawalGroups.get( withdrawalGroup.withdrawalGroupId, ); @@ -2726,7 +2816,14 @@ export async function internalPerformCreateWithdrawalGroup( reservePriv: withdrawalGroup.reservePriv, }); - const exchange = await tx.exchanges.get(withdrawalGroup.exchangeBaseUrl); + if (!prep.creationInfo) { + return { + withdrawalGroup, + transitionInfo: undefined, + exchangeNotif: undefined, + }; + } + const exchange = await tx.exchanges.get(prep.creationInfo.canonExchange); if (exchange) { exchange.lastWithdrawal = timestampPreciseToDb(TalerPreciseTimestamp.now()); await tx.exchanges.put(exchange); @@ -2745,7 +2842,7 @@ export async function internalPerformCreateWithdrawalGroup( const exchangeUsedRes = await markExchangeUsed( wex, tx, - prep.withdrawalGroup.exchangeBaseUrl, + prep.creationInfo.canonExchange, ); const ctx = new WithdrawTransactionContext( @@ -2774,8 +2871,8 @@ export async function internalCreateWithdrawalGroup( wex: WalletExecutionContext, args: { reserveStatus: WithdrawalGroupStatus; - amount: AmountJson; - exchangeBaseUrl: string; + amount?: AmountJson; + exchangeBaseUrl?: string; forcedWithdrawalGroupId?: string; forcedDenomSel?: ForcedDenomSel; reserveKeyPair?: EddsaKeypair; @@ -2820,9 +2917,6 @@ export async function prepareBankIntegratedWithdrawal( wex: WalletExecutionContext, req: { talerWithdrawUri: string; - selectedExchange: string; - forcedDenomSel?: ForcedDenomSel; - restrictAge?: number; }, ): Promise<PrepareBankIntegratedWithdrawalResponse> { const existingWithdrawalGroup = await wex.db.runReadOnlyTx( @@ -2850,44 +2944,23 @@ export async function prepareBankIntegratedWithdrawal( }; } - const selectedExchange = req.selectedExchange; - const exchange = await fetchFreshExchange(wex, selectedExchange); - - const withdrawInfo = await getBankWithdrawalInfo( - wex.http, - req.talerWithdrawUri, - ); - const exchangePaytoUri = await getExchangePaytoUri( - wex, - selectedExchange, - withdrawInfo.wireTypes, - ); - - const withdrawalAccountList = await fetchWithdrawalAccountInfo( - wex, - { - exchange, - instructedAmount: withdrawInfo.amount, - }, - wex.cancellationToken, - ); - + /** + * Withdrawal group without exchange and amount + * this is an special case when the user haven't yet + * choose. We are still tracking this object since the state + * can change from the bank side or another wallet with the + * same URI + */ const withdrawalGroup = await internalCreateWithdrawalGroup(wex, { - amount: withdrawInfo.amount, - exchangeBaseUrl: req.selectedExchange, wgInfo: { withdrawalType: WithdrawalRecordType.BankIntegrated, - exchangeCreditAccounts: withdrawalAccountList, bankInfo: { - exchangePaytoUri, talerWithdrawUri: req.talerWithdrawUri, - confirmUrl: withdrawInfo.confirmTransferUrl, + confirmUrl: undefined, timestampBankConfirmed: undefined, timestampReserveInfoPosted: undefined, }, }, - restrictAge: req.restrictAge, - forcedDenomSel: req.forcedDenomSel, reserveStatus: WithdrawalGroupStatus.DialogProposed, }); @@ -2904,9 +2977,9 @@ export async function prepareBankIntegratedWithdrawal( export async function confirmWithdrawal( wex: WalletExecutionContext, - transactionId: string, + req: ConfirmWithdrawalRequest, ): Promise<void> { - const parsedTx = parseTransactionIdentifier(transactionId); + const parsedTx = parseTransactionIdentifier(req.transactionId); if (parsedTx?.tag !== TransactionType.Withdrawal) { throw Error("invalid withdrawal transaction ID"); } @@ -2921,16 +2994,70 @@ export async function confirmWithdrawal( throw Error("withdrawal group not found"); } + if ( + withdrawalGroup.wgInfo.withdrawalType !== + WithdrawalRecordType.BankIntegrated + ) { + throw Error("not a bank integrated withdrawal"); + } + + const selectedExchange = req.exchangeBaseUrl; + const exchange = await fetchFreshExchange(wex, selectedExchange); + + const talerWithdrawUri = withdrawalGroup.wgInfo.bankInfo.talerWithdrawUri; + + const withdrawInfo = await getBankWithdrawalInfo(wex.http, talerWithdrawUri); + const exchangePaytoUri = await getExchangePaytoUri( + wex, + selectedExchange, + withdrawInfo.wireTypes, + ); + + const withdrawalAccountList = await fetchWithdrawalAccountInfo( + wex, + { + exchange, + instructedAmount: withdrawInfo.amount, + }, + wex.cancellationToken, + ); + const ctx = new WithdrawTransactionContext( wex, withdrawalGroup.withdrawalGroupId, ); + const initalDenoms = await getInitialDenomsSelection( + wex, + req.exchangeBaseUrl, + Amounts.parseOrThrow(req.amount), + req.forcedDenomSel, + ); + ctx.transition({}, async (rec) => { if (!rec) { return TransitionResult.stay(); } switch (rec.status) { case WithdrawalGroupStatus.DialogProposed: { + rec.exchangeBaseUrl = req.exchangeBaseUrl; + rec.instructedAmount = req.amount; + rec.denomsSel = initalDenoms; + rec.rawWithdrawalAmount = initalDenoms.totalWithdrawCost; + rec.effectiveWithdrawalAmount = initalDenoms.totalCoinValue; + rec.restrictAge = req.restrictAge; + + rec.wgInfo = { + withdrawalType: WithdrawalRecordType.BankIntegrated, + exchangeCreditAccounts: withdrawalAccountList, + bankInfo: { + exchangePaytoUri, + talerWithdrawUri, + confirmUrl: withdrawInfo.confirmTransferUrl, + timestampBankConfirmed: undefined, + timestampReserveInfoPosted: undefined, + }, + }; + rec.status = WithdrawalGroupStatus.PendingRegisteringBank; return TransitionResult.transition(rec); }