diff options
-rw-r--r-- | packages/taler-util/src/taler-types.ts | 43 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/withdraw.ts | 190 |
2 files changed, 167 insertions, 66 deletions
diff --git a/packages/taler-util/src/taler-types.ts b/packages/taler-util/src/taler-types.ts index 1bdcf3fb1..4a0c53a79 100644 --- a/packages/taler-util/src/taler-types.ts +++ b/packages/taler-util/src/taler-types.ts @@ -27,9 +27,9 @@ import { Amounts, codecForAmountString } from "./amounts.js"; import { + Codec, buildCodecForObject, buildCodecForUnion, - Codec, codecForAny, codecForBoolean, codecForConstNumber, @@ -45,14 +45,14 @@ import { strcmp } from "./helpers.js"; import { CurrencySpecification, codecForCurrencySpecificiation, + codecForEither, } from "./index.js"; -import { AgeCommitmentProof, Edx25519PublicKeyEnc } from "./taler-crypto.js"; +import { Edx25519PublicKeyEnc } from "./taler-crypto.js"; import { - codecForAbsoluteTime, - codecForDuration, - codecForTimestamp, TalerProtocolDuration, TalerProtocolTimestamp, + codecForDuration, + codecForTimestamp, } from "./time.js"; /** @@ -1228,9 +1228,42 @@ export interface MerchantOrderStatusUnpaid { * POST {talerBankIntegrationApi}/withdrawal-operation/{wopid} */ export interface BankWithdrawalOperationPostResponse { + // Current status of the operation + // pending: the operation is pending parameters selection (exchange and reserve public key) + // selected: the operations has been selected and is pending confirmation + // aborted: the operation has been aborted + // confirmed: the transfer has been confirmed and registered by the bank + status: "selected" | "aborted" | "confirmed" | "pending"; + + // URL that the user needs to navigate to in order to + // complete some final confirmation (e.g. 2FA). + // + // Only applicable when status is selected or pending. + // It may contain withdrawal operation id + confirm_transfer_url?: string; + + // Deprecated field use status instead + // The transfer has been confirmed and registered by the bank. + // Does not guarantee that the funds have arrived at the exchange already. transfer_done: boolean; } +export const codeForBankWithdrawalOperationPostResponse = + (): Codec<BankWithdrawalOperationPostResponse> => + buildCodecForObject<BankWithdrawalOperationPostResponse>() + .property( + "status", + codecForEither( + codecForConstString("selected"), + codecForConstString("confirmed"), + codecForConstString("aborted"), + codecForConstString("pending"), + ), + ) + .property("confirm_transfer_url", codecOptional(codecForString())) + .property("transfer_done", codecForBoolean()) + .build("BankWithdrawalOperationPostResponse"); + export type DenominationPubKey = RsaDenominationPubKey | CsDenominationPubKey; export interface RsaDenominationPubKey { diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts index 45792af41..9cf1ad36d 100644 --- a/packages/taler-wallet-core/src/withdraw.ts +++ b/packages/taler-wallet-core/src/withdraw.ts @@ -70,7 +70,7 @@ import { WithdrawalExchangeAccountDetails, addPaytoQueryParams, canonicalizeBaseUrl, - codecForAny, + codeForBankWithdrawalOperationPostResponse, codecForCashinConversionResponse, codecForConversionBankConfig, codecForExchangeWithdrawBatchResponse, @@ -1257,7 +1257,7 @@ async function updateWithdrawalDenoms( * and are big enough to withdraw with available denominations, * create a new withdrawal group for the remaining amount. */ -async function queryReserve( +async function processQueryReserve( ws: InternalWalletState, withdrawalGroupId: string, cancellationToken: CancellationToken, @@ -1330,7 +1330,11 @@ async function queryReserve( notifyTransition(ws, transactionId, transitionResult); - return TaskRunResult.backoff(); + if (transitionResult) { + return TaskRunResult.progress(); + } else { + return TaskRunResult.backoff(); + } } /** @@ -1664,9 +1668,13 @@ export async function processWithdrawalGroup( switch (withdrawalGroup.status) { case WithdrawalGroupStatus.PendingRegisteringBank: - return await processReserveBankStatus(ws, withdrawalGroupId); + return await processBankRegisterReserve( + ws, + withdrawalGroupId, + cancellationToken, + ); case WithdrawalGroupStatus.PendingQueryingStatus: - return queryReserve(ws, withdrawalGroupId, cancellationToken); + return processQueryReserve(ws, withdrawalGroupId, cancellationToken); case WithdrawalGroupStatus.PendingWaitConfirmBank: return await processReserveBankStatus(ws, withdrawalGroupId); case WithdrawalGroupStatus.PendingAml: @@ -2064,7 +2072,7 @@ async function registerReserveWithBank( if ( withdrawalGroup.wgInfo.withdrawalType != WithdrawalRecordType.BankIntegrated ) { - throw Error("expecting withdrarwal type = bank integrated"); + throw Error("expecting withdrawal type = bank integrated"); } const bankInfo = withdrawalGroup.wgInfo.bankInfo; if (!bankInfo) { @@ -2081,8 +2089,10 @@ async function registerReserveWithBank( body: reqBody, timeout: getReserveRequestTimeout(withdrawalGroup), }); - // FIXME: libeufin-bank currently doesn't return a response in the right format, so we don't validate at all. - await readSuccessResponseJsonOrThrow(httpResp, codecForAny()); + const status = await readSuccessResponseJsonOrThrow( + httpResp, + codeForBankWithdrawalOperationPostResponse(), + ); const transitionInfo = await ws.db.runReadWriteTx( ["withdrawalGroups"], async (tx) => { @@ -2105,6 +2115,7 @@ async function registerReserveWithBank( ); const oldTxState = computeWithdrawalTransactionStatus(r); r.status = WithdrawalGroupStatus.PendingWaitConfirmBank; + r.wgInfo.bankInfo.confirmUrl = status.confirm_transfer_url; const newTxState = computeWithdrawalTransactionStatus(r); await tx.withdrawalGroups.put(r); return { @@ -2117,25 +2128,109 @@ async function registerReserveWithBank( notifyTransition(ws, transactionId, transitionInfo); } -async function processReserveBankStatus( +async function transitionBankAborted( + ctx: WithdrawTransactionContext, +): Promise<TaskRunResult> { + logger.info("bank aborted the withdrawal"); + const transitionInfo = await ctx.ws.db.runReadWriteTx( + ["withdrawalGroups"], + async (tx) => { + const r = await tx.withdrawalGroups.get(ctx.withdrawalGroupId); + if (!r) { + return; + } + switch (r.status) { + case WithdrawalGroupStatus.PendingRegisteringBank: + case WithdrawalGroupStatus.PendingWaitConfirmBank: + break; + default: + return; + } + if (r.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) { + throw Error("invariant failed"); + } + const now = AbsoluteTime.toPreciseTimestamp(AbsoluteTime.now()); + const oldTxState = computeWithdrawalTransactionStatus(r); + r.wgInfo.bankInfo.timestampBankConfirmed = timestampPreciseToDb(now); + r.status = WithdrawalGroupStatus.FailedBankAborted; + const newTxState = computeWithdrawalTransactionStatus(r); + await tx.withdrawalGroups.put(r); + return { + oldTxState, + newTxState, + }; + }, + ); + notifyTransition(ctx.ws, ctx.transactionId, transitionInfo); + return TaskRunResult.finished(); +} + +async function processBankRegisterReserve( ws: InternalWalletState, withdrawalGroupId: string, + cancellationToken: CancellationToken, ): Promise<TaskRunResult> { + const ctx = new WithdrawTransactionContext(ws, withdrawalGroupId); const withdrawalGroup = await getWithdrawalGroupRecordTx(ws.db, { withdrawalGroupId, }); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Withdrawal, + if (!withdrawalGroup) { + return TaskRunResult.finished(); + } + + if ( + withdrawalGroup.wgInfo.withdrawalType != WithdrawalRecordType.BankIntegrated + ) { + throw Error("wrong withdrawal record type"); + } + const bankInfo = withdrawalGroup.wgInfo.bankInfo; + if (!bankInfo) { + throw Error("no bank info in bank-integrated withdrawal"); + } + + const uriResult = parseWithdrawUri(bankInfo.talerWithdrawUri); + if (!uriResult) { + throw Error(`can't parse withdrawal URL ${bankInfo.talerWithdrawUri}`); + } + const url = new URL( + `withdrawal-operation/${uriResult.withdrawalOperationId}`, + uriResult.bankIntegrationApiBaseUrl, + ); + + const statusResp = await ws.http.fetch(url.href, { + timeout: getReserveRequestTimeout(withdrawalGroup), + cancellationToken, + }); + + const status = await readSuccessResponseJsonOrThrow( + statusResp, + codecForWithdrawOperationStatusResponse(), + ); + + if (status.aborted) { + return transitionBankAborted(ctx); + } + + // FIXME: Put confirm transfer URL in the DB! + + await registerReserveWithBank(ws, withdrawalGroupId); + return TaskRunResult.progress(); +} + +async function processReserveBankStatus( + ws: InternalWalletState, + withdrawalGroupId: string, +): Promise<TaskRunResult> { + const withdrawalGroup = await getWithdrawalGroupRecordTx(ws.db, { withdrawalGroupId, }); - switch (withdrawalGroup?.status) { - case WithdrawalGroupStatus.PendingWaitConfirmBank: - case WithdrawalGroupStatus.PendingRegisteringBank: - break; - default: - return TaskRunResult.backoff(); + + if (!withdrawalGroup) { + return TaskRunResult.finished(); } + const ctx = new WithdrawTransactionContext(ws, withdrawalGroupId); + if ( withdrawalGroup.wgInfo.withdrawalType != WithdrawalRecordType.BankIntegrated ) { @@ -2143,58 +2238,35 @@ async function processReserveBankStatus( } const bankInfo = withdrawalGroup.wgInfo.bankInfo; if (!bankInfo) { - return TaskRunResult.backoff(); + throw Error("no bank info in bank-integrated withdrawal"); } - const bankStatusUrl = getBankStatusUrl(bankInfo.talerWithdrawUri); + const uriResult = parseWithdrawUri(bankInfo.talerWithdrawUri); + if (!uriResult) { + throw Error(`can't parse withdrawal URL ${bankInfo.talerWithdrawUri}`); + } + const url = new URL( + `withdrawal-operation/${uriResult.withdrawalOperationId}`, + uriResult.bankIntegrationApiBaseUrl, + ); + url.searchParams.set("timeout_ms", "30000"); - const statusResp = await ws.http.fetch(bankStatusUrl, { + const statusResp = await ws.http.fetch(url.href, { timeout: getReserveRequestTimeout(withdrawalGroup), }); + const status = await readSuccessResponseJsonOrThrow( statusResp, codecForWithdrawOperationStatusResponse(), ); if (status.aborted) { - logger.info("bank aborted the withdrawal"); - const transitionInfo = await ws.db.runReadWriteTx( - ["withdrawalGroups"], - async (tx) => { - const r = await tx.withdrawalGroups.get(withdrawalGroupId); - if (!r) { - return; - } - switch (r.status) { - case WithdrawalGroupStatus.PendingRegisteringBank: - case WithdrawalGroupStatus.PendingWaitConfirmBank: - break; - default: - return; - } - if (r.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) { - throw Error("invariant failed"); - } - const now = AbsoluteTime.toPreciseTimestamp(AbsoluteTime.now()); - const oldTxState = computeWithdrawalTransactionStatus(r); - r.wgInfo.bankInfo.timestampBankConfirmed = timestampPreciseToDb(now); - r.status = WithdrawalGroupStatus.FailedBankAborted; - const newTxState = computeWithdrawalTransactionStatus(r); - await tx.withdrawalGroups.put(r); - return { - oldTxState, - newTxState, - }; - }, - ); - notifyTransition(ws, transactionId, transitionInfo); - return TaskRunResult.finished(); + return transitionBankAborted(ctx); } - // Bank still needs to know our reserve info - if (!status.selection_done) { - await registerReserveWithBank(ws, withdrawalGroupId); - return TaskRunResult.progress(); + if (!status.transfer_done) { + // FIXME: This is a long-poll result + return TaskRunResult.backoff(); } const transitionInfo = await ws.db.runReadWriteTx( @@ -2206,7 +2278,6 @@ async function processReserveBankStatus( } // Re-check reserve status within transaction switch (r.status) { - case WithdrawalGroupStatus.PendingRegisteringBank: case WithdrawalGroupStatus.PendingWaitConfirmBank: break; default: @@ -2222,9 +2293,6 @@ async function processReserveBankStatus( r.wgInfo.bankInfo.timestampBankConfirmed = timestampPreciseToDb(now); r.status = WithdrawalGroupStatus.PendingQueryingStatus; } else { - logger.trace("withdrawal: transfer not yet confirmed by bank"); - r.wgInfo.bankInfo.confirmUrl = status.confirm_transfer_url; - r.senderWire = status.sender_wire; } const newTxState = computeWithdrawalTransactionStatus(r); await tx.withdrawalGroups.put(r); @@ -2235,7 +2303,7 @@ async function processReserveBankStatus( }, ); - notifyTransition(ws, transactionId, transitionInfo); + notifyTransition(ws, ctx.transactionId, transitionInfo); if (transitionInfo) { return TaskRunResult.progress(); |