aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2024-01-29 21:29:26 +0100
committerFlorian Dold <florian@dold.me>2024-01-29 21:29:35 +0100
commit61e3484d6ce1db846c435d84cbf5b9c3711a30f2 (patch)
tree841e9889c5814c458622904203a46853aa0e2708
parent57d988cf4ceffb51e8936bd36eed4aefcdc2d2dc (diff)
wallet-core,harness: let runIntegrationTest wait for its own transactions
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallettesting.ts10
-rw-r--r--packages/taler-util/src/transactions-types.ts25
-rw-r--r--packages/taler-util/src/wallet-types.ts13
-rw-r--r--packages/taler-wallet-core/src/db.ts22
-rw-r--r--packages/taler-wallet-core/src/internal-wallet-state.ts28
-rw-r--r--packages/taler-wallet-core/src/operations/common.ts7
-rw-r--r--packages/taler-wallet-core/src/operations/deposits.ts4
-rw-r--r--packages/taler-wallet-core/src/operations/pay-merchant.ts22
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts1
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts2
-rw-r--r--packages/taler-wallet-core/src/operations/recoup.ts49
-rw-r--r--packages/taler-wallet-core/src/operations/refresh.ts30
-rw-r--r--packages/taler-wallet-core/src/operations/testing.ts227
-rw-r--r--packages/taler-wallet-core/src/operations/transactions.ts18
-rw-r--r--packages/taler-wallet-core/src/wallet-api-types.ts38
-rw-r--r--packages/taler-wallet-core/src/wallet.ts34
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);
}