taler-typescript-core

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

commit 87d3eb80c6ddb1b24740e073705d515d12f3e9f4
parent 32693c20db860b7543b55595593f51666560da53
Author: Florian Dold <florian@dold.me>
Date:   Thu, 16 Jan 2025 15:39:19 +0100

fix test, simplify

Diffstat:
Mpackages/taler-harness/src/integrationtests/test-bank-wop.ts | 32+++++++++++++++++++++++++++-----
Mpackages/taler-harness/src/integrationtests/test-withdrawal-conflict.ts | 38++++++++++++++++++++------------------
Mpackages/taler-util/src/codec.ts | 3+++
Mpackages/taler-util/src/http-client/bank-integration.ts | 3++-
Mpackages/taler-util/src/types-taler-common.ts | 1+
Mpackages/taler-util/src/types-taler-corebank.ts | 3+++
Mpackages/taler-util/src/types-taler-wallet-transactions.ts | 2++
Mpackages/taler-util/src/types-taler-wallet.ts | 13+++++++++++--
Mpackages/taler-wallet-core/src/testing.ts | 23++++++++++++++---------
Mpackages/taler-wallet-core/src/withdraw.ts | 126+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
10 files changed, 161 insertions(+), 83 deletions(-)

diff --git a/packages/taler-harness/src/integrationtests/test-bank-wop.ts b/packages/taler-harness/src/integrationtests/test-bank-wop.ts @@ -18,6 +18,8 @@ * Imports. */ import { + createEddsaKeyPair, + encodeCrock, j2s, narrowOpSuccessOrThrow, TalerBankIntegrationHttpClient, @@ -35,7 +37,8 @@ import { GlobalTestState } from "../harness/harness.js"; export async function runBankWopTest(t: GlobalTestState) { // Set up test environment - const { bank } = await createSimpleTestkudosEnvironmentV3(t); + const { bank, exchange, exchangeBankAccount } = + await createSimpleTestkudosEnvironmentV3(t); const bankClientNg = new TalerCoreBankHttpClient(bank.corebankApiBaseUrl); @@ -48,15 +51,34 @@ export async function runBankWopTest(t: GlobalTestState) { narrowOpSuccessOrThrow("", withdrawalRes); const biClient = new TalerBankIntegrationHttpClient( - `${bank.corebankApiBaseUrl}/taler-integration/`, + `${bank.corebankApiBaseUrl}taler-integration/`, ); - const wopStatus = await biClient.getWithdrawalOperationById( - withdrawalRes.body.withdrawal_id, - ); + const wopid = withdrawalRes.body.withdrawal_id; + + const wopStatus = await biClient.getWithdrawalOperationById(wopid); narrowOpSuccessOrThrow("", wopStatus); console.log(`${j2s(wopStatus)}`); + + const keyPair = createEddsaKeyPair(); + + const postRes = await biClient.completeWithdrawalOperationById(wopid, { + selected_exchange: exchangeBankAccount.accountPaytoUri, + reserve_pub: encodeCrock(keyPair.eddsaPub), + }); + narrowOpSuccessOrThrow("", postRes); + + const confirmResp = await bankClientNg.confirmWithdrawalById( + bankUser, + {}, + wopid, + ); + narrowOpSuccessOrThrow("", confirmResp); + + const wopStatus2 = await biClient.getWithdrawalOperationById(wopid); + narrowOpSuccessOrThrow("", wopStatus2); + console.log(`status after: ${j2s(wopStatus2.body)}`); } runBankWopTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-conflict.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-conflict.ts @@ -22,6 +22,7 @@ import { TransactionIdStr, TransactionMajorState, TransactionMinorState, + TransactionStatePattern, j2s, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; @@ -131,30 +132,31 @@ export async function runWithdrawalConflictTest(t: GlobalTestState) { // One wallet will succeed, another one will have an aborted transaction. // Order is non-determinstic. + // The "aborted(bank)" state is only present because the taler-exchange-fakebank + // returns 404 when one wallet completes the withdrawal, which is a + // bug. + const expectedFinalStates = [ + { + major: TransactionMajorState.Done, + }, + { + major: TransactionMajorState.Aborted, + minor: "*", + }, + { + major: TransactionMajorState.Failed, + minor: "*", + }, + ] satisfies TransactionStatePattern[]; + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { transactionId: wMainPrepareResp.transactionId as TransactionIdStr, - txState: [ - { - major: TransactionMajorState.Done, - }, - { - major: TransactionMajorState.Aborted, - minor: TransactionMinorState.CompletedByOtherWallet, - }, - ], + txState: expectedFinalStates, }); await w2.walletClient.call(WalletApiOperation.TestingWaitTransactionState, { transactionId: w2PrepareResp.transactionId as TransactionIdStr, - txState: [ - { - major: TransactionMajorState.Done, - }, - { - major: TransactionMajorState.Aborted, - minor: TransactionMinorState.CompletedByOtherWallet, - }, - ], + txState: expectedFinalStates, }); } diff --git a/packages/taler-util/src/codec.ts b/packages/taler-util/src/codec.ts @@ -114,6 +114,9 @@ class ObjectCodecBuilder<OutputType, PartialOutputType> { return this as any; } + /** + * Do not log warnings if the object has extra properties. + */ allowExtra(): ObjectCodecBuilder<OutputType, PartialOutputType> { this._allowExtra = true; return this; diff --git a/packages/taler-util/src/http-client/bank-integration.ts b/packages/taler-util/src/http-client/bank-integration.ts @@ -127,8 +127,9 @@ export class TalerBankIntegrationHttpClient { } /** - * https://docs.taler.net/core/api-bank-integration.html#post-$BANK_API_BASE_URL-withdrawal-operation-$wopid + * FIXME: This is a misnomer! * + * https://docs.taler.net/core/api-bank-integration.html#post-$BANK_API_BASE_URL-withdrawal-operation-$wopid */ async completeWithdrawalOperationById( woid: string, diff --git a/packages/taler-util/src/types-taler-common.ts b/packages/taler-util/src/types-taler-common.ts @@ -232,6 +232,7 @@ export const codecForTalerCommonConfigResponse = .allowExtra() .property("name", codecForString()) .property("version", codecForString()) + .allowExtra() .build("TalerCommonConfigResponse"); export enum ExchangeProtocolVersion { diff --git a/packages/taler-util/src/types-taler-corebank.ts b/packages/taler-util/src/types-taler-corebank.ts @@ -61,6 +61,8 @@ export interface IntegrationConfig { // Name of the API. name: "taler-bank-integration"; + + implementation?: string; } export interface TalerCorebankConfigResponse { @@ -673,6 +675,7 @@ export const codecForIntegrationBankConfig = (): Codec<IntegrationConfig> => .property("version", codecForString()) .property("currency", codecForString()) .property("currency_specification", codecForCurrencySpecificiation()) + .property("implementation", codecOptional(codecForString())) .build("TalerCorebankApi.IntegrationConfig"); export const codecForCoreBankConfig = (): Codec<TalerCorebankConfigResponse> => diff --git a/packages/taler-util/src/types-taler-wallet-transactions.ts b/packages/taler-util/src/types-taler-wallet-transactions.ts @@ -161,6 +161,8 @@ export interface TransactionState { minor?: TransactionMinorState; } +export type TransactionStateWildcard = "*"; + export enum TransactionMajorState { // No state, only used when reporting transitions into the initial state None = "none", diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts @@ -89,7 +89,11 @@ import { codecForMerchantContractTerms, } from "./types-taler-merchant.js"; import { BackupRecovery } from "./types-taler-sync.js"; -import { TransactionState } from "./types-taler-wallet-transactions.js"; +import { + TransactionMajorState, + TransactionMinorState, + TransactionStateWildcard, +} from "./types-taler-wallet-transactions.js"; /** * Identifier for a transaction in the wallet. @@ -3288,9 +3292,14 @@ export interface TestingWaitExchangeStateRequest { walletKycStatus?: ExchangeWalletKycStatus; } +export interface TransactionStatePattern { + major: TransactionMajorState | TransactionStateWildcard; + minor?: TransactionMinorState | TransactionStateWildcard; +} + export interface TestingWaitTransactionRequest { transactionId: TransactionIdStr; - txState: TransactionState | TransactionState[]; + txState: TransactionStatePattern | TransactionStatePattern[]; } export interface TestingGetReserveHistoryRequest { diff --git a/packages/taler-wallet-core/src/testing.ts b/packages/taler-wallet-core/src/testing.ts @@ -48,6 +48,7 @@ import { TransactionMajorState, TransactionMinorState, TransactionState, + TransactionStatePattern, TransactionType, URL, WithdrawTestBalanceRequest, @@ -596,13 +597,23 @@ async function waitUntilTransactionPendingReady( }); } +function matchState( + st: TransactionState, + pat: TransactionStatePattern, +): boolean { + return ( + (pat.major === "*" || st.major === pat.major) && + (pat.minor === "*" || st.minor === pat.minor) + ); +} + /** * Wait until a transaction is in a particular state. */ export async function waitTransactionState( wex: WalletExecutionContext, transactionId: string, - txState: TransactionState | TransactionState[], + txState: TransactionStatePattern | TransactionStatePattern[], ): Promise<void> { logger.info( `starting waiting for ${transactionId} to be in ${JSON.stringify( @@ -616,19 +627,13 @@ export async function waitTransactionState( }); if (Array.isArray(txState)) { for (const myState of txState) { - if ( - tx.txState.major === myState.major && - tx.txState.minor === myState.minor - ) { + if (matchState(tx.txState, myState)) { return true; } } return false; } else { - return ( - tx.txState.major === txState.major && - tx.txState.minor === txState.minor - ); + return matchState(tx.txState, txState); } }, filterNotification(notif) { diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts @@ -763,7 +763,7 @@ export function computeWithdrawalTransactionStatus( switch (wgRecord.status) { case WithdrawalGroupStatus.FailedBankAborted: return { - major: TransactionMajorState.Aborted, + major: TransactionMajorState.Failed, }; case WithdrawalGroupStatus.Done: return { @@ -1057,6 +1057,35 @@ async function processWithdrawalGroupBalanceKyc( } } +/** + * Perform a simple transition of a withdrawal transaction + * from one state to another. + * + * If the transaction is in a different state, do not do anything. + */ +async function transitionSimple( + ctx: WithdrawTransactionContext, + from: WithdrawalGroupStatus, + to: WithdrawalGroupStatus, +): Promise<void> { + await ctx.transition({}, async (rec) => { + switch (rec?.status) { + case from: { + rec.status = to; + return TransitionResult.transition(rec); + } + } + return TransitionResult.stay(); + }); +} + +/** + * Handle state "dialog(proposed)" for a withdrawal transaction. + * + * In this state, we wait for the user to confirm + * and also monitor the state of the bank's withdrawal + * operation. + */ async function processWithdrawalGroupDialogProposed( ctx: WithdrawTransactionContext, withdrawalGroup: WithdrawalGroupRecord, @@ -1098,26 +1127,30 @@ async function processWithdrawalGroupDialogProposed( }, ); - // If the bank claims that the withdrawal operation is already - // pending, but we're still in DialogProposed, some other wallet - // must've completed the withdrawal, we're giving up. - switch (resp.status) { + case HttpStatusCode.NotFound: { + // FIXME: Further inspect the error body + await transitionSimple( + ctx, + WithdrawalGroupStatus.DialogProposed, + WithdrawalGroupStatus.AbortedBank, + ); + break; + } case HttpStatusCode.Ok: { + // If the bank claims that the withdrawal operation is already + // pending, but we're still in DialogProposed, some other wallet + // must've completed the withdrawal, we're giving up. const body = await readSuccessResponseJsonOrThrow( resp, codecForBankWithdrawalOperationStatus(), ); if (body.status !== "pending") { - await ctx.transition({}, async (rec) => { - switch (rec?.status) { - case WithdrawalGroupStatus.DialogProposed: { - rec.status = WithdrawalGroupStatus.AbortedOtherWallet; - return TransitionResult.transition(rec); - } - } - return TransitionResult.stay(); - }); + await transitionSimple( + ctx, + WithdrawalGroupStatus.DialogProposed, + WithdrawalGroupStatus.AbortedOtherWallet, + ); } break; } @@ -2830,16 +2863,21 @@ async function registerReserveWithBank( }); switch (httpResp.status) { + case HttpStatusCode.NotFound: { + // FIXME: Inspect particular status code + await transitionSimple( + ctx, + WithdrawalGroupStatus.PendingRegisteringBank, + WithdrawalGroupStatus.FailedBankAborted, + ); + return TaskRunResult.progress(); + } case HttpStatusCode.Conflict: - await ctx.transition({}, async (rec) => { - switch (rec?.status) { - case WithdrawalGroupStatus.PendingRegisteringBank: { - rec.status = WithdrawalGroupStatus.FailedBankAborted; - return TransitionResult.transition(rec); - } - } - return TransitionResult.stay(); - }); + await transitionSimple( + ctx, + WithdrawalGroupStatus.PendingRegisteringBank, + WithdrawalGroupStatus.FailedBankAborted, + ); return TaskRunResult.progress(); } @@ -2935,32 +2973,24 @@ async function processBankRegisterReserve( cancellationToken: wex.cancellationToken, }); - if (statusResp.status >= 400 && statusResp.status >= 499) { - let newSt: WithdrawalGroupStatus | undefined; - // FIXME: Consider looking at the exact status code - switch (statusResp.status) { - case HttpStatusCode.NotFound: - newSt = WithdrawalGroupStatus.FailedAbortingBank; - break; - case HttpStatusCode.Conflict: - newSt = WithdrawalGroupStatus.AbortedOtherWallet; - break; - default: - break; - } - if (newSt != null) { - // FIXME: Consider looking at the exact status code - await ctx.transition({}, async (rec) => { - switch (rec?.status) { - case WithdrawalGroupStatus.PendingRegisteringBank: { - rec.status = WithdrawalGroupStatus.FailedBankAborted; - return TransitionResult.transition(rec); - } - } - return TransitionResult.stay(); - }); + // FIXME: Consider looking at the exact taler error code + switch (statusResp.status) { + case HttpStatusCode.NotFound: + await transitionSimple( + ctx, + WithdrawalGroupStatus.PendingRegisteringBank, + WithdrawalGroupStatus.FailedBankAborted, + ); return TaskRunResult.progress(); - } + case HttpStatusCode.Conflict: + await transitionSimple( + ctx, + WithdrawalGroupStatus.PendingRegisteringBank, + WithdrawalGroupStatus.AbortedOtherWallet, + ); + return TaskRunResult.progress(); + default: + break; } const status = await readSuccessResponseJsonOrThrow(