From 22cb8adaa6d9584ad7638b9019b6d0c3eb7158bf Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Mon, 13 Feb 2023 13:15:47 +0100 Subject: wallet-core,harness: introduce reserveIsReady flag, test tx lifeycle --- packages/taler-harness/src/harness/harness.ts | 47 ++++++++--- packages/taler-harness/src/harness/helpers.ts | 25 +++++- .../taler-harness/src/integrationtests/test-kyc.ts | 2 +- .../test-withdrawal-bank-integrated.ts | 96 +++++++++++++++++++--- packages/taler-util/src/notifications.ts | 22 ++++- packages/taler-util/src/transactions-types.ts | 10 +++ .../src/operations/transactions.ts | 7 ++ .../taler-wallet-core/src/operations/withdraw.ts | 24 +++++- 8 files changed, 200 insertions(+), 33 deletions(-) diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts index 275592091..0b7ba14cf 100644 --- a/packages/taler-harness/src/harness/harness.ts +++ b/packages/taler-harness/src/harness/harness.ts @@ -1169,6 +1169,29 @@ export class ExchangeService implements ExchangeServiceInterface { return !!this.exchangeWirewatchProc || !!this.exchangeHttpProc; } + /** + * Stop the wirewatch service (which runs by default). + * + * Useful for some tests. + */ + async stopWirewatch(): Promise { + const wirewatch = this.exchangeWirewatchProc; + if (wirewatch) { + wirewatch.proc.kill("SIGTERM"); + await wirewatch.wait(); + this.exchangeWirewatchProc = undefined; + } + } + + async startWirewatch(): Promise { + const wirewatch = this.exchangeWirewatchProc; + if (wirewatch) { + logger.warn("wirewatch already running"); + } else { + this.internalCreateWirewatchProc(); + } + } + async stop(): Promise { const wirewatch = this.exchangeWirewatchProc; if (wirewatch) { @@ -1332,6 +1355,19 @@ export class ExchangeService implements ExchangeServiceInterface { ); } + private internalCreateWirewatchProc() { + this.exchangeWirewatchProc = this.globalState.spawnService( + "taler-exchange-wirewatch", + [ + "-c", + this.configFilename, + "--longpoll-timeout=5s", + ...this.timetravelArgArr, + ], + `exchange-wirewatch-${this.name}`, + ); + } + async start(): Promise { if (this.isRunning()) { throw Error("exchange is already running"); @@ -1360,16 +1396,7 @@ export class ExchangeService implements ExchangeServiceInterface { `exchange-crypto-rsa-${this.name}`, ); - this.exchangeWirewatchProc = this.globalState.spawnService( - "taler-exchange-wirewatch", - [ - "-c", - this.configFilename, - "--longpoll-timeout=5s", - ...this.timetravelArgArr, - ], - `exchange-wirewatch-${this.name}`, - ); + this.internalCreateWirewatchProc(); this.exchangeHttpProc = this.globalState.spawnService( "taler-exchange-httpd", diff --git a/packages/taler-harness/src/harness/helpers.ts b/packages/taler-harness/src/harness/helpers.ts index 4c2ca80a7..ad77ce6ca 100644 --- a/packages/taler-harness/src/harness/helpers.ts +++ b/packages/taler-harness/src/harness/helpers.ts @@ -209,7 +209,7 @@ export async function createSimpleTestkudosEnvironmentV2( t: GlobalTestState, coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")), opts: EnvOptions = {}, -): Promise { +): Promise { const db = await setupDb(t); const bank = await BankService.create(t, { @@ -293,15 +293,32 @@ export async function createSimpleTestkudosEnvironmentV2( ), }); - console.log("setup done!"); + const walletService = new WalletService(t, { + name: "wallet", + useInMemoryDb: true, + }); + await walletService.start(); + await walletService.pingUntilAvailable(); - const wallet = new WalletCli(t); + const walletClient = new WalletClient({ + unixPath: walletService.socketPath, + onNotification(n) { + console.log("got notification", n); + }, + }); + await walletClient.connect(); + await walletClient.client.call(WalletApiOperation.InitWallet, { + skipDefaults: true, + }); + + console.log("setup done!"); return { commonDb: db, exchange, merchant, - wallet, + walletClient, + walletService, bank, exchangeBankAccount, }; diff --git a/packages/taler-harness/src/integrationtests/test-kyc.ts b/packages/taler-harness/src/integrationtests/test-kyc.ts index b08db66f7..490673cee 100644 --- a/packages/taler-harness/src/integrationtests/test-kyc.ts +++ b/packages/taler-harness/src/integrationtests/test-kyc.ts @@ -307,7 +307,7 @@ export async function runKycTest(t: GlobalTestState) { // Withdraw const kycNotificationCond = walletClient.waitForNotificationCond((x) => { - if (x.type === NotificationType.WithdrawalKycRequested) { + if (x.type === NotificationType.WithdrawalGroupKycRequested) { return x; } return false; 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 dc7298e5d..c98c18db5 100644 --- a/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts +++ b/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts @@ -18,13 +18,13 @@ * Imports. */ import { GlobalTestState } from "../harness/harness.js"; -import { createSimpleTestkudosEnvironment } from "../harness/helpers.js"; +import { createSimpleTestkudosEnvironmentV2 } from "../harness/helpers.js"; import { WalletApiOperation, BankApi, BankAccessApi, } from "@gnu-taler/taler-wallet-core"; -import { j2s } from "@gnu-taler/taler-util"; +import { j2s, NotificationType, TransactionType, WithdrawalType } from "@gnu-taler/taler-util"; /** * Run test for basic, bank-integrated withdrawal. @@ -32,7 +32,8 @@ import { j2s } from "@gnu-taler/taler-util"; export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) { // Set up test environment - const { wallet, bank, exchange } = await createSimpleTestkudosEnvironment(t); + const { walletClient, bank, exchange } = + await createSimpleTestkudosEnvironmentV2(t); // Create a withdrawal operation @@ -45,46 +46,117 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) { // Hand it to the wallet - const r1 = await wallet.client.call( + const r1 = await walletClient.client.call( WalletApiOperation.GetWithdrawalDetailsForUri, { talerWithdrawUri: wop.taler_withdraw_uri, }, ); - await wallet.runPending(); - // Withdraw - const r2 = await wallet.client.call( + const withdrawalBankConfirmedCond = walletClient.waitForNotificationCond( + (x) => { + return x.type === NotificationType.WithdrawalGroupBankConfirmed; + }, + ); + + const withdrawalFinishedCond = walletClient.waitForNotificationCond((x) => { + return x.type === NotificationType.WithdrawGroupFinished; + }); + + const withdrawalReserveReadyCond = walletClient.waitForNotificationCond( + (x) => { + return x.type === NotificationType.WithdrawalGroupReserveReady; + }, + ); + + const r2 = await walletClient.client.call( WalletApiOperation.AcceptBankIntegratedWithdrawal, { exchangeBaseUrl: exchange.baseUrl, talerWithdrawUri: wop.taler_withdraw_uri, }, ); + // Do it twice to check idempotency - const r3 = await wallet.client.call( + const r3 = await walletClient.client.call( WalletApiOperation.AcceptBankIntegratedWithdrawal, { exchangeBaseUrl: exchange.baseUrl, talerWithdrawUri: wop.taler_withdraw_uri, }, ); - await wallet.runPending(); + + await exchange.stopWirewatch(); + + // Check status before withdrawal is confirmed by bank. + { + const txn = await walletClient.client.call( + WalletApiOperation.GetTransactions, + {}, + ); + console.log("transactions before confirmation:", j2s(txn)); + const tx0 = txn.transactions[0]; + t.assertTrue(tx0.type === TransactionType.Withdrawal); + t.assertTrue(tx0.withdrawalDetails.type === WithdrawalType.TalerBankIntegrationApi); + t.assertTrue(tx0.withdrawalDetails.confirmed === false); + t.assertTrue(tx0.withdrawalDetails.reserveIsReady === false); + } // Confirm it await BankApi.confirmWithdrawalOperation(bank, user, wop); - await wallet.runUntilDone(); + await withdrawalBankConfirmedCond; + + // Check status after withdrawal is confirmed by bank, + // but before funds are wired to the exchange. + { + const txn = await walletClient.client.call( + WalletApiOperation.GetTransactions, + {}, + ); + console.log("transactions after confirmation:", j2s(txn)); + const tx0 = txn.transactions[0]; + t.assertTrue(tx0.type === TransactionType.Withdrawal); + t.assertTrue(tx0.withdrawalDetails.type === WithdrawalType.TalerBankIntegrationApi); + t.assertTrue(tx0.withdrawalDetails.confirmed === true); + t.assertTrue(tx0.withdrawalDetails.reserveIsReady === false); + } + + await exchange.startWirewatch(); + + await withdrawalReserveReadyCond; + + // Check status after funds were wired. + { + const txn = await walletClient.client.call( + WalletApiOperation.GetTransactions, + {}, + ); + console.log("transactions after reserve ready:", j2s(txn)); + const tx0 = txn.transactions[0]; + t.assertTrue(tx0.type === TransactionType.Withdrawal); + t.assertTrue(tx0.withdrawalDetails.type === WithdrawalType.TalerBankIntegrationApi); + t.assertTrue(tx0.withdrawalDetails.confirmed === true); + t.assertTrue(tx0.withdrawalDetails.reserveIsReady === true); + } + + await withdrawalFinishedCond; // Check balance - const balResp = await wallet.client.call(WalletApiOperation.GetBalances, {}); + const balResp = await walletClient.client.call( + WalletApiOperation.GetBalances, + {}, + ); t.assertAmountEquals("TESTKUDOS:9.72", balResp.balances[0].available); - const txn = await wallet.client.call(WalletApiOperation.GetTransactions, {}); + const txn = await walletClient.client.call( + WalletApiOperation.GetTransactions, + {}, + ); console.log(`transactions: ${j2s(txn)}`); } diff --git a/packages/taler-util/src/notifications.ts b/packages/taler-util/src/notifications.ts index 9d3ca32b0..823c4130d 100644 --- a/packages/taler-util/src/notifications.ts +++ b/packages/taler-util/src/notifications.ts @@ -62,7 +62,9 @@ export enum NotificationType { PendingOperationProcessed = "pending-operation-processed", ProposalRefused = "proposal-refused", ReserveRegisteredWithBank = "reserve-registered-with-bank", - WithdrawalKycRequested = "withdrawal-kyc-requested", + WithdrawalGroupKycRequested = "withdrawal-group-kyc-requested", + WithdrawalGroupBankConfirmed = "withdrawal-group-bank-confirmed", + WithdrawalGroupReserveReady = "withdrawal-group-reserve-ready", DepositOperationError = "deposit-operation-error", } @@ -118,12 +120,22 @@ export interface RefreshMeltedNotification { type: NotificationType.RefreshMelted; } -export interface WithdrawalKycRequested { - type: NotificationType.WithdrawalKycRequested; +export interface WithdrawalGroupKycRequested { + type: NotificationType.WithdrawalGroupKycRequested; transactionId: string; kycUrl: string; } +export interface WithdrawalGroupBankConfirmed { + type: NotificationType.WithdrawalGroupBankConfirmed; + transactionId: string; +} + +export interface WithdrawalGroupReserveReady { + type: NotificationType.WithdrawalGroupReserveReady; + transactionId: string; +} + export interface RefreshRevealedNotification { type: NotificationType.RefreshRevealed; } @@ -293,4 +305,6 @@ export type WalletNotification = | ReserveRegisteredWithBankNotification | ReserveNotYetFoundNotification | PayOperationSuccessNotification - | WithdrawalKycRequested; + | WithdrawalGroupKycRequested + | WithdrawalGroupBankConfirmed + | WithdrawalGroupReserveReady; diff --git a/packages/taler-util/src/transactions-types.ts b/packages/taler-util/src/transactions-types.ts index 7562b5884..d108d1a77 100644 --- a/packages/taler-util/src/transactions-types.ts +++ b/packages/taler-util/src/transactions-types.ts @@ -167,6 +167,11 @@ interface WithdrawalDetailsForManualTransfer { // Public key of the reserve reservePub: string; + + /** + * Is the reserve ready for withdrawal? + */ + reserveIsReady: boolean; } interface WithdrawalDetailsForTalerBankIntegrationApi { @@ -187,6 +192,11 @@ interface WithdrawalDetailsForTalerBankIntegrationApi { // Public key of the reserve reservePub: string; + + /** + * Is the reserve ready for withdrawal? + */ + reserveIsReady: boolean; } // This should only be used for actual withdrawals diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index 9951fd6b2..c1b82f9c9 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -55,6 +55,7 @@ import { PeerPushPaymentInitiationStatus, PeerPullPaymentIncomingStatus, TransactionStatus, + WithdrawalGroupStatus, } from "../db.js"; import { InternalWalletState } from "../internal-wallet-state.js"; import { assertUnreachable } from "../util/assertUnreachable.js"; @@ -515,6 +516,9 @@ function buildTransactionForBankIntegratedWithdraw( confirmed: wsr.wgInfo.bankInfo.timestampBankConfirmed ? true : false, reservePub: wsr.reservePub, bankConfirmationUrl: wsr.wgInfo.bankInfo.confirmUrl, + reserveIsReady: + wsr.status === WithdrawalGroupStatus.Finished || + wsr.status === WithdrawalGroupStatus.Ready, }, exchangeBaseUrl: wsr.exchangeBaseUrl, extendedStatus: wsr.timestampFinish @@ -558,6 +562,9 @@ function buildTransactionForManualWithdraw( type: WithdrawalType.ManualTransfer, reservePub: withdrawalGroup.reservePub, exchangePaytoUris, + reserveIsReady: + withdrawalGroup.status === WithdrawalGroupStatus.Finished || + withdrawalGroup.status === WithdrawalGroupStatus.Ready, }, exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl, extendedStatus: withdrawalGroup.timestampFinish diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 28754c77e..bcc8600c7 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -987,6 +987,14 @@ async function queryReserve( await tx.withdrawalGroups.put(wg); }); + ws.notify({ + type: NotificationType.WithdrawalGroupReserveReady, + transactionId: makeTransactionId( + TransactionType.Withdrawal, + withdrawalGroupId, + ), + }); + return { ready: true }; } @@ -1250,7 +1258,12 @@ export async function processWithdrawalGroup( if (numKycRequired > 0) { if (kycInfo) { - await checkWithdrawalKycStatus(ws, withdrawalGroup, kycInfo, "individual"); + await checkWithdrawalKycStatus( + ws, + withdrawalGroup, + kycInfo, + "individual", + ); return { type: OperationAttemptResultType.Pending, result: undefined, @@ -1310,7 +1323,7 @@ export async function checkWithdrawalKycStatus( const kycStatus = await kycStatusReq.json(); logger.info(`kyc status: ${j2s(kycStatus)}`); ws.notify({ - type: NotificationType.WithdrawalKycRequested, + type: NotificationType.WithdrawalGroupKycRequested, kycUrl: kycStatus.kyc_url, transactionId: makeTransactionId( TransactionType.Withdrawal, @@ -1794,6 +1807,13 @@ async function processReserveBankStatus( const now = AbsoluteTime.toTimestamp(AbsoluteTime.now()); r.wgInfo.bankInfo.timestampBankConfirmed = now; r.status = WithdrawalGroupStatus.QueryingStatus; + ws.notify({ + type: NotificationType.WithdrawalGroupBankConfirmed, + transactionId: makeTransactionId( + TransactionType.Withdrawal, + r.withdrawalGroupId, + ), + }); } else { logger.info("withdrawal: transfer not yet confirmed by bank"); r.wgInfo.bankInfo.confirmUrl = status.confirm_transfer_url; -- cgit v1.2.3