summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/testing.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-core/src/testing.ts')
-rw-r--r--packages/taler-wallet-core/src/testing.ts871
1 files changed, 871 insertions, 0 deletions
diff --git a/packages/taler-wallet-core/src/testing.ts b/packages/taler-wallet-core/src/testing.ts
new file mode 100644
index 000000000..899c4a8b2
--- /dev/null
+++ b/packages/taler-wallet-core/src/testing.ts
@@ -0,0 +1,871 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * @file
+ * Implementation of wallet-core operations that are used for testing,
+ * but typically not in the production wallet.
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AbsoluteTime,
+ addPaytoQueryParams,
+ Amounts,
+ AmountString,
+ checkLogicInvariant,
+ CheckPaymentResponse,
+ codecForAny,
+ codecForCheckPaymentResponse,
+ ConfirmPayResultType,
+ Duration,
+ IntegrationTestArgs,
+ IntegrationTestV2Args,
+ j2s,
+ Logger,
+ NotificationType,
+ parsePaytoUri,
+ PreparePayResultType,
+ TalerCorebankApiClient,
+ TestPayArgs,
+ TestPayResult,
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionState,
+ TransactionType,
+ URL,
+ WithdrawTestBalanceRequest,
+} from "@gnu-taler/taler-util";
+import {
+ HttpRequestLibrary,
+ readSuccessResponseJsonOrThrow,
+} from "@gnu-taler/taler-util/http";
+import { getBalances } from "./balance.js";
+import { genericWaitForState } from "./common.js";
+import { createDepositGroup } from "./deposits.js";
+import { fetchFreshExchange } from "./exchanges.js";
+import {
+ confirmPay,
+ preparePayForUri,
+ startRefundQueryForUri,
+} from "./pay-merchant.js";
+import { initiatePeerPullPayment } from "./pay-peer-pull-credit.js";
+import {
+ confirmPeerPullDebit,
+ preparePeerPullDebit,
+} from "./pay-peer-pull-debit.js";
+import {
+ confirmPeerPushCredit,
+ preparePeerPushCredit,
+} from "./pay-peer-push-credit.js";
+import { initiatePeerPushDebit } from "./pay-peer-push-debit.js";
+import { getRefreshesForTransaction } from "./refresh.js";
+import { getTransactionById, getTransactions } from "./transactions.js";
+import type { WalletExecutionContext } from "./wallet.js";
+import { acceptWithdrawalFromUri } from "./withdraw.js";
+
+const logger = new Logger("operations/testing.ts");
+
+interface MerchantBackendInfo {
+ baseUrl: string;
+ authToken?: string;
+}
+
+export interface WithdrawTestBalanceResult {
+ /**
+ * Transaction ID of the newly created withdrawal transaction.
+ */
+ transactionId: string;
+
+ /**
+ * Account of the user registered for the withdrawal.
+ */
+ accountPaytoUri: string;
+}
+
+export async function withdrawTestBalance(
+ wex: WalletExecutionContext,
+ req: WithdrawTestBalanceRequest,
+): Promise<WithdrawTestBalanceResult> {
+ const amount = req.amount;
+ const exchangeBaseUrl = req.exchangeBaseUrl;
+ const corebankApiBaseUrl = req.corebankApiBaseUrl;
+
+ logger.trace(
+ `Registering bank user, bank access base url ${corebankApiBaseUrl}`,
+ );
+
+ const corebankClient = new TalerCorebankApiClient(corebankApiBaseUrl);
+
+ const bankUser = await corebankClient.createRandomBankUser();
+ logger.trace(`Registered bank user ${JSON.stringify(bankUser)}`);
+
+ corebankClient.setAuth(bankUser);
+
+ const wresp = await corebankClient.createWithdrawalOperation(
+ bankUser.username,
+ amount,
+ );
+
+ const acceptResp = await acceptWithdrawalFromUri(wex, {
+ talerWithdrawUri: wresp.taler_withdraw_uri,
+ selectedExchange: exchangeBaseUrl,
+ forcedDenomSel: req.forcedDenomSel,
+ });
+
+ await corebankClient.confirmWithdrawalOperation(bankUser.username, {
+ withdrawalOperationId: wresp.withdrawal_id,
+ });
+
+ return {
+ transactionId: acceptResp.transactionId,
+ accountPaytoUri: bankUser.accountPaytoUri,
+ };
+}
+
+/**
+ * FIXME: User MerchantApiClient instead.
+ */
+function getMerchantAuthHeader(m: MerchantBackendInfo): Record<string, string> {
+ if (m.authToken) {
+ return {
+ Authorization: `Bearer ${m.authToken}`,
+ };
+ }
+ return {};
+}
+
+/**
+ * FIXME: User MerchantApiClient instead.
+ */
+async function refund(
+ http: HttpRequestLibrary,
+ merchantBackend: MerchantBackendInfo,
+ orderId: string,
+ reason: string,
+ refundAmount: string,
+): Promise<string> {
+ const reqUrl = new URL(
+ `private/orders/${orderId}/refund`,
+ merchantBackend.baseUrl,
+ );
+ const refundReq = {
+ order_id: orderId,
+ reason,
+ refund: refundAmount,
+ };
+ const resp = await http.fetch(reqUrl.href, {
+ method: "POST",
+ body: refundReq,
+ headers: getMerchantAuthHeader(merchantBackend),
+ });
+ const r = await readSuccessResponseJsonOrThrow(resp, codecForAny());
+ const refundUri = r.taler_refund_uri;
+ if (!refundUri) {
+ throw Error("no refund URI in response");
+ }
+ return refundUri;
+}
+
+/**
+ * FIXME: User MerchantApiClient instead.
+ */
+async function createOrder(
+ http: HttpRequestLibrary,
+ merchantBackend: MerchantBackendInfo,
+ amount: string,
+ summary: string,
+ fulfillmentUrl: string,
+): Promise<{ orderId: string }> {
+ const t = Math.floor(new Date().getTime() / 1000) + 15 * 60;
+ const reqUrl = new URL("private/orders", merchantBackend.baseUrl).href;
+ const orderReq = {
+ order: {
+ amount,
+ summary,
+ fulfillment_url: fulfillmentUrl,
+ refund_deadline: { t_s: t },
+ wire_transfer_deadline: { t_s: t },
+ },
+ };
+ const resp = await http.fetch(reqUrl, {
+ method: "POST",
+ body: orderReq,
+ headers: getMerchantAuthHeader(merchantBackend),
+ });
+ const r = await readSuccessResponseJsonOrThrow(resp, codecForAny());
+ const orderId = r.order_id;
+ if (!orderId) {
+ throw Error("no order id in response");
+ }
+ return { orderId };
+}
+
+/**
+ * FIXME: User MerchantApiClient instead.
+ */
+async function checkPayment(
+ http: HttpRequestLibrary,
+ merchantBackend: MerchantBackendInfo,
+ orderId: string,
+): Promise<CheckPaymentResponse> {
+ const reqUrl = new URL(`private/orders/${orderId}`, merchantBackend.baseUrl);
+ reqUrl.searchParams.set("order_id", orderId);
+ const resp = await http.fetch(reqUrl.href, {
+ headers: getMerchantAuthHeader(merchantBackend),
+ });
+ return readSuccessResponseJsonOrThrow(resp, codecForCheckPaymentResponse());
+}
+
+interface MakePaymentResult {
+ orderId: string;
+ paymentTransactionId: string;
+}
+
+async function makePayment(
+ wex: WalletExecutionContext,
+ merchant: MerchantBackendInfo,
+ amount: string,
+ summary: string,
+): Promise<MakePaymentResult> {
+ const orderResp = await createOrder(
+ wex.http,
+ merchant,
+ amount,
+ summary,
+ "taler://fulfillment-success/thx",
+ );
+
+ logger.trace("created order with orderId", orderResp.orderId);
+
+ let paymentStatus = await checkPayment(wex.http, merchant, orderResp.orderId);
+
+ logger.trace("payment status", paymentStatus);
+
+ const talerPayUri = paymentStatus.taler_pay_uri;
+ if (!talerPayUri) {
+ throw Error("no taler://pay/ URI in payment response");
+ }
+
+ const preparePayResult = await preparePayForUri(wex, talerPayUri);
+
+ logger.trace("prepare pay result", preparePayResult);
+
+ if (preparePayResult.status != "payment-possible") {
+ throw Error("payment not possible");
+ }
+
+ const confirmPayResult = await confirmPay(
+ wex,
+ preparePayResult.transactionId,
+ undefined,
+ );
+
+ logger.trace("confirmPayResult", confirmPayResult);
+
+ paymentStatus = await checkPayment(wex.http, merchant, orderResp.orderId);
+
+ logger.trace("payment status after wallet payment:", paymentStatus);
+
+ if (paymentStatus.order_status !== "paid") {
+ throw Error("payment did not succeed");
+ }
+
+ return {
+ orderId: orderResp.orderId,
+ paymentTransactionId: preparePayResult.transactionId,
+ };
+}
+
+export async function runIntegrationTest(
+ wex: WalletExecutionContext,
+ args: IntegrationTestArgs,
+): Promise<void> {
+ logger.info("running test with arguments", args);
+
+ const parsedSpendAmount = Amounts.parseOrThrow(args.amountToSpend);
+ const currency = parsedSpendAmount.currency;
+
+ logger.info("withdrawing test balance");
+ const withdrawRes1 = await withdrawTestBalance(wex, {
+ amount: args.amountToWithdraw,
+ corebankApiBaseUrl: args.corebankApiBaseUrl,
+ exchangeBaseUrl: args.exchangeBaseUrl,
+ });
+ await waitUntilGivenTransactionsFinal(wex, [withdrawRes1.transactionId]);
+ logger.info("done withdrawing test balance");
+
+ const balance = await getBalances(wex);
+
+ logger.trace(JSON.stringify(balance, null, 2));
+
+ const myMerchant: MerchantBackendInfo = {
+ baseUrl: args.merchantBaseUrl,
+ authToken: args.merchantAuthToken,
+ };
+
+ const makePaymentRes = await makePayment(
+ wex,
+ myMerchant,
+ args.amountToSpend,
+ "hello world",
+ );
+
+ await waitUntilTransactionWithAssociatedRefreshesFinal(
+ wex,
+ makePaymentRes.paymentTransactionId,
+ );
+
+ logger.trace("withdrawing test balance for refund");
+ const withdrawAmountTwo = Amounts.parseOrThrow(`${currency}:18`);
+ const spendAmountTwo = Amounts.parseOrThrow(`${currency}:7`);
+ const refundAmount = Amounts.parseOrThrow(`${currency}:6`);
+ const spendAmountThree = Amounts.parseOrThrow(`${currency}:3`);
+
+ const withdrawRes2 = await withdrawTestBalance(wex, {
+ amount: Amounts.stringify(withdrawAmountTwo),
+ corebankApiBaseUrl: args.corebankApiBaseUrl,
+ exchangeBaseUrl: args.exchangeBaseUrl,
+ });
+
+ await waitUntilGivenTransactionsFinal(wex, [withdrawRes2.transactionId]);
+
+ const { orderId: refundOrderId } = await makePayment(
+ wex,
+ myMerchant,
+ Amounts.stringify(spendAmountTwo),
+ "order that will be refunded",
+ );
+
+ const refundUri = await refund(
+ wex.http,
+ myMerchant,
+ refundOrderId,
+ "test refund",
+ Amounts.stringify(refundAmount),
+ );
+
+ logger.trace("refund URI", refundUri);
+
+ const refundResp = await startRefundQueryForUri(wex, refundUri);
+
+ logger.trace("integration test: applied refund");
+
+ // Wait until the refund is done
+ await waitUntilTransactionWithAssociatedRefreshesFinal(
+ wex,
+ refundResp.transactionId,
+ );
+
+ logger.trace("integration test: making payment after refund");
+
+ const paymentResp2 = await makePayment(
+ wex,
+ myMerchant,
+ Amounts.stringify(spendAmountThree),
+ "payment after refund",
+ );
+
+ logger.trace("integration test: make payment done");
+
+ await waitUntilGivenTransactionsFinal(wex, [
+ paymentResp2.paymentTransactionId,
+ ]);
+ await waitUntilGivenTransactionsFinal(
+ wex,
+ await getRefreshesForTransaction(wex, paymentResp2.paymentTransactionId),
+ );
+
+ logger.trace("integration test: all done!");
+}
+
+/**
+ * Wait until all transactions are in a final state.
+ */
+export async function waitUntilAllTransactionsFinal(
+ wex: WalletExecutionContext,
+): Promise<void> {
+ logger.info("waiting until all transactions are in a final state");
+ await wex.taskScheduler.ensureRunning();
+ await genericWaitForState(wex, {
+ filterNotification(notif) {
+ if (notif.type !== NotificationType.TransactionStateTransition) {
+ return false;
+ }
+ switch (notif.newTxState.major) {
+ case TransactionMajorState.Pending:
+ case TransactionMajorState.Aborting:
+ return false;
+ default:
+ return true;
+ }
+ },
+ async checkState() {
+ const txs = await getTransactions(wex, {
+ includeRefreshes: true,
+ filterByState: "nonfinal",
+ });
+ for (const tx of txs.transactions) {
+ switch (tx.txState.major) {
+ case TransactionMajorState.Pending:
+ case TransactionMajorState.Aborting:
+ case TransactionMajorState.Suspended:
+ case TransactionMajorState.SuspendedAborting:
+ logger.info(
+ `continuing waiting, ${tx.transactionId} in ${tx.txState.major}(${tx.txState.minor})`,
+ );
+ return false;
+ }
+ }
+ return true;
+ },
+ });
+ logger.info("done waiting until all transactions are in a final state");
+}
+
+export async function waitTasksDone(
+ wex: WalletExecutionContext,
+): Promise<void> {
+ await genericWaitForState(wex, {
+ async checkState() {
+ return wex.taskScheduler.isIdle();
+ },
+ filterNotification(notif) {
+ return notif.type === NotificationType.Idle;
+ },
+ });
+}
+
+/**
+ * Wait until all chosen transactions are in a final state.
+ */
+export async function waitUntilGivenTransactionsFinal(
+ wex: WalletExecutionContext,
+ 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;
+ }
+
+ const txIdSet = new Set(transactionIds);
+
+ await genericWaitForState(wex, {
+ filterNotification(notif) {
+ if (notif.type !== NotificationType.TransactionStateTransition) {
+ return false;
+ }
+ if (!txIdSet.has(notif.transactionId)) {
+ return false;
+ }
+ switch (notif.newTxState.major) {
+ case TransactionMajorState.Pending:
+ case TransactionMajorState.Aborting:
+ case TransactionMajorState.Suspended:
+ case TransactionMajorState.SuspendedAborting:
+ return false;
+ }
+ return true;
+ },
+ async checkState() {
+ const txs = await getTransactions(wex, {
+ includeRefreshes: true,
+ filterByState: "nonfinal",
+ });
+ 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:
+ logger.info(
+ `continuing waiting, ${tx.transactionId} in ${tx.txState.major}(${tx.txState.minor})`,
+ );
+ return false;
+ }
+ }
+ // No transaction is pending, we're done waiting!
+ return true;
+ },
+ });
+ logger.info("done waiting until given transactions are in a final state");
+}
+
+export async function waitUntilRefreshesDone(
+ wex: WalletExecutionContext,
+): Promise<void> {
+ logger.info("waiting until all refresh transactions are in a final state");
+
+ await genericWaitForState(wex, {
+ filterNotification(notif) {
+ if (notif.type !== NotificationType.TransactionStateTransition) {
+ return false;
+ }
+ switch (notif.newTxState.major) {
+ case TransactionMajorState.Pending:
+ case TransactionMajorState.Aborting:
+ return false;
+ default:
+ return true;
+ }
+ },
+ async checkState() {
+ const txs = await getTransactions(wex, {
+ includeRefreshes: true,
+ filterByState: "nonfinal",
+ });
+ for (const tx of txs.transactions) {
+ if (tx.type !== TransactionType.Refresh) {
+ continue;
+ }
+ switch (tx.txState.major) {
+ case TransactionMajorState.Pending:
+ case TransactionMajorState.Aborting:
+ case TransactionMajorState.Suspended:
+ case TransactionMajorState.SuspendedAborting:
+ logger.info(
+ `continuing waiting, ${tx.transactionId} in ${tx.txState.major}(${tx.txState.minor})`,
+ );
+ return false;
+ }
+ }
+ return true;
+ },
+ });
+ logger.info("done waiting until all refreshes are in a final state");
+}
+
+async function waitUntilTransactionPendingReady(
+ wex: WalletExecutionContext,
+ transactionId: string,
+): Promise<void> {
+ return await waitTransactionState(wex, transactionId, {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.Ready,
+ });
+}
+
+/**
+ * Wait until a transaction is in a particular state.
+ */
+export async function waitTransactionState(
+ wex: WalletExecutionContext,
+ transactionId: string,
+ txState: TransactionState,
+): Promise<void> {
+ logger.info(
+ `starting waiting for ${transactionId} to be in ${JSON.stringify(
+ txState,
+ )})`,
+ );
+ await genericWaitForState(wex, {
+ async checkState() {
+ const tx = await getTransactionById(wex, {
+ transactionId,
+ });
+ return (
+ tx.txState.major === txState.major && tx.txState.minor === txState.minor
+ );
+ },
+ filterNotification(notif) {
+ return notif.type === NotificationType.TransactionStateTransition;
+ },
+ });
+ logger.info(
+ `done waiting for ${transactionId} to be in ${JSON.stringify(txState)}`,
+ );
+}
+
+export async function waitUntilTransactionWithAssociatedRefreshesFinal(
+ wex: WalletExecutionContext,
+ transactionId: string,
+): Promise<void> {
+ await waitUntilGivenTransactionsFinal(wex, [transactionId]);
+ await waitUntilGivenTransactionsFinal(
+ wex,
+ await getRefreshesForTransaction(wex, transactionId),
+ );
+}
+
+export async function waitUntilTransactionFinal(
+ wex: WalletExecutionContext,
+ transactionId: string,
+): Promise<void> {
+ await waitUntilGivenTransactionsFinal(wex, [transactionId]);
+}
+
+export async function runIntegrationTest2(
+ wex: WalletExecutionContext,
+ args: IntegrationTestV2Args,
+): Promise<void> {
+ await wex.taskScheduler.ensureRunning();
+ logger.info("running test with arguments", args);
+
+ const exchangeInfo = await fetchFreshExchange(wex, args.exchangeBaseUrl);
+
+ const currency = exchangeInfo.currency;
+
+ const amountToWithdraw = Amounts.parseOrThrow(`${currency}:10`);
+ const amountToSpend = Amounts.parseOrThrow(`${currency}:2`);
+
+ logger.info("withdrawing test balance");
+ const withdrawalRes = await withdrawTestBalance(wex, {
+ amount: Amounts.stringify(amountToWithdraw),
+ corebankApiBaseUrl: args.corebankApiBaseUrl,
+ exchangeBaseUrl: args.exchangeBaseUrl,
+ });
+ await waitUntilTransactionFinal(wex, withdrawalRes.transactionId);
+ logger.info("done withdrawing test balance");
+
+ const balance = await getBalances(wex);
+
+ logger.trace(JSON.stringify(balance, null, 2));
+
+ const myMerchant: MerchantBackendInfo = {
+ baseUrl: args.merchantBaseUrl,
+ authToken: args.merchantAuthToken,
+ };
+
+ const makePaymentRes = await makePayment(
+ wex,
+ myMerchant,
+ Amounts.stringify(amountToSpend),
+ "hello world",
+ );
+
+ await waitUntilTransactionWithAssociatedRefreshesFinal(
+ wex,
+ makePaymentRes.paymentTransactionId,
+ );
+
+ logger.trace("withdrawing test balance for refund");
+ const withdrawAmountTwo = Amounts.parseOrThrow(`${currency}:18`);
+ const spendAmountTwo = Amounts.parseOrThrow(`${currency}:7`);
+ const refundAmount = Amounts.parseOrThrow(`${currency}:6`);
+ const spendAmountThree = Amounts.parseOrThrow(`${currency}:3`);
+
+ const withdrawalRes2 = await withdrawTestBalance(wex, {
+ amount: Amounts.stringify(withdrawAmountTwo),
+ corebankApiBaseUrl: args.corebankApiBaseUrl,
+ exchangeBaseUrl: args.exchangeBaseUrl,
+ });
+
+ // Wait until the withdraw is done
+ await waitUntilTransactionFinal(wex, withdrawalRes2.transactionId);
+
+ const { orderId: refundOrderId } = await makePayment(
+ wex,
+ myMerchant,
+ Amounts.stringify(spendAmountTwo),
+ "order that will be refunded",
+ );
+
+ const refundUri = await refund(
+ wex.http,
+ myMerchant,
+ refundOrderId,
+ "test refund",
+ Amounts.stringify(refundAmount),
+ );
+
+ logger.trace("refund URI", refundUri);
+
+ const refundResp = await startRefundQueryForUri(wex, refundUri);
+
+ logger.trace("integration test: applied refund");
+
+ // Wait until the refund is done
+ await waitUntilTransactionWithAssociatedRefreshesFinal(
+ wex,
+ refundResp.transactionId,
+ );
+
+ logger.trace("integration test: making payment after refund");
+
+ const makePaymentRes2 = await makePayment(
+ wex,
+ myMerchant,
+ Amounts.stringify(spendAmountThree),
+ "payment after refund",
+ );
+
+ await waitUntilTransactionWithAssociatedRefreshesFinal(
+ wex,
+ makePaymentRes2.paymentTransactionId,
+ );
+
+ logger.trace("integration test: make payment done");
+
+ const peerPushInit = await initiatePeerPushDebit(wex, {
+ partialContractTerms: {
+ amount: `${currency}:1` as AmountString,
+ summary: "Payment Peer Push Test",
+ purse_expiration: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ hours: 1 }),
+ ),
+ ),
+ },
+ });
+
+ await waitUntilTransactionPendingReady(wex, peerPushInit.transactionId);
+ const txDetails = await getTransactionById(wex, {
+ transactionId: peerPushInit.transactionId,
+ });
+
+ if (txDetails.type !== TransactionType.PeerPushDebit) {
+ throw Error("internal invariant failed");
+ }
+
+ if (!txDetails.talerUri) {
+ throw Error("internal invariant failed");
+ }
+
+ const peerPushCredit = await preparePeerPushCredit(wex, {
+ talerUri: txDetails.talerUri,
+ });
+
+ await confirmPeerPushCredit(wex, {
+ transactionId: peerPushCredit.transactionId,
+ });
+
+ const peerPullInit = await initiatePeerPullPayment(wex, {
+ partialContractTerms: {
+ amount: `${currency}:1` as AmountString,
+ summary: "Payment Peer Pull Test",
+ purse_expiration: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ hours: 1 }),
+ ),
+ ),
+ },
+ });
+
+ await waitUntilTransactionPendingReady(wex, peerPullInit.transactionId);
+
+ const peerPullInc = await preparePeerPullDebit(wex, {
+ talerUri: peerPullInit.talerUri,
+ });
+
+ await confirmPeerPullDebit(wex, {
+ transactionId: peerPullInc.transactionId,
+ });
+
+ await waitUntilTransactionWithAssociatedRefreshesFinal(
+ wex,
+ peerPullInc.transactionId,
+ );
+
+ await waitUntilTransactionWithAssociatedRefreshesFinal(
+ wex,
+ peerPullInit.transactionId,
+ );
+
+ await waitUntilTransactionWithAssociatedRefreshesFinal(
+ wex,
+ peerPushCredit.transactionId,
+ );
+
+ await waitUntilTransactionWithAssociatedRefreshesFinal(
+ wex,
+ peerPushInit.transactionId,
+ );
+
+ let depositPayto = withdrawalRes.accountPaytoUri;
+
+ const parsedPayto = parsePaytoUri(depositPayto);
+ if (!parsedPayto) {
+ throw Error("invalid payto");
+ }
+
+ // Work around libeufin-bank bug where receiver-name is missing
+ if (!parsedPayto.params["receiver-name"]) {
+ depositPayto = addPaytoQueryParams(depositPayto, {
+ "receiver-name": "Test",
+ });
+ }
+
+ await createDepositGroup(wex, {
+ amount: `${currency}:5` as AmountString,
+ depositPaytoUri: depositPayto,
+ });
+
+ logger.trace("integration test: all done!");
+}
+
+export async function testPay(
+ wex: WalletExecutionContext,
+ args: TestPayArgs,
+): Promise<TestPayResult> {
+ logger.trace("creating order");
+ const merchant = {
+ authToken: args.merchantAuthToken,
+ baseUrl: args.merchantBaseUrl,
+ };
+ const orderResp = await createOrder(
+ wex.http,
+ merchant,
+ args.amount,
+ args.summary,
+ "taler://fulfillment-success/thank+you",
+ );
+ logger.trace("created new order with order ID", orderResp.orderId);
+ const checkPayResp = await checkPayment(
+ wex.http,
+ merchant,
+ orderResp.orderId,
+ );
+ const talerPayUri = checkPayResp.taler_pay_uri;
+ if (!talerPayUri) {
+ console.error("fatal: no taler pay URI received from backend");
+ process.exit(1);
+ }
+ logger.trace("taler pay URI:", talerPayUri);
+ const result = await preparePayForUri(wex, talerPayUri);
+ if (result.status !== PreparePayResultType.PaymentPossible) {
+ throw Error(`unexpected prepare pay status: ${result.status}`);
+ }
+ const r = await confirmPay(
+ wex,
+ result.transactionId,
+ undefined,
+ args.forcedCoinSel,
+ );
+ if (r.type != ConfirmPayResultType.Done) {
+ throw Error("payment not done");
+ }
+ const purchase = await wex.db.runReadOnlyTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ return tx.purchases.get(result.proposalId);
+ },
+ );
+ checkLogicInvariant(!!purchase);
+ return {
+ numCoins: purchase.payInfo?.payCoinSelection?.coinContributions.length ?? 0,
+ };
+}