From e951075d2ef52fa8e9e7489c62031777c3a7e66b Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Mon, 19 Feb 2024 18:05:48 +0100 Subject: wallet-core: flatten directory structure --- packages/taler-wallet-core/src/testing.ts | 913 ++++++++++++++++++++++++++++++ 1 file changed, 913 insertions(+) create mode 100644 packages/taler-wallet-core/src/testing.ts (limited to 'packages/taler-wallet-core/src/testing.ts') diff --git a/packages/taler-wallet-core/src/testing.ts b/packages/taler-wallet-core/src/testing.ts new file mode 100644 index 000000000..38b2471e3 --- /dev/null +++ b/packages/taler-wallet-core/src/testing.ts @@ -0,0 +1,913 @@ +/* + 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 + */ + +/** + * @file + * Implementation of wallet-core operations that are used for testing, + * but typically not in the production wallet. + */ + +/** + * Imports. + */ +import { + AbsoluteTime, + addPaytoQueryParams, + Amounts, + AmountString, + CheckPaymentResponse, + codecForAny, + codecForCheckPaymentResponse, + ConfirmPayResultType, + Duration, + IntegrationTestArgs, + IntegrationTestV2Args, + j2s, + Logger, + NotificationType, + OpenedPromise, + openPromise, + 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 { getRefreshesForTransaction } from "./index.js"; +import { InternalWalletState } from "./internal-wallet-state.js"; +import { checkLogicInvariant } from "./util/invariants.js"; +import { getBalances } from "./balance.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 { getTransactionById, getTransactions } from "./transactions.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( + ws: InternalWalletState, + req: WithdrawTestBalanceRequest, +): Promise { + 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(ws, { + 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 { + 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 { + 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 { + 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( + ws: InternalWalletState, + merchant: MerchantBackendInfo, + amount: string, + summary: string, +): Promise { + const orderResp = await createOrder( + ws.http, + merchant, + amount, + summary, + "taler://fulfillment-success/thx", + ); + + logger.trace("created order with orderId", orderResp.orderId); + + let paymentStatus = await checkPayment(ws.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(ws, talerPayUri); + + logger.trace("prepare pay result", preparePayResult); + + if (preparePayResult.status != "payment-possible") { + throw Error("payment not possible"); + } + + const confirmPayResult = await confirmPay( + ws, + preparePayResult.transactionId, + undefined, + ); + + logger.trace("confirmPayResult", confirmPayResult); + + paymentStatus = await checkPayment(ws.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( + ws: InternalWalletState, + args: IntegrationTestArgs, +): Promise { + 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(ws, { + amount: args.amountToWithdraw, + corebankApiBaseUrl: args.corebankApiBaseUrl, + exchangeBaseUrl: args.exchangeBaseUrl, + }); + await waitUntilGivenTransactionsFinal(ws, [withdrawRes1.transactionId]); + logger.info("done withdrawing test balance"); + + const balance = await getBalances(ws); + + logger.trace(JSON.stringify(balance, null, 2)); + + const myMerchant: MerchantBackendInfo = { + baseUrl: args.merchantBaseUrl, + authToken: args.merchantAuthToken, + }; + + const makePaymentRes = await makePayment( + ws, + myMerchant, + args.amountToSpend, + "hello world", + ); + + await waitUntilTransactionWithAssociatedRefreshesFinal( + ws, + 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(ws, { + amount: Amounts.stringify(withdrawAmountTwo), + corebankApiBaseUrl: args.corebankApiBaseUrl, + exchangeBaseUrl: args.exchangeBaseUrl, + }); + + await waitUntilGivenTransactionsFinal(ws, [withdrawRes2.transactionId]); + + const { orderId: refundOrderId } = await makePayment( + ws, + myMerchant, + Amounts.stringify(spendAmountTwo), + "order that will be refunded", + ); + + const refundUri = await refund( + ws.http, + myMerchant, + refundOrderId, + "test refund", + Amounts.stringify(refundAmount), + ); + + logger.trace("refund URI", refundUri); + + const refundResp = await startRefundQueryForUri(ws, refundUri); + + logger.trace("integration test: applied refund"); + + // Wait until the refund is done + await waitUntilTransactionWithAssociatedRefreshesFinal( + ws, + refundResp.transactionId, + ); + + logger.trace("integration test: making payment after refund"); + + const paymentResp2 = await makePayment( + ws, + myMerchant, + Amounts.stringify(spendAmountThree), + "payment after refund", + ); + + logger.trace("integration test: make payment done"); + + await waitUntilGivenTransactionsFinal(ws, [ + paymentResp2.paymentTransactionId, + ]); + await waitUntilGivenTransactionsFinal( + ws, + await getRefreshesForTransaction(ws, paymentResp2.paymentTransactionId), + ); + + logger.trace("integration test: all done!"); +} + +/** + * Wait until all transactions are in a final state. + */ +export async function waitUntilAllTransactionsFinal( + ws: InternalWalletState, +): Promise { + logger.info("waiting until all transactions are in a final state"); + ws.ensureTaskLoopRunning(); + let p: OpenedPromise | undefined = undefined; + const cancelNotifs = ws.addNotificationListener((notif) => { + if (!p) { + return; + } + if (notif.type === NotificationType.TransactionStateTransition) { + switch (notif.newTxState.major) { + case TransactionMajorState.Pending: + case TransactionMajorState.Aborting: + 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) { + 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 all transactions are in a final state"); +} + +/** + * Wait until all chosen transactions are in a final state. + */ +export async function waitUntilGivenTransactionsFinal( + ws: InternalWalletState, + transactionIds: string[], +): Promise { + 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 | 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"); +} + +export async function waitUntilRefreshesDone( + ws: InternalWalletState, +): Promise { + logger.info("waiting until all refresh transactions are in a final state"); + ws.ensureTaskLoopRunning(); + let p: OpenedPromise | undefined = undefined; + const cancelNotifs = ws.addNotificationListener((notif) => { + if (!p) { + return; + } + if (notif.type === NotificationType.TransactionStateTransition) { + switch (notif.newTxState.major) { + case TransactionMajorState.Pending: + case TransactionMajorState.Aborting: + 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 (tx.type !== TransactionType.Refresh) { + 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 all refreshes are in a final state"); +} + +async function waitUntilTransactionPendingReady( + ws: InternalWalletState, + transactionId: string, +): Promise { + logger.info(`starting waiting for ${transactionId} to be in pending(ready)`); + ws.ensureTaskLoopRunning(); + let p: OpenedPromise | undefined = undefined; + const cancelNotifs = ws.addNotificationListener((notif) => { + if (!p) { + return; + } + if (notif.type === NotificationType.TransactionStateTransition) { + p.resolve(); + } + }); + while (1) { + p = openPromise(); + const tx = await getTransactionById(ws, { + transactionId, + }); + if ( + tx.txState.major == TransactionMajorState.Pending && + tx.txState.minor === TransactionMinorState.Ready + ) { + break; + } + // Wait until transaction state changed + await p.promise; + } + logger.info(`done waiting for ${transactionId} to be in pending(ready)`); + cancelNotifs(); +} + +/** + * Wait until a transaction is in a particular state. + */ +export async function waitTransactionState( + ws: InternalWalletState, + transactionId: string, + txState: TransactionState, +): Promise { + logger.info( + `starting waiting for ${transactionId} to be in ${JSON.stringify( + txState, + )})`, + ); + ws.ensureTaskLoopRunning(); + let p: OpenedPromise | undefined = undefined; + const cancelNotifs = ws.addNotificationListener((notif) => { + if (!p) { + return; + } + if (notif.type === NotificationType.TransactionStateTransition) { + p.resolve(); + } + }); + while (1) { + p = openPromise(); + const tx = await getTransactionById(ws, { + transactionId, + }); + if ( + tx.txState.major === txState.major && + tx.txState.minor === txState.minor + ) { + break; + } + // Wait until transaction state changed + await p.promise; + } + logger.info( + `done waiting for ${transactionId} to be in ${JSON.stringify(txState)}`, + ); + cancelNotifs(); +} + +export async function waitUntilTransactionWithAssociatedRefreshesFinal( + ws: InternalWalletState, + transactionId: string, +): Promise { + await waitUntilGivenTransactionsFinal(ws, [transactionId]); + await waitUntilGivenTransactionsFinal( + ws, + await getRefreshesForTransaction(ws, transactionId), + ); +} + +export async function waitUntilTransactionFinal( + ws: InternalWalletState, + transactionId: string, +): Promise { + await waitUntilGivenTransactionsFinal(ws, [transactionId]); +} + +export async function runIntegrationTest2( + ws: InternalWalletState, + args: IntegrationTestV2Args, +): Promise { + ws.ensureTaskLoopRunning(); + logger.info("running test with arguments", args); + + const exchangeInfo = await fetchFreshExchange(ws, 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(ws, { + amount: Amounts.stringify(amountToWithdraw), + corebankApiBaseUrl: args.corebankApiBaseUrl, + exchangeBaseUrl: args.exchangeBaseUrl, + }); + await waitUntilTransactionFinal(ws, withdrawalRes.transactionId); + logger.info("done withdrawing test balance"); + + const balance = await getBalances(ws); + + logger.trace(JSON.stringify(balance, null, 2)); + + const myMerchant: MerchantBackendInfo = { + baseUrl: args.merchantBaseUrl, + authToken: args.merchantAuthToken, + }; + + const makePaymentRes = await makePayment( + ws, + myMerchant, + Amounts.stringify(amountToSpend), + "hello world", + ); + + await waitUntilTransactionWithAssociatedRefreshesFinal( + ws, + 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(ws, { + amount: Amounts.stringify(withdrawAmountTwo), + corebankApiBaseUrl: args.corebankApiBaseUrl, + exchangeBaseUrl: args.exchangeBaseUrl, + }); + + // Wait until the withdraw is done + await waitUntilTransactionFinal(ws, withdrawalRes2.transactionId); + + const { orderId: refundOrderId } = await makePayment( + ws, + myMerchant, + Amounts.stringify(spendAmountTwo), + "order that will be refunded", + ); + + const refundUri = await refund( + ws.http, + myMerchant, + refundOrderId, + "test refund", + Amounts.stringify(refundAmount), + ); + + logger.trace("refund URI", refundUri); + + const refundResp = await startRefundQueryForUri(ws, refundUri); + + logger.trace("integration test: applied refund"); + + // Wait until the refund is done + await waitUntilTransactionWithAssociatedRefreshesFinal( + ws, + refundResp.transactionId, + ); + + logger.trace("integration test: making payment after refund"); + + const makePaymentRes2 = await makePayment( + ws, + myMerchant, + Amounts.stringify(spendAmountThree), + "payment after refund", + ); + + await waitUntilTransactionWithAssociatedRefreshesFinal( + ws, + makePaymentRes2.paymentTransactionId, + ); + + logger.trace("integration test: make payment done"); + + const peerPushInit = await initiatePeerPushDebit(ws, { + 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(ws, peerPushInit.transactionId); + const txDetails = await getTransactionById(ws, { + 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(ws, { + talerUri: txDetails.talerUri, + }); + + await confirmPeerPushCredit(ws, { + transactionId: peerPushCredit.transactionId, + }); + + const peerPullInit = await initiatePeerPullPayment(ws, { + 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(ws, peerPullInit.transactionId); + + const peerPullInc = await preparePeerPullDebit(ws, { + talerUri: peerPullInit.talerUri, + }); + + await confirmPeerPullDebit(ws, { + peerPullDebitId: peerPullInc.peerPullDebitId, + }); + + await waitUntilTransactionWithAssociatedRefreshesFinal( + ws, + peerPullInc.transactionId, + ); + + await waitUntilTransactionWithAssociatedRefreshesFinal( + ws, + peerPullInit.transactionId, + ); + + await waitUntilTransactionWithAssociatedRefreshesFinal( + ws, + peerPushCredit.transactionId, + ); + + await waitUntilTransactionWithAssociatedRefreshesFinal( + ws, + 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(ws, { + amount: `${currency}:5` as AmountString, + depositPaytoUri: depositPayto, + }); + + logger.trace("integration test: all done!"); +} + +export async function testPay( + ws: InternalWalletState, + args: TestPayArgs, +): Promise { + logger.trace("creating order"); + const merchant = { + authToken: args.merchantAuthToken, + baseUrl: args.merchantBaseUrl, + }; + const orderResp = await createOrder( + ws.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(ws.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(ws, talerPayUri); + if (result.status !== PreparePayResultType.PaymentPossible) { + throw Error(`unexpected prepare pay status: ${result.status}`); + } + const r = await confirmPay( + ws, + result.transactionId, + undefined, + args.forcedCoinSel, + ); + if (r.type != ConfirmPayResultType.Done) { + throw Error("payment not done"); + } + const purchase = await ws.db.runReadOnlyTx(["purchases"], async (tx) => { + return tx.purchases.get(result.proposalId); + }); + checkLogicInvariant(!!purchase); + return { + payCoinSelection: purchase.payInfo?.payCoinSelection!, + }; +} -- cgit v1.2.3