diff options
author | Florian Dold <florian@dold.me> | 2024-01-29 21:29:26 +0100 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2024-01-29 21:29:35 +0100 |
commit | 61e3484d6ce1db846c435d84cbf5b9c3711a30f2 (patch) | |
tree | 841e9889c5814c458622904203a46853aa0e2708 | |
parent | 57d988cf4ceffb51e8936bd36eed4aefcdc2d2dc (diff) |
wallet-core,harness: let runIntegrationTest wait for its own transactions
16 files changed, 391 insertions, 139 deletions
diff --git a/packages/taler-harness/src/integrationtests/test-wallettesting.ts b/packages/taler-harness/src/integrationtests/test-wallettesting.ts index ec393204f..932284d62 100644 --- a/packages/taler-harness/src/integrationtests/test-wallettesting.ts +++ b/packages/taler-harness/src/integrationtests/test-wallettesting.ts @@ -232,6 +232,16 @@ export async function runWallettestingTest(t: GlobalTestState) { merchantBaseUrl: merchant.makeInstanceBaseUrl(), summary: "foo", }); + + await walletClient.call(WalletApiOperation.ClearDb, {}); + await walletClient.call(WalletApiOperation.RunIntegrationTestV2, { + amountToSpend: "TESTKUDOS:5" as AmountString, + amountToWithdraw: "TESTKUDOS:10" as AmountString, + corebankApiBaseUrl: bank.corebankApiBaseUrl, + exchangeBaseUrl: exchange.baseUrl, + merchantAuthToken: merchantAuthToken, + merchantBaseUrl: merchant.makeInstanceBaseUrl(), + }); } runWallettestingTest.suites = ["wallet"]; diff --git a/packages/taler-util/src/transactions-types.ts b/packages/taler-util/src/transactions-types.ts index 00802577a..a0bc2a89d 100644 --- a/packages/taler-util/src/transactions-types.ts +++ b/packages/taler-util/src/transactions-types.ts @@ -207,7 +207,8 @@ export type Transaction = | TransactionPeerPullDebit | TransactionPeerPushCredit | TransactionPeerPushDebit - | TransactionInternalWithdrawal; + | TransactionInternalWithdrawal + | TransactionRecoup; export enum TransactionType { Withdrawal = "withdrawal", @@ -221,6 +222,7 @@ export enum TransactionType { PeerPushCredit = "peer-push-credit", PeerPullDebit = "peer-pull-debit", PeerPullCredit = "peer-pull-credit", + Recoup = "recoup", } export enum WithdrawalType { @@ -446,6 +448,13 @@ export interface TransactionPeerPushCredit extends TransactionCommon { amountEffective: AmountString; } +/** + * The exchange revoked a key and the wallet recoups funds. + */ +export interface TransactionRecoup extends TransactionCommon { + type: TransactionType.Recoup; +} + export enum PaymentStatus { /** * Explicitly aborted after timeout / failure @@ -754,3 +763,17 @@ export const codecForOrderShortInfo = (): Codec<OrderShortInfo> => .property("summary", codecForString()) .property("summary_i18n", codecOptional(codecForInternationalizedString())) .build("OrderShortInfo"); + +export interface ListAssociatedRefreshesRequest { + transactionId: string; +} + +export const codecForListAssociatedRefreshesRequest = + (): Codec<ListAssociatedRefreshesRequest> => + buildCodecForObject<ListAssociatedRefreshesRequest>() + .property("transactionId", codecForString()) + .build("ListAssociatedRefreshesRequest"); + +export interface ListAssociatedRefreshesResponse { + transactionIds: string[]; +} diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts index 806e2f22b..6fcd43fc5 100644 --- a/packages/taler-util/src/wallet-types.ts +++ b/packages/taler-util/src/wallet-types.ts @@ -577,11 +577,11 @@ export interface CoinDumpJson { withdrawal_reserve_pub: string | undefined; coin_status: CoinStatus; spend_allocation: - | { - id: string; - amount: AmountString; - } - | undefined; + | { + id: string; + amount: AmountString; + } + | undefined; /** * Information about the age restriction */ @@ -2095,6 +2095,9 @@ export interface PrepareRefundRequest { } export interface StartRefundQueryForUriResponse { + /** + * Transaction id of the *payment* where the refund query was started. + */ transactionId: TransactionIdStr; } diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index a0613fd39..b0605cb1d 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -151,7 +151,7 @@ export const CURRENT_DB_CONFIG_KEY = "currentMainDbName"; * backwards-compatible way or object stores and indices * are added. */ -export const WALLET_DB_MINOR_VERSION = 4; +export const WALLET_DB_MINOR_VERSION = 5; declare const symDbProtocolTimestamp: unique symbol; @@ -996,14 +996,6 @@ export enum DepositElementStatus { RefundFailed = 0x0501_0000, } -/** - * Additional information about the reason of a refresh. - */ -export interface RefreshReasonDetails { - originatingTransactionId?: string; - proposalId?: string; -} - export interface RefreshGroupPerExchangeInfo { /** * (Expected) output once the refresh group succeeded. @@ -1035,10 +1027,7 @@ export interface RefreshGroupRecord { */ reason: RefreshReason; - /** - * Extra information depending on the reason. - */ - reasonDetails?: RefreshReasonDetails; + originatingTransactionId?: string; oldCoinPubs: string[]; @@ -2461,6 +2450,13 @@ export const WalletStoresV1 = { }), { byStatus: describeIndex("byStatus", "operationStatus"), + byOriginatingTransactionId: describeIndex( + "byOriginatingTransactionId", + "originatingTransactionId", + { + versionAdded: 5, + }, + ), }, ), refreshSessions: describeStore( diff --git a/packages/taler-wallet-core/src/internal-wallet-state.ts b/packages/taler-wallet-core/src/internal-wallet-state.ts index 8c49f8e5e..fdf04a65f 100644 --- a/packages/taler-wallet-core/src/internal-wallet-state.ts +++ b/packages/taler-wallet-core/src/internal-wallet-state.ts @@ -29,8 +29,8 @@ /** * Imports. */ +import { IDBFactory } from "@gnu-taler/idb-bridge"; import { - CancellationToken, CoinRefreshRequest, DenominationInfo, RefreshGroupId, @@ -40,12 +40,7 @@ import { } from "@gnu-taler/taler-util"; import { HttpRequestLibrary } from "@gnu-taler/taler-util/http"; import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js"; -import { - ExchangeDetailsRecord, - ExchangeEntryRecord, - RefreshReasonDetails, - WalletStoresV1, -} from "./db.js"; +import { WalletStoresV1 } from "./db.js"; import { AsyncCondition } from "./util/promiseUtils.js"; import { DbAccess, @@ -54,8 +49,6 @@ import { } from "./util/query.js"; import { TimerGroup } from "./util/timer.js"; import { WalletConfig } from "./wallet-api-types.js"; -import { IDBFactory } from "@gnu-taler/idb-bridge"; -import { ReadyExchangeSummary } from "./index.js"; export const EXCHANGE_COINS_LOCK = "exchange-coins-lock"; export const EXCHANGE_RESERVES_LOCK = "exchange-reserves-lock"; @@ -69,22 +62,6 @@ export interface MerchantInfo { protocolVersionCurrent: number; } -export interface RefreshOperations { - createRefreshGroup( - ws: InternalWalletState, - tx: GetReadWriteAccess<{ - denominations: typeof WalletStoresV1.denominations; - coins: typeof WalletStoresV1.coins; - refreshGroups: typeof WalletStoresV1.refreshGroups; - coinAvailability: typeof WalletStoresV1.coinAvailability; - }>, - currency: string, - oldCoinPubs: CoinRefreshRequest[], - reason: RefreshReason, - reasonDetails?: RefreshReasonDetails, - ): Promise<RefreshGroupId>; -} - export interface RecoupOperations { createRecoupGroup( ws: InternalWalletState, @@ -144,7 +121,6 @@ export interface InternalWalletState { merchantInfoCache: Record<string, MerchantInfo>; recoupOps: RecoupOperations; - refreshOps: RefreshOperations; isTaskLoopRunning: boolean; diff --git a/packages/taler-wallet-core/src/operations/common.ts b/packages/taler-wallet-core/src/operations/common.ts index d626f0056..460a32666 100644 --- a/packages/taler-wallet-core/src/operations/common.ts +++ b/packages/taler-wallet-core/src/operations/common.ts @@ -70,6 +70,7 @@ import { PendingTaskType, TaskId } from "../pending-types.js"; import { assertUnreachable } from "../util/assertUnreachable.js"; import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; import { GetReadOnlyAccess, GetReadWriteAccess } from "../util/query.js"; +import { createRefreshGroup } from "./refresh.js"; import { constructTransactionIdentifier } from "./transactions.js"; const logger = new Logger("operations/common.ts"); @@ -240,15 +241,13 @@ export async function spendCoins( await tx.coinAvailability.put(coinAvailability); } - await ws.refreshOps.createRefreshGroup( + await createRefreshGroup( ws, tx, Amounts.currencyOf(csi.contributions[0]), refreshCoinPubs, csi.refreshReason, - { - originatingTransactionId: csi.allocationId, - }, + csi.allocationId, ); } diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts index 62c1e406c..4a6791922 100644 --- a/packages/taler-wallet-core/src/operations/deposits.ts +++ b/packages/taler-wallet-core/src/operations/deposits.ts @@ -558,6 +558,10 @@ async function refundDepositGroup( currency, refreshCoins, RefreshReason.AbortDeposit, + constructTransactionIdentifier({ + tag: TransactionType.Deposit, + depositGroupId: newDg.depositGroupId, + }), ); newDg.abortRefreshGroupId = rgid.refreshGroupId; } diff --git a/packages/taler-wallet-core/src/operations/pay-merchant.ts b/packages/taler-wallet-core/src/operations/pay-merchant.ts index f6bbe5b9f..c71dd7d90 100644 --- a/packages/taler-wallet-core/src/operations/pay-merchant.ts +++ b/packages/taler-wallet-core/src/operations/pay-merchant.ts @@ -146,6 +146,7 @@ import { import { constructTransactionIdentifier, notifyTransition, + parseTransactionIdentifier, stopLongpolling, } from "./transactions.js"; @@ -319,6 +320,7 @@ export class PayMerchantTransactionContext implements TransactionContext { currency, refreshCoins, RefreshReason.AbortPay, + this.transactionId, ); } break; @@ -1669,10 +1671,15 @@ async function runPayForConfirmPay( */ export async function confirmPay( ws: InternalWalletState, - proposalId: string, + transactionId: string, sessionIdOverride?: string, forcedCoinSel?: ForcedCoinSel, ): Promise<ConfirmPayResult> { + const parsedTx = parseTransactionIdentifier(transactionId); + if (parsedTx?.tag !== TransactionType.Payment) { + throw Error("expected payment transaction ID"); + } + const proposalId = parsedTx.proposalId; logger.trace( `executing confirmPay with proposalId ${proposalId} and sessionIdOverride ${sessionIdOverride}`, ); @@ -1686,11 +1693,6 @@ export async function confirmPay( throw Error(`proposal with id ${proposalId} not found`); } - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Payment, - proposalId, - }); - const d = await expectProposalDownload(ws, proposal); if (!d) { throw Error("proposal is in invalid state"); @@ -1815,6 +1817,8 @@ export async function confirmPay( hintTransactionId: transactionId, }); + // We directly make a first attempt to pay. + // FIXME: In the future we should just wait for the right event return runPayForConfirmPay(ws, proposalId); } @@ -3082,6 +3086,12 @@ async function storeRefunds( Amounts.currencyOf(download.contractData.amount), refreshCoins, RefreshReason.Refund, + // Since refunds are really just pseudo-transactions, + // the originating transaction for the refresh is the payment transaction. + constructTransactionIdentifier({ + tag: TransactionType.Payment, + proposalId: myPurchase.proposalId, + }), ); } } diff --git a/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts index 0f9f29fb5..c7e447dab 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts @@ -267,6 +267,7 @@ export class PeerPullDebitTransactionContext implements TransactionContext { currency, coinPubs, RefreshReason.AbortPeerPullDebit, + this.transactionId, ); pi.status = PeerPullDebitRecordStatus.AbortingRefresh; diff --git a/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts index 4fd1ef3b2..2e5af4e78 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts @@ -615,6 +615,7 @@ async function processPeerPushDebitAbortingDeletePurse( currency, coinPubs, RefreshReason.AbortPeerPushDebit, + transactionId, ); ppiRec.status = PeerPushDebitStatus.AbortingRefreshDeleted; ppiRec.abortRefreshGroupId = refresh.refreshGroupId; @@ -847,6 +848,7 @@ async function processPeerPushDebitReady( currency, coinPubs, RefreshReason.AbortPeerPushDebit, + transactionId, ); ppiRec.status = PeerPushDebitStatus.AbortingRefreshExpired; ppiRec.abortRefreshGroupId = refresh.refreshGroupId; diff --git a/packages/taler-wallet-core/src/operations/recoup.ts b/packages/taler-wallet-core/src/operations/recoup.ts index 782e98d1c..0ae873125 100644 --- a/packages/taler-wallet-core/src/operations/recoup.ts +++ b/packages/taler-wallet-core/src/operations/recoup.ts @@ -30,6 +30,7 @@ import { Logger, RefreshReason, TalerPreciseTimestamp, + TransactionType, URL, codecForRecoupConfirmation, codecForReserveStatus, @@ -52,9 +53,15 @@ import { import { InternalWalletState } from "../internal-wallet-state.js"; import { checkDbInvariant } from "../util/invariants.js"; import { GetReadWriteAccess } from "../util/query.js"; -import { TaskRunResult } from "./common.js"; +import { + TaskRunResult, + TransactionContext, + constructTaskIdentifier, +} from "./common.js"; import { createRefreshGroup } from "./refresh.js"; import { internalCreateWithdrawalGroup } from "./withdraw.js"; +import { constructTransactionIdentifier } from "./transactions.js"; +import { PendingTaskType } from "../pending-types.js"; const logger = new Logger("operations/recoup.ts"); @@ -394,12 +401,16 @@ export async function processRecoupGroup( } rg2.timestampFinished = timestampPreciseToDb(TalerPreciseTimestamp.now()); if (rg2.scheduleRefreshCoins.length > 0) { - const refreshGroupId = await createRefreshGroup( + await createRefreshGroup( ws, tx, Amounts.currencyOf(rg2.scheduleRefreshCoins[0].amount), rg2.scheduleRefreshCoins, RefreshReason.Recoup, + constructTransactionIdentifier({ + tag: TransactionType.Recoup, + recoupGroupId: rg2.recoupGroupId, + }), ); } await tx.recoupGroups.put(rg2); @@ -407,6 +418,40 @@ export async function processRecoupGroup( return TaskRunResult.finished(); } +export class RewardTransactionContext implements TransactionContext { + abortTransaction(): Promise<void> { + throw new Error("Method not implemented."); + } + suspendTransaction(): Promise<void> { + throw new Error("Method not implemented."); + } + resumeTransaction(): Promise<void> { + throw new Error("Method not implemented."); + } + failTransaction(): Promise<void> { + throw new Error("Method not implemented."); + } + deleteTransaction(): Promise<void> { + throw new Error("Method not implemented."); + } + public transactionId: string; + public retryTag: string; + + constructor( + public ws: InternalWalletState, + private recoupGroupId: string, + ) { + this.transactionId = constructTransactionIdentifier({ + tag: TransactionType.Recoup, + recoupGroupId, + }); + this.retryTag = constructTaskIdentifier({ + tag: PendingTaskType.Recoup, + recoupGroupId, + }); + } +} + export async function createRecoupGroup( ws: InternalWalletState, tx: GetReadWriteAccess<{ diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts index efaeeefb1..39c6ef906 100644 --- a/packages/taler-wallet-core/src/operations/refresh.ts +++ b/packages/taler-wallet-core/src/operations/refresh.ts @@ -75,7 +75,6 @@ import { RefreshCoinStatus, RefreshGroupRecord, RefreshOperationStatus, - RefreshReasonDetails, WalletStoresV1, } from "../db.js"; import { @@ -84,7 +83,6 @@ import { RefreshSessionRecord, timestampPreciseToDb, timestampProtocolFromDb, - WalletDbReadWriteTransaction, WalletDbReadWriteTransactionArr, } from "../index.js"; import { @@ -1268,8 +1266,8 @@ export async function createRefreshGroup( >, currency: string, oldCoinPubs: CoinRefreshRequest[], - reason: RefreshReason, - reasonDetails?: RefreshReasonDetails, + refreshReason: RefreshReason, + originatingTransactionId: string | undefined, ): Promise<CreateRefreshGroupResult> { const refreshGroupId = encodeCrock(getRandomBytes(32)); @@ -1285,8 +1283,8 @@ export async function createRefreshGroup( timestampFinished: undefined, statusPerCoin: oldCoinPubs.map(() => RefreshCoinStatus.Pending), oldCoinPubs: oldCoinPubs.map((x) => x.coinPub), - reasonDetails, - reason, + originatingTransactionId, + reason: refreshReason, refreshGroupId, inputPerCoin: oldCoinPubs.map((x) => x.amount), expectedOutputPerCoin: estimatedOutputPerCoin.map((x) => @@ -1418,6 +1416,7 @@ export async function autoRefresh( exchange.detailsPointer?.currency, refreshCoins, RefreshReason.Scheduled, + undefined, ); logger.trace( `created refresh group for auto-refresh (${res.refreshGroupId})`, @@ -1476,6 +1475,24 @@ export function computeRefreshTransactionActions( } } +export function getRefreshesForTransaction( + ws: InternalWalletState, + transactionId: string, +): Promise<string[]> { + return ws.db.runReadOnlyTx(["refreshGroups"], async (tx) => { + const groups = + await tx.refreshGroups.indexes.byOriginatingTransactionId.getAll( + transactionId, + ); + return groups.map((x) => + constructTransactionIdentifier({ + tag: TransactionType.Refresh, + refreshGroupId: x.refreshGroupId, + }), + ); + }); +} + export async function forceRefresh( ws: InternalWalletState, req: ForceRefreshRequest, @@ -1515,6 +1532,7 @@ export async function forceRefresh( Amounts.currencyOf(coinPubs[0].amount), coinPubs, RefreshReason.Manual, + undefined, ); }); diff --git a/packages/taler-wallet-core/src/operations/testing.ts b/packages/taler-wallet-core/src/operations/testing.ts index d75fb54a7..f2fb74bdb 100644 --- a/packages/taler-wallet-core/src/operations/testing.ts +++ b/packages/taler-wallet-core/src/operations/testing.ts @@ -54,7 +54,11 @@ import { HttpRequestLibrary, readSuccessResponseJsonOrThrow, } from "@gnu-taler/taler-util/http"; -import { OpenedPromise, openPromise } from "../index.js"; +import { + getRefreshesForTransaction, + OpenedPromise, + openPromise, +} from "../index.js"; import { InternalWalletState } from "../internal-wallet-state.js"; import { checkLogicInvariant } from "../util/invariants.js"; import { getBalances } from "./balance.js"; @@ -85,10 +89,17 @@ interface MerchantBackendInfo { authToken?: string; } +export interface WithdrawTestBalanceResult { + /** + * Transaction ID of the newly created withdrawal transaction. + */ + transactionId: string; +} + export async function withdrawTestBalance( ws: InternalWalletState, req: WithdrawTestBalanceRequest, -): Promise<void> { +): Promise<WithdrawTestBalanceResult> { const amount = req.amount; const exchangeBaseUrl = req.exchangeBaseUrl; const corebankApiBaseUrl = req.corebankApiBaseUrl; @@ -109,7 +120,7 @@ export async function withdrawTestBalance( amount, ); - await acceptWithdrawalFromUri(ws, { + const acceptResp = await acceptWithdrawalFromUri(ws, { talerWithdrawUri: wresp.taler_withdraw_uri, selectedExchange: exchangeBaseUrl, forcedDenomSel: req.forcedDenomSel, @@ -118,6 +129,10 @@ export async function withdrawTestBalance( await corebankClient.confirmWithdrawalOperation(bankUser.username, { withdrawalOperationId: wresp.withdrawal_id, }); + + return { + transactionId: acceptResp.transactionId, + }; } /** @@ -151,7 +166,9 @@ async function refund( reason, refund: refundAmount, }; - const resp = await http.postJson(reqUrl.href, refundReq, { + const resp = await http.fetch(reqUrl.href, { + method: "POST", + body: refundReq, headers: getMerchantAuthHeader(merchantBackend), }); const r = await readSuccessResponseJsonOrThrow(resp, codecForAny()); @@ -210,12 +227,17 @@ async function checkPayment( return readSuccessResponseJsonOrThrow(resp, codecForCheckPaymentResponse()); } +interface MakePaymentResult { + orderId: string; + paymentTransactionId: string; +} + async function makePayment( ws: InternalWalletState, merchant: MerchantBackendInfo, amount: string, summary: string, -): Promise<{ orderId: string }> { +): Promise<MakePaymentResult> { const orderResp = await createOrder( ws.http, merchant, @@ -245,7 +267,7 @@ async function makePayment( const confirmPayResult = await confirmPay( ws, - preparePayResult.proposalId, + preparePayResult.transactionId, undefined, ); @@ -261,6 +283,7 @@ async function makePayment( return { orderId: orderResp.orderId, + paymentTransactionId: preparePayResult.transactionId, }; } @@ -274,12 +297,12 @@ export async function runIntegrationTest( const currency = parsedSpendAmount.currency; logger.info("withdrawing test balance"); - await withdrawTestBalance(ws, { + const withdrawRes1 = await withdrawTestBalance(ws, { amount: args.amountToWithdraw, corebankApiBaseUrl: args.corebankApiBaseUrl, exchangeBaseUrl: args.exchangeBaseUrl, }); - await waitUntilTransactionsFinal(ws); + await waitUntilGivenTransactionsFinal(ws, [withdrawRes1.transactionId]); logger.info("done withdrawing test balance"); const balance = await getBalances(ws); @@ -291,10 +314,17 @@ export async function runIntegrationTest( authToken: args.merchantAuthToken, }; - await makePayment(ws, myMerchant, args.amountToSpend, "hello world"); + const makePaymentRes = await makePayment( + ws, + myMerchant, + args.amountToSpend, + "hello world", + ); - // Wait until the refresh is done - await waitUntilTransactionsFinal(ws); + await waitUntilTransactionWithAssociatedRefreshesFinal( + ws, + makePaymentRes.paymentTransactionId, + ); logger.trace("withdrawing test balance for refund"); const withdrawAmountTwo = Amounts.parseOrThrow(`${currency}:18`); @@ -302,14 +332,13 @@ export async function runIntegrationTest( const refundAmount = Amounts.parseOrThrow(`${currency}:6`); const spendAmountThree = Amounts.parseOrThrow(`${currency}:3`); - await withdrawTestBalance(ws, { + const withdrawRes2 = await withdrawTestBalance(ws, { amount: Amounts.stringify(withdrawAmountTwo), corebankApiBaseUrl: args.corebankApiBaseUrl, exchangeBaseUrl: args.exchangeBaseUrl, }); - // Wait until the withdraw is done - await waitUntilTransactionsFinal(ws); + await waitUntilGivenTransactionsFinal(ws, [withdrawRes2.transactionId]); const { orderId: refundOrderId } = await makePayment( ws, @@ -328,16 +357,19 @@ export async function runIntegrationTest( logger.trace("refund URI", refundUri); - await startRefundQueryForUri(ws, refundUri); + const refundResp = await startRefundQueryForUri(ws, refundUri); logger.trace("integration test: applied refund"); // Wait until the refund is done - await waitUntilTransactionsFinal(ws); + await waitUntilTransactionWithAssociatedRefreshesFinal( + ws, + refundResp.transactionId, + ); logger.trace("integration test: making payment after refund"); - await makePayment( + const paymentResp2 = await makePayment( ws, myMerchant, Amounts.stringify(spendAmountThree), @@ -346,7 +378,13 @@ export async function runIntegrationTest( logger.trace("integration test: make payment done"); - await waitUntilTransactionsFinal(ws); + await waitUntilGivenTransactionsFinal(ws, [ + paymentResp2.paymentTransactionId, + ]); + await waitUntilGivenTransactionsFinal( + ws, + await getRefreshesForTransaction(ws, paymentResp2.paymentTransactionId), + ); logger.trace("integration test: all done!"); } @@ -354,7 +392,7 @@ export async function runIntegrationTest( /** * Wait until all transactions are in a final state. */ -export async function waitUntilTransactionsFinal( +export async function waitUntilAllTransactionsFinal( ws: InternalWalletState, ): Promise<void> { logger.info("waiting until all transactions are in a final state"); @@ -405,6 +443,76 @@ export async function waitUntilTransactionsFinal( } /** + * Wait until all chosen transactions are in a final state. + */ +export async function waitUntilGivenTransactionsFinal( + ws: InternalWalletState, + transactionIds: string[], +): Promise<void> { + logger.info( + `waiting until given ${transactionIds.length} transactions are in a final state`, + ); + logger.info(`transaction IDs are: ${j2s(transactionIds)}`); + if (transactionIds.length === 0) { + return; + } + ws.ensureTaskLoopRunning(); + const txIdSet = new Set(transactionIds); + let p: OpenedPromise<void> | undefined = undefined; + const cancelNotifs = ws.addNotificationListener((notif) => { + if (!p) { + return; + } + if (notif.type === NotificationType.TransactionStateTransition) { + if (!txIdSet.has(notif.transactionId)) { + return; + } + switch (notif.newTxState.major) { + case TransactionMajorState.Pending: + case TransactionMajorState.Aborting: + case TransactionMajorState.Suspended: + case TransactionMajorState.SuspendedAborting: + break; + default: + p.resolve(); + } + } + }); + while (1) { + p = openPromise(); + const txs = await getTransactions(ws, { + includeRefreshes: true, + filterByState: "nonfinal", + }); + let finished = true; + for (const tx of txs.transactions) { + if (!txIdSet.has(tx.transactionId)) { + // Don't look at this transaction, we're not interested in it. + continue; + } + switch (tx.txState.major) { + case TransactionMajorState.Pending: + case TransactionMajorState.Aborting: + case TransactionMajorState.Suspended: + case TransactionMajorState.SuspendedAborting: + finished = false; + logger.info( + `continuing waiting, ${tx.transactionId} in ${tx.txState.major}(${tx.txState.minor})`, + ); + break; + } + } + if (finished) { + break; + } + // Wait until transaction state changed + await p.promise; + } + cancelNotifs(); + logger.info("done waiting until given transactions are in a final state"); +} + +/** * Wait until pending work is processed. */ export async function waitUntilTasksProcessed( @@ -571,12 +679,29 @@ export async function waitTransactionState( cancelNotifs(); } +export async function waitUntilTransactionWithAssociatedRefreshesFinal( + ws: InternalWalletState, + transactionId: string, +): Promise<void> { + await waitUntilGivenTransactionsFinal(ws, [transactionId]); + await waitUntilGivenTransactionsFinal( + ws, + await getRefreshesForTransaction(ws, transactionId), + ); +} + +export async function waitUntilTransactionFinal( + ws: InternalWalletState, + transactionId: string, +): Promise<void> { + await waitUntilGivenTransactionsFinal(ws, [transactionId]); +} + export async function runIntegrationTest2( ws: InternalWalletState, args: IntegrationTestV2Args, ): Promise<void> { - // FIXME: Make sure that a task look is running, since we're - // waiting for notifications. + ws.ensureTaskLoopRunning(); logger.info("running test with arguments", args); const exchangeInfo = await fetchFreshExchange(ws, args.exchangeBaseUrl); @@ -587,12 +712,12 @@ export async function runIntegrationTest2( const amountToSpend = Amounts.parseOrThrow(`${currency}:2`); logger.info("withdrawing test balance"); - await withdrawTestBalance(ws, { + const withdrawalRes = await withdrawTestBalance(ws, { amount: Amounts.stringify(amountToWithdraw), corebankApiBaseUrl: args.corebankApiBaseUrl, exchangeBaseUrl: args.exchangeBaseUrl, }); - await waitUntilTransactionsFinal(ws); + await waitUntilTransactionFinal(ws, withdrawalRes.transactionId); logger.info("done withdrawing test balance"); const balance = await getBalances(ws); @@ -604,15 +729,17 @@ export async function runIntegrationTest2( authToken: args.merchantAuthToken, }; - await makePayment( + const makePaymentRes = await makePayment( ws, myMerchant, Amounts.stringify(amountToSpend), "hello world", ); - // Wait until the refresh is done - await waitUntilTransactionsFinal(ws); + await waitUntilTransactionWithAssociatedRefreshesFinal( + ws, + makePaymentRes.paymentTransactionId, + ); logger.trace("withdrawing test balance for refund"); const withdrawAmountTwo = Amounts.parseOrThrow(`${currency}:18`); @@ -620,14 +747,14 @@ export async function runIntegrationTest2( const refundAmount = Amounts.parseOrThrow(`${currency}:6`); const spendAmountThree = Amounts.parseOrThrow(`${currency}:3`); - await withdrawTestBalance(ws, { + const withdrawalRes2 = await withdrawTestBalance(ws, { amount: Amounts.stringify(withdrawAmountTwo), corebankApiBaseUrl: args.corebankApiBaseUrl, exchangeBaseUrl: args.exchangeBaseUrl, }); // Wait until the withdraw is done - await waitUntilTransactionsFinal(ws); + await waitUntilTransactionFinal(ws, withdrawalRes2.transactionId); const { orderId: refundOrderId } = await makePayment( ws, @@ -646,25 +773,31 @@ export async function runIntegrationTest2( logger.trace("refund URI", refundUri); - await startRefundQueryForUri(ws, refundUri); + const refundResp = await startRefundQueryForUri(ws, refundUri); logger.trace("integration test: applied refund"); // Wait until the refund is done - await waitUntilTransactionsFinal(ws); + await waitUntilTransactionWithAssociatedRefreshesFinal( + ws, + refundResp.transactionId, + ); logger.trace("integration test: making payment after refund"); - await makePayment( + const makePaymentRes2 = await makePayment( ws, myMerchant, Amounts.stringify(spendAmountThree), "payment after refund", ); - logger.trace("integration test: make payment done"); + await waitUntilTransactionWithAssociatedRefreshesFinal( + ws, + makePaymentRes2.paymentTransactionId, + ); - await waitUntilTransactionsFinal(ws); + logger.trace("integration test: make payment done"); const peerPushInit = await initiatePeerPushDebit(ws, { partialContractTerms: { @@ -680,12 +813,6 @@ export async function runIntegrationTest2( }); await waitUntilTransactionPendingReady(ws, peerPushInit.transactionId); - const talerUri = stringifyTalerUri({ - type: TalerUriAction.PayPush, - exchangeBaseUrl: peerPushInit.exchangeBaseUrl, - contractPriv: peerPushInit.contractPriv, - }); - const txDetails = await getTransactionById(ws, { transactionId: peerPushInit.transactionId, }); @@ -729,7 +856,25 @@ export async function runIntegrationTest2( peerPullDebitId: peerPullInc.peerPullDebitId, }); - await waitUntilTransactionsFinal(ws); + await waitUntilTransactionWithAssociatedRefreshesFinal( + ws, + peerPullInc.transactionId, + ); + + await waitUntilTransactionWithAssociatedRefreshesFinal( + ws, + peerPullInit.transactionId, + ); + + await waitUntilTransactionWithAssociatedRefreshesFinal( + ws, + peerPushCredit.transactionId, + ); + + await waitUntilTransactionWithAssociatedRefreshesFinal( + ws, + peerPushInit.transactionId, + ); logger.trace("integration test: all done!"); } @@ -764,7 +909,7 @@ export async function testPay( } const r = await confirmPay( ws, - result.proposalId, + result.transactionId, undefined, args.forcedCoinSel, ); diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index 89f756ae9..8fd7afae6 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -192,6 +192,7 @@ const txOrder: { [t in TransactionType]: number } = { [TransactionType.Refund]: 8, [TransactionType.Deposit]: 9, [TransactionType.Refresh]: 10, + [TransactionType.Recoup]: 11, [TransactionType.InternalWithdrawal]: 12, }; @@ -248,6 +249,9 @@ export async function getTransactionById( }); } + case TransactionType.Recoup: + throw new Error("not yet supported"); + case TransactionType.Payment: { const proposalId = parsedTx.proposalId; return await ws.db @@ -791,8 +795,7 @@ function buildTransactionForRefresh( ), refreshInputAmount: Amounts.stringify(inputAmount), refreshOutputAmount: Amounts.stringify(outputAmount), - originatingTransactionId: - refreshGroupRecord.reasonDetails?.originatingTransactionId, + originatingTransactionId: refreshGroupRecord.originatingTransactionId, timestamp: timestampPreciseFromDb(refreshGroupRecord.timestampCreated), transactionId: constructTransactionIdentifier({ tag: TransactionType.Refresh, @@ -1391,7 +1394,8 @@ export type ParsedTransactionIdentifier = | { tag: TransactionType.Refund; refundGroupId: string } | { tag: TransactionType.Reward; walletRewardId: string } | { tag: TransactionType.Withdrawal; withdrawalGroupId: string } - | { tag: TransactionType.InternalWithdrawal; withdrawalGroupId: string }; + | { tag: TransactionType.InternalWithdrawal; withdrawalGroupId: string } + | { tag: TransactionType.Recoup; recoupGroupId: string }; export function constructTransactionIdentifier( pTxId: ParsedTransactionIdentifier, @@ -1419,6 +1423,8 @@ export function constructTransactionIdentifier( return `txn:${pTxId.tag}:${pTxId.withdrawalGroupId}` as TransactionIdStr; case TransactionType.InternalWithdrawal: return `txn:${pTxId.tag}:${pTxId.withdrawalGroupId}` as TransactionIdStr; + case TransactionType.Recoup: + return `txn:${pTxId.tag}:${pTxId.recoupGroupId}` as TransactionIdStr; default: assertUnreachable(pTxId); } @@ -1597,6 +1603,9 @@ export async function retryTransaction( case TransactionType.Refund: // Nothing to do for a refund transaction. break; + case TransactionType.Recoup: + // FIXME! + throw Error("not implemented"); default: assertUnreachable(parsedTx); } @@ -1632,6 +1641,9 @@ async function getContextForTransaction( return new RefundTransactionContext(ws, tx.refundGroupId); case TransactionType.Reward: return new RewardTransactionContext(ws, tx.walletRewardId); + case TransactionType.Recoup: + throw new Error("not yet supported"); + //return new RecoupTransactionContext(ws, tx.recoupGroupId); default: assertUnreachable(tx); } diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts index cc67781ae..38b944475 100644 --- a/packages/taler-wallet-core/src/wallet-api-types.ts +++ b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -33,6 +33,8 @@ import { AcceptTipResponse, AcceptWithdrawalResponse, AddExchangeRequest, + AddGlobalCurrencyAuditorRequest, + AddGlobalCurrencyExchangeRequest, AddKnownBankAccountsRequest, AmountResponse, ApplyDevExperimentRequest, @@ -51,6 +53,7 @@ import { CreateDepositGroupRequest, CreateDepositGroupResponse, CreateStoredBackupResponse, + DeleteExchangeRequest, DeleteStoredBackupRequest, DeleteTransactionRequest, ExchangeDetailedResponse, @@ -66,12 +69,15 @@ import { GetCurrencySpecificationResponse, GetExchangeEntryByUrlRequest, GetExchangeEntryByUrlResponse, + GetExchangeResourcesRequest, + GetExchangeResourcesResponse, GetExchangeTosRequest, GetExchangeTosResult, GetPlanForOperationRequest, GetPlanForOperationResponse, GetWithdrawalDetailsForAmountRequest, GetWithdrawalDetailsForUriRequest, + ImportDbRequest, InitRequest, InitResponse, InitiatePeerPullCreditRequest, @@ -80,9 +86,12 @@ import { InitiatePeerPushDebitResponse, IntegrationTestArgs, KnownBankAccounts, + ListAssociatedRefreshesRequest, + ListAssociatedRefreshesResponse, ListExchangesForScopedCurrencyRequest, + ListGlobalCurrencyAuditorsResponse, + ListGlobalCurrencyExchangesResponse, ListKnownBankAccountsRequest, - WithdrawalDetailsForAmount, PrepareDepositRequest, PrepareDepositResponse, PreparePayRequest, @@ -99,6 +108,8 @@ import { PrepareWithdrawExchangeResponse, RecoverStoredBackupRequest, RecoveryLoadRequest, + RemoveGlobalCurrencyAuditorRequest, + RemoveGlobalCurrencyExchangeRequest, RetryTransactionRequest, SetCoinSuspendedRequest, SetWalletDeviceIdRequest, @@ -113,6 +124,7 @@ import { TestingWaitTransactionRequest, Transaction, TransactionByIdRequest, + TransactionWithdrawal, TransactionsRequest, TransactionsResponse, TxIdResponse, @@ -125,21 +137,10 @@ import { ValidateIbanResponse, WalletContractData, WalletCoreVersion, - WalletCurrencyInfo, WithdrawTestBalanceRequest, WithdrawUriInfoResponse, + WithdrawalDetailsForAmount, WithdrawalTransactionByURIRequest, - TransactionWithdrawal, - GetExchangeResourcesRequest, - DeleteExchangeRequest, - GetExchangeResourcesResponse, - ListGlobalCurrencyExchangesResponse, - ListGlobalCurrencyAuditorsResponse, - AddGlobalCurrencyExchangeRequest, - AddGlobalCurrencyAuditorRequest, - RemoveGlobalCurrencyExchangeRequest, - RemoveGlobalCurrencyAuditorRequest, - ImportDbRequest, } from "@gnu-taler/taler-util"; import { AddBackupProviderRequest, @@ -255,6 +256,7 @@ export enum WalletApiOperation { RemoveGlobalCurrencyExchange = "removeGlobalCurrencyExchange", AddGlobalCurrencyAuditor = "addGlobalCurrencyAuditor", RemoveGlobalCurrencyAuditor = "removeGlobalCurrencyAuditor", + ListAssociatedRefreshes = "listAssociatedRefreshes", } // group: Initialization @@ -383,6 +385,15 @@ export type GetTransactionsOp = { }; /** + * List refresh transactions associated with another transaction. + */ +export type ListAssociatedRefreshesOp = { + op: WalletApiOperation.ListAssociatedRefreshes; + request: ListAssociatedRefreshesRequest; + response: ListAssociatedRefreshesResponse; +}; + +/** * Get sample transactions. */ export type TestingGetSampleTransactionsOp = { @@ -1277,6 +1288,7 @@ export type WalletOperations = { [WalletApiOperation.RemoveGlobalCurrencyAuditor]: RemoveGlobalCurrencyAuditorOp; [WalletApiOperation.AddGlobalCurrencyExchange]: AddGlobalCurrencyExchangeOp; [WalletApiOperation.RemoveGlobalCurrencyExchange]: RemoveGlobalCurrencyExchangeOp; + [WalletApiOperation.ListAssociatedRefreshes]: ListAssociatedRefreshesOp; }; export type WalletCoreRequestType< diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index ea64ff1aa..005fac3c4 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -46,7 +46,6 @@ import { PrepareWithdrawExchangeRequest, PrepareWithdrawExchangeResponse, RecoverStoredBackupRequest, - ScopeType, StoredBackupList, TalerError, TalerErrorCode, @@ -161,7 +160,6 @@ import { MerchantInfo, NotificationListener, RecoupOperations, - RefreshOperations, } from "./internal-wallet-state.js"; import { getUserAttentions, @@ -262,13 +260,14 @@ import { runIntegrationTest2, testPay, waitTransactionState, + waitUntilAllTransactionsFinal, waitUntilRefreshesDone, waitUntilTasksProcessed, - waitUntilTransactionsFinal, withdrawTestBalance, } from "./operations/testing.js"; import { abortTransaction, + constructTransactionIdentifier, deleteTransaction, failTransaction, getTransactionById, @@ -735,9 +734,9 @@ async function dumpCoins(ws: InternalWalletState): Promise<CoinDumpJson> { ageCommitmentProof: c.ageCommitmentProof, spend_allocation: c.spendAllocation ? { - amount: c.spendAllocation.amount, - id: c.spendAllocation.id, - } + amount: c.spendAllocation.amount, + id: c.spendAllocation.id, + } : undefined, }); } @@ -1125,20 +1124,19 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>( } case WalletApiOperation.ConfirmPay: { const req = codecForConfirmPayRequest().decode(payload); - let proposalId; + let transactionId; if (req.proposalId) { // legacy client support - proposalId = req.proposalId; + transactionId = constructTransactionIdentifier({ + tag: TransactionType.Payment, + proposalId: req.proposalId, + }); } else if (req.transactionId) { - const txIdParsed = parseTransactionIdentifier(req.transactionId); - if (txIdParsed?.tag != TransactionType.Payment) { - throw Error("payment transaction ID required"); - } - proposalId = txIdParsed.proposalId; + transactionId = req.transactionId; } else { throw Error("transactionId or (deprecated) proposalId required"); } - return await confirmPay(ws, proposalId, req.sessionId); + return await confirmPay(ws, transactionId, req.sessionId); } case WalletApiOperation.AbortTransaction: { const req = codecForAbortTransaction().decode(payload); @@ -1491,7 +1489,7 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>( return getVersion(ws); } case WalletApiOperation.TestingWaitTransactionsFinal: - return await waitUntilTransactionsFinal(ws); + return await waitUntilAllTransactionsFinal(ws); case WalletApiOperation.TestingWaitRefreshesFinal: return await waitUntilRefreshesDone(ws); case WalletApiOperation.TestingSetTimetravel: { @@ -1727,10 +1725,6 @@ class InternalWalletStateImpl implements InternalWalletState { createRecoupGroup, }; - refreshOps: RefreshOperations = { - createRefreshGroup, - }; - // FIXME: Use an LRU cache here. private denomCache: Record<string, DenominationInfo> = {}; @@ -1864,6 +1858,8 @@ class InternalWalletStateImpl implements InternalWalletState { } return computeRewardTransactionStatus(rec); } + case TransactionType.Recoup: + throw Error("not yet supported"); default: assertUnreachable(parsedTxId); } |