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:
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");
}