taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit ef2f1c0d0628c50c6f768bc552655119b59f7687
parent df794dd2e222863d4f15831fc2a9d5ea508792be
Author: Florian Dold <florian@dold.me>
Date:   Fri, 22 Aug 2025 23:15:58 +0200

wallet-core: allow faking payment transactions

Diffstat:
Mpackages/taler-wallet-core/src/dev-experiments.ts | 112++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 111 insertions(+), 1 deletion(-)

diff --git a/packages/taler-wallet-core/src/dev-experiments.ts b/packages/taler-wallet-core/src/dev-experiments.ts @@ -28,9 +28,12 @@ import { AbsoluteTime, Amounts, + ContractTermsUtil, DenomLossEventType, Duration, Logger, + MerchantContractTermsV0, + MerchantContractVersion, RefreshReason, TalerPreciseTimestamp, encodeCrock, @@ -42,10 +45,12 @@ import { HttpRequestOptions, HttpResponse, } from "@gnu-taler/taler-util/http"; +import { randomBytes } from "crypto"; import { PendingTaskType, constructTaskIdentifier } from "./common.js"; import { DenomLossEventRecord, DenomLossStatus, + PurchaseStatus, RefreshGroupRecord, RefreshOperationStatus, WithdrawalGroupStatus, @@ -53,6 +58,7 @@ import { timestampPreciseToDb, } from "./db.js"; import { DenomLossTransactionContext } from "./exchanges.js"; +import { PayMerchantTransactionContext } from "./pay-merchant.js"; import { RefreshTransactionContext } from "./refresh.js"; import { rematerializeTransactions } from "./transactions.js"; import { DevExperimentState, WalletExecutionContext } from "./wallet.js"; @@ -207,7 +213,7 @@ export async function applyDevExperiment( return; } case "add-fake-tx": { - const txType = parsedUri.query?.get("txType") ?? "withdrawal"; + const txType = parsedUri.query?.get("txType") ?? "withdrawal"; const withdrawalGroupId = encodeCrock(getRandomBytes(32)); const ctx = new WithdrawTransactionContext(wex, withdrawalGroupId); switch (txType) { @@ -270,6 +276,110 @@ export async function applyDevExperiment( }); break; } + case "payment": { + const proposalId = encodeCrock(getRandomBytes(32)); + const ctx = new PayMerchantTransactionContext(wex, proposalId); + const summary = parsedUri.query?.get("summary") ?? "Test"; + const amountEffectiveStr = parsedUri.query?.get("amountEffective"); + if (!amountEffectiveStr) { + throw Error("missing amountEffective option"); + } + const merchantName = + parsedUri.query?.get("merchantName") ?? "Test Merchant"; + const noncePair = await wex.ws.cryptoApi.createEddsaKeypair({}); + const merchantPub = encodeCrock(getRandomBytes(32)); + const exchangeBaseUrl = + parsedUri.query?.get("exchangeBaseUrl") ?? + "https://exchange.demo.taler.net/"; + const merchantBaseUrl = + parsedUri.query?.get("merchantBaseUrl") ?? + "https://backend.demo.taler.net/"; + const amountEffective = Amounts.parseOrThrow(amountEffectiveStr); + const trelStr = parsedUri.query?.get("tRel") ?? undefined; + let timestamp: AbsoluteTime; + if (trelStr) { + timestamp = AbsoluteTime.subtractDuraction( + AbsoluteTime.now(), + Duration.fromPrettyString(trelStr), + ); + } else { + timestamp = AbsoluteTime.now(); + } + const orderId = encodeCrock(randomBytes(8)); + const ct: MerchantContractTermsV0 = { + amount: Amounts.stringify(amountEffective), + exchanges: [ + { + master_pub: encodeCrock(getRandomBytes(32)), + priority: 1, + url: exchangeBaseUrl, + }, + ], + h_wire: encodeCrock(getRandomBytes(64)), + max_fee: Amounts.stringify(Amounts.zeroOfAmount(amountEffective)), + timestamp: AbsoluteTime.toProtocolTimestamp(timestamp), + summary, + order_id: orderId, + merchant_base_url: merchantBaseUrl, + merchant_pub: merchantPub, + wire_method: "x-taler-bank", + version: MerchantContractVersion.V0, + nonce: encodeCrock(getRandomBytes(32)), + merchant: { + name: merchantName, + }, + pay_deadline: AbsoluteTime.toProtocolTimestamp(timestamp), + refund_deadline: AbsoluteTime.toProtocolTimestamp(timestamp), + wire_transfer_deadline: AbsoluteTime.toProtocolTimestamp(timestamp), + }; + const contractTermsHash = ContractTermsUtil.hashContractTerms(ct); + await wex.db.runAllStoresReadWriteTx({}, async (tx) => { + await tx.contractTerms.put({ + contractTermsRaw: ct, + h: contractTermsHash, + }); + await tx.purchases.put({ + autoRefundDeadline: undefined, + claimToken: encodeCrock(randomBytes(32)), + downloadSessionId: encodeCrock(randomBytes(32)), + orderId, + proposalId, + download: { + contractTermsHash, + contractTermsMerchantSig: encodeCrock(randomBytes(64)), + currency: Amounts.currencyOf(amountEffective), + }, + merchantBaseUrl, + purchaseStatus: PurchaseStatus.Done, + repurchaseProposalId: undefined, + lastSessionId: undefined, + merchantPaySig: encodeCrock(randomBytes(64)), + noncePriv: noncePair.priv, + noncePub: noncePair.pub, + payInfo: { + totalPayCost: Amounts.stringify(amountEffective), + }, + posConfirmation: undefined, + secretSeed: encodeCrock(randomBytes(32)), + timestamp: timestampPreciseToDb( + AbsoluteTime.toPreciseTimestamp(timestamp), + ), + shared: false, + timestampFirstSuccessfulPay: timestampPreciseToDb( + AbsoluteTime.toPreciseTimestamp(timestamp), + ), + timestampAccept: timestampPreciseToDb( + AbsoluteTime.toPreciseTimestamp(timestamp), + ), + timestampLastRefundStatus: timestampPreciseToDb( + AbsoluteTime.toPreciseTimestamp(timestamp), + ), + refundAmountAwaiting: undefined, + }); + await ctx.updateTransactionMeta(tx); + }); + break; + } default: { throw Error("transaction type not supported"); }