commit 6085e8f811f2ce2067213fbf6a7eb5a7527116d1
parent 34df06f55f4cc7fc80aec28a28f943c2a58e3f31
Author: Florian Dold <florian@dold.me>
Date: Mon, 19 May 2025 15:07:43 +0200
wallet: fix handling of extra contract terms fields
Also adds a test.
Issue: https://bugs.taler.net/n/9974
Diffstat:
8 files changed, 374 insertions(+), 114 deletions(-)
diff --git a/packages/taler-harness/src/harness/fake-challenger.ts b/packages/taler-harness/src/harness/fake-challenger.ts
@@ -17,6 +17,12 @@
import { AbsoluteTime, Duration, j2s, Logger } from "@gnu-taler/taler-util";
import * as http from "node:http";
import { inflateSync } from "node:zlib";
+import {
+ readBodyBytes,
+ readBodyStr,
+ respondJson,
+ splitInTwoAt,
+} from "./http-server.js";
const logger = new Logger("fake-challenger.ts");
@@ -26,49 +32,6 @@ export interface TestfakeChallengerService {
getSetupRequest(nonce: string): any;
}
-function splitInTwoAt(s: string, separator: string): [string, string] {
- const idx = s.indexOf(separator);
- if (idx === -1) {
- return [s, ""];
- }
- return [s.slice(0, idx), s.slice(idx + 1)];
-}
-
-function readBodyStr(req: http.IncomingMessage): Promise<string> {
- return new Promise((resolve, reject) => {
- let reqBody = "";
- req.on("data", (x) => {
- reqBody += x;
- });
-
- req.on("end", () => {
- resolve(reqBody);
- });
- });
-}
-
-function readBodyBytes(req: http.IncomingMessage): Promise<Uint8Array> {
- return new Promise((resolve, reject) => {
- let chunks: Buffer[] = [];
- req.on("data", (x) => {
- chunks.push(Buffer.from(x));
- });
-
- req.on("end", () => {
- resolve(new Uint8Array(Buffer.concat(chunks)));
- });
- });
-}
-
-function respondJson(
- resp: http.ServerResponse<http.IncomingMessage>,
- status: number,
- body: any,
-): void {
- resp.writeHead(status, { "Content-Type": "application/json" });
- resp.end(JSON.stringify(body));
-}
-
/**
* Testfake for the kyc service that the exchange talks to.
*/
diff --git a/packages/taler-harness/src/harness/http-server.ts b/packages/taler-harness/src/harness/http-server.ts
@@ -0,0 +1,142 @@
+/*
+ This file is part of GNU Taler
+ (C) 2025 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/>
+ */
+
+import { Logger } from "@gnu-taler/taler-util";
+import * as http from "node:http";
+
+const logger = new Logger("http-server.ts");
+
+export function splitInTwoAt(s: string, separator: string): [string, string] {
+ const idx = s.indexOf(separator);
+ if (idx === -1) {
+ return [s, ""];
+ }
+ return [s.slice(0, idx), s.slice(idx + 1)];
+}
+
+export function readBodyStr(req: http.IncomingMessage): Promise<string> {
+ return new Promise((resolve, reject) => {
+ let reqBody = "";
+ req.on("data", (x) => {
+ reqBody += x;
+ });
+
+ req.on("end", () => {
+ resolve(reqBody);
+ });
+ });
+}
+
+export function readBodyBytes(req: http.IncomingMessage): Promise<Uint8Array> {
+ return new Promise((resolve, reject) => {
+ let chunks: Buffer[] = [];
+ req.on("data", (x) => {
+ chunks.push(Buffer.from(x));
+ });
+
+ req.on("end", () => {
+ resolve(new Uint8Array(Buffer.concat(chunks)));
+ });
+ });
+}
+
+export function respondJson(
+ resp: http.ServerResponse<http.IncomingMessage>,
+ status: number,
+ body: any,
+): void {
+ resp.writeHead(status, { "Content-Type": "application/json" });
+ resp.end(JSON.stringify(body));
+}
+
+export interface SimpleServerResponse {
+ /**
+ * HTTP response status.
+ */
+ status: number;
+
+ /**
+ * JSON body.
+ */
+ body: any;
+}
+
+export interface SimpleServerRequest {
+ body: any;
+ method: string;
+}
+
+/**
+ * Very simple HTTP server to be used as a mock server for tests.
+ */
+export class HarnessHttpServer {
+ private handlers: {
+ path: string;
+ handler: (req: SimpleServerRequest) => Promise<SimpleServerResponse>;
+ }[] = [];
+
+ private server:
+ | http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>
+ | undefined;
+
+ /**
+ * Add a handler for a hard-coded request path.
+ */
+ addSimpleJsonHandler(
+ path: string,
+ handler: (req: SimpleServerRequest) => Promise<any>,
+ ): void {
+ this.handlers.push({ path, handler });
+ }
+
+ async start(port: number): Promise<void> {
+ const server = (this.server = http.createServer(async (req, res) => {
+ const requestUrl = req.url!;
+ logger.info(`harness server: got ${req.method} request, ${requestUrl}`);
+ const method = req.method?.toUpperCase();
+ if (!method) {
+ throw Error("no method");
+ }
+ let body: any | undefined;
+ if (method === "POST" || method === "PUT" || method === "PATCH") {
+ body = JSON.parse(await readBodyStr(req));
+ }
+
+ const [path, query] = splitInTwoAt(requestUrl, "?");
+
+ for (const handler of this.handlers) {
+ if (handler.path === path) {
+ const handlerResp = await handler.handler({
+ method,
+ body,
+ });
+ respondJson(res, handlerResp.status, handlerResp.body);
+ return;
+ }
+ }
+ logger.warn("no handler");
+ respondJson(res, 404, {});
+ }));
+
+ await new Promise<void>((resolve, reject) => {
+ server.listen(port, () => resolve());
+ });
+ }
+
+ stop() {
+ this.server?.close();
+ }
+}
diff --git a/packages/taler-harness/src/integrationtests/test-util-merchant-client.ts b/packages/taler-harness/src/integrationtests/test-util-merchant-client.ts
@@ -0,0 +1,110 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 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/>
+ */
+
+/**
+ * Imports.
+ */
+import { TalerMerchantInstanceHttpClient } from "@gnu-taler/taler-util";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ HarnessHttpServer,
+ SimpleServerRequest,
+} from "../harness/http-server.js";
+
+const contractTerms = {
+ amount: "CHF:0.01",
+ exchanges: [
+ {
+ master_pub: "3QMK2XAETQVAGFFS07PKBC6T7509P78WY71KKTQCQ5NRD7RW9SZG",
+ max_contribution: "CHF:0.01",
+ priority: 1024,
+ url: "https://exchange.chf.taler.net/",
+ },
+ ],
+ h_wire:
+ "HPRC43CQETW1XGMCHFWE5MNT9J06WBTBZAWTS0BEBX05NKJH7GMSWX0XWF8DWFS4C1FFEEF6YH7RBFMM05EXSPQDW31Q1C9BD5SRKE0",
+ max_fee: "CHF:0",
+ merchant: {
+ address: {},
+ jurisdiction: {},
+ name: "BFH TI Snack Machine",
+ website: "https://bfh.ch/",
+ fav_food: "tofu",
+ },
+ merchant_base_url: "https://backend.chf.taler.net/instances/snack/",
+ merchant_pub: "YPTPDHNZJ4QQQ8AS0DP4GHRSDMQ4G9VYFPS1TGM7HNN4XK894DGG",
+ minimum_age: 0,
+ nonce: "8BANE1BD5TEAAAA44DY0DRRPG3DJFJA2H9PNT11P9Q4AD0TD3SY0",
+ order_id: "2025.139-01R8J0HBQ5282",
+ pay_deadline: {
+ t_s: 1747655143,
+ },
+ products: [],
+ refund_deadline: {
+ t_s: 1747820743,
+ },
+ summary: "Test FD1",
+ timestamp: {
+ t_s: 1747647943,
+ },
+ version: 0,
+ wire_method: "iban",
+ wire_transfer_deadline: {
+ t_s: 1747820743,
+ },
+ foo: 42,
+};
+
+/**
+ * Test to ensure that the wallet does not throw away unknown fields
+ * when parsing contract terms.
+ */
+export async function runUtilMerchantClientTest(t: GlobalTestState) {
+ const server = new HarnessHttpServer();
+
+ server.addSimpleJsonHandler(
+ "/orders/myId/claim",
+ async (req: SimpleServerRequest) => {
+ return {
+ status: 200,
+ body: { contract_terms: contractTerms, sig: "foo" },
+ };
+ },
+ );
+
+ await server.start(8081);
+
+ const merchantClient = new TalerMerchantInstanceHttpClient(
+ "http://localhost:8081",
+ );
+
+ const orderResp = await merchantClient.claimOrder({
+ body: {
+ nonce: "foobar",
+ },
+ orderId: "myId",
+ });
+
+ t.assertDeepEqual(orderResp.case, "ok");
+ t.assertDeepEqual(orderResp.body.contract_terms.foo, 42);
+ t.assertDeepEqual(orderResp.body.contract_terms.merchant.fav_food, "tofu");
+
+ console.log("yay");
+
+ server.stop();
+}
+
+runUtilMerchantClientTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts
@@ -145,6 +145,7 @@ import { runWalletBlockedPayPeerPullTest } from "./test-wallet-blocked-pay-peer-
import { runWalletBlockedPayPeerPushTest } from "./test-wallet-blocked-pay-peer-push.js";
import { runWalletCliTerminationTest } from "./test-wallet-cli-termination.js";
import { runWalletConfigTest } from "./test-wallet-config.js";
+import { runUtilMerchantClientTest } from "./test-util-merchant-client.js";
import { runWalletCryptoWorkerTest } from "./test-wallet-cryptoworker.js";
import { runWalletDblessTest } from "./test-wallet-dbless.js";
import { runWalletDd48Test } from "./test-wallet-dd48.js";
@@ -341,6 +342,7 @@ const allTests: TestMainFunction[] = [
runTopsChallengerTwiceTest,
runKycFormBadMeasureTest,
runKycBalanceWithdrawalChangeManualTest,
+ runUtilMerchantClientTest,
];
export interface TestRunSpec {
diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts
@@ -16,6 +16,7 @@
import {
AccessToken,
+ CancellationToken,
FailCasesByMethod,
HttpStatusCode,
LibtoolVersion,
@@ -78,7 +79,7 @@ import {
HttpRequestLibrary,
HttpResponse,
createPlatformHttpLib,
- readSuccessResponseJsonOrThrow
+ readSuccessResponseJsonOrThrow,
} from "@gnu-taler/taler-util/http";
import { opSuccessFromHttp } from "../operation.js";
import {
@@ -150,14 +151,17 @@ export class TalerMerchantInstanceHttpClient {
readonly httpLib: HttpRequestLibrary;
readonly cacheEvictor: CacheEvictor<TalerMerchantInstanceCacheEviction>;
+ readonly cancellationToken: CancellationToken | undefined;
constructor(
readonly baseUrl: string,
httpClient?: HttpRequestLibrary,
cacheEvictor?: CacheEvictor<TalerMerchantInstanceCacheEviction>,
+ cancellationToken?: CancellationToken,
) {
this.httpLib = httpClient ?? createPlatformHttpLib();
this.cacheEvictor = cacheEvictor ?? nullEvictor;
+ this.cancellationToken = cancellationToken;
}
isCompatible(version: string): boolean {
@@ -262,12 +266,20 @@ export class TalerMerchantInstanceHttpClient {
/**
* https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-orders-$ORDER_ID-claim
*/
- async claimOrder(orderId: string, body: TalerMerchantApi.ClaimRequest) {
+ async claimOrder(args: {
+ orderId: string;
+ body: TalerMerchantApi.ClaimRequest;
+ }): Promise<
+ | OperationOk<TalerMerchantApi.ClaimResponse>
+ | OperationFail<HttpStatusCode.Conflict>
+ > {
+ const { orderId, body } = args;
const url = new URL(`orders/${orderId}/claim`, this.baseUrl);
const resp = await this.httpLib.fetch(url.href, {
method: "POST",
body,
+ cancellationToken: this.cancellationToken,
});
switch (resp.status) {
@@ -279,8 +291,6 @@ export class TalerMerchantInstanceHttpClient {
}
case HttpStatusCode.Conflict:
return opKnownHttpFailure(resp.status, resp);
- case HttpStatusCode.NotFound:
- return opKnownHttpFailure(resp.status, resp);
default:
return opUnknownHttpFailure(resp);
}
diff --git a/packages/taler-util/src/types-taler-merchant.ts b/packages/taler-util/src/types-taler-merchant.ts
@@ -86,8 +86,10 @@ import {
/**
* Proposal returned from the contract URL.
+ *
+ * Doc name: api-merchant/ClaimResponse.
*/
-export interface Proposal {
+export interface MerchantClaimResponse {
/**
* Contract terms for the propoal.
* Raw, un-decoded JSON object.
@@ -119,9 +121,7 @@ export interface SignedTokenEnvelope {
blind_sig: TokenIssueBlindSig;
}
-export type TokenIssueBlindSig =
-| RSATokenIssueBlindSig
-| CSTokenIssueBlindSig;
+export type TokenIssueBlindSig = RSATokenIssueBlindSig | CSTokenIssueBlindSig;
export interface RSATokenIssueBlindSig {
cipher: DenomKeyType.Rsa;
@@ -204,11 +204,11 @@ export const codecForMerchantRefundPermission =
.property("exchange_pub", codecOptional(codecForString()))
.build("MerchantRefundPermission");
-export const codecForProposal = (): Codec<Proposal> =>
- buildCodecForObject<Proposal>()
+export const codecForMerchantClaimResponse = (): Codec<MerchantClaimResponse> =>
+ buildCodecForObject<MerchantClaimResponse>()
.property("contract_terms", codecForMerchantContractTerms())
.property("sig", codecForString())
- .build("Proposal");
+ .build("MerchantClaimResponse");
export const codecForCheckPaymentResponse = (): Codec<CheckPaymentResponse> =>
buildCodecForObject<CheckPaymentResponse>()
@@ -297,7 +297,10 @@ export const codecForMerchantPayResponse = (): Codec<MerchantPayResponse> =>
buildCodecForObject<MerchantPayResponse>()
.property("sig", codecForString())
.property("pos_confirmation", codecOptional(codecForString()))
- .property("token_sigs", codecOptional(codecForList(codecForSignedTokenEnvelope())))
+ .property(
+ "token_sigs",
+ codecOptional(codecForList(codecForSignedTokenEnvelope())),
+ )
.build("MerchantPayResponse");
export const codecForSignedTokenEnvelope = (): Codec<SignedTokenEnvelope> =>
@@ -990,7 +993,10 @@ const codecForMerchantContractTermsCommon =
export const codecForMerchantContractTermsV0 =
(): Codec<MerchantContractTermsV0> =>
buildCodecForObject<MerchantContractTermsV0>()
- .property("version", codecOptional(codecForConstNumber(MerchantContractVersion.V0)))
+ .property(
+ "version",
+ codecOptional(codecForConstNumber(MerchantContractVersion.V0)),
+ )
.property("amount", codecForAmountString())
.property("max_fee", codecForAmountString())
.mixin(codecForMerchantContractTermsCommon())
@@ -1028,7 +1034,10 @@ export const codecForMerchantContractChoice =
export const codecForMerchantContractInput = (): Codec<MerchantContractInput> =>
buildCodecForUnion<MerchantContractInput>()
.discriminateOn("type")
- .alternative(MerchantContractInputType.Token, codecForMerchantContractInputToken())
+ .alternative(
+ MerchantContractInputType.Token,
+ codecForMerchantContractInputToken(),
+ )
.build("TalerMerchantApi.ContractInput");
export const codecForMerchantContractInputToken =
@@ -1045,11 +1054,11 @@ export const codecForMerchantContractOutput =
.discriminateOn("type")
.alternative(
MerchantContractOutputType.Token,
- codecForMerchantContractOutputToken()
+ codecForMerchantContractOutputToken(),
)
.alternative(
MerchantContractOutputType.TaxReceipt,
- codecForMerchantContractOutputTaxReceipt()
+ codecForMerchantContractOutputTaxReceipt(),
)
.build("TalerMerchantApi.ContractOutput");
@@ -1065,7 +1074,10 @@ export const codecForMerchantContractOutputToken =
export const codecForMerchantContractOutputTaxReceipt =
(): Codec<MerchantContractOutputTaxReceipt> =>
buildCodecForObject<MerchantContractOutputTaxReceipt>()
- .property("type", codecForConstString(MerchantContractOutputType.TaxReceipt))
+ .property(
+ "type",
+ codecForConstString(MerchantContractOutputType.TaxReceipt),
+ )
.property("donau_urls", codecForList(codecForString()))
.property("amount", codecOptional(codecForAmountString()))
.build("TalerMerchantApi.ContractOutputTaxReceipt");
@@ -1118,21 +1130,27 @@ export const codecForMerchantContractTokenDetails =
)
.alternative(
MerchantContractTokenKind.Discount,
- codecForMerchantContractDiscountTokenDetails()
+ codecForMerchantContractDiscountTokenDetails(),
)
.build("TalerMerchantApi.ContractTokenDetails");
export const codecForMerchantContractSubscriptionTokenDetails =
(): Codec<MerchantContractSubscriptionTokenDetails> =>
buildCodecForObject<MerchantContractSubscriptionTokenDetails>()
- .property("class", codecForConstString(MerchantContractTokenKind.Subscription))
+ .property(
+ "class",
+ codecForConstString(MerchantContractTokenKind.Subscription),
+ )
.property("trusted_domains", codecForList(codecForString()))
.build("TalerMerchantApi.ContractSubscriptionTokenDetails");
export const codecForMerchantContractDiscountTokenDetails =
(): Codec<MerchantContractDiscountTokenDetails> =>
buildCodecForObject<MerchantContractDiscountTokenDetails>()
- .property("class", codecForConstString(MerchantContractTokenKind.Discount))
+ .property(
+ "class",
+ codecForConstString(MerchantContractTokenKind.Discount),
+ )
.property("expected_domains", codecForList(codecForString()))
.build("TalerMerchantApi.ContractDiscountTokenDetails");
@@ -1202,7 +1220,7 @@ export interface ClaimRequest {
export interface ClaimResponse {
// Contract terms of the claimed order
- contract_terms: MerchantContractTerms;
+ contract_terms: any;
// Signature by the merchant over the contract terms.
sig: EddsaSignatureString;
@@ -1280,14 +1298,14 @@ export interface GetKycStatusRequestParams {
timeout?: number;
/**
- * Specifies what status change we are long-polling for.
- * Use 1 to wait for the KYC auth transfer (access token available),
- * 2 to wait for an AML investigation to be done,
- * and 3 to wait for the KYC status to be OK. If multiple accounts
- * or exchanges match the query, any account reaching the TARGET
+ * Specifies what status change we are long-polling for.
+ * Use 1 to wait for the KYC auth transfer (access token available),
+ * 2 to wait for an AML investigation to be done,
+ * and 3 to wait for the KYC status to be OK. If multiple accounts
+ * or exchanges match the query, any account reaching the TARGET
* state will cause the response to be returned.
*/
- reason?: KycStatusLongPollingReason,
+ reason?: KycStatusLongPollingReason;
}
export interface GetOtpDeviceRequestParams {
// Timestamp in seconds to use when calculating
@@ -2311,9 +2329,7 @@ export interface PostOrderRequest {
otp_id?: string;
}
-export type Order =
- | OrderV0
- | OrderV1;
+export type Order = OrderV0 | OrderV1;
export interface MinimalOrderDetail {
// Amount to be paid by the customer.
@@ -3060,8 +3076,7 @@ export enum OrderInputType {
Token = "token",
}
-export type OrderInput =
- | OrderInputToken;
+export type OrderInput = OrderInputToken;
export interface OrderInputToken {
// Token input.
@@ -3082,9 +3097,7 @@ export enum OrderOutputType {
TaxReceipt = "tax-receipt",
}
-export type OrderOutput =
- | OrderOutputToken
- | OrderOutputTaxReceipt;
+export type OrderOutput = OrderOutputToken | OrderOutputTaxReceipt;
export interface OrderOutputToken {
type: OrderOutputType.Token;
@@ -3235,7 +3248,7 @@ export interface OrderV1 extends OrderCommon {
// Version 1 order support discounts and subscriptions.
// https://docs.taler.net/design-documents/046-mumimo-contracts.html
// @since protocol **vSUBSCRIBE**
- version: OrderVersion.V1,
+ version: OrderVersion.V1;
// List of contract choices that the customer can select from.
// @since protocol **vSUBSCRIBE**
@@ -3443,7 +3456,8 @@ export const codecForTalerMerchantConfigResponse =
export const codecForClaimResponse = (): Codec<ClaimResponse> =>
buildCodecForObject<ClaimResponse>()
- .property("contract_terms", codecForMerchantContractTerms())
+ // Must be 'any', otherwise, contract terms won't match.
+ .property("contract_terms", codecForAny())
.property("sig", codecForEddsaSignature())
.build("TalerMerchantApi.ClaimResponse");
diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts
@@ -42,7 +42,6 @@ import {
codecForMerchantOrderStatusPaid,
codecForMerchantPayResponse,
codecForPostOrderResponse,
- codecForProposal,
codecForWalletRefundResponse,
codecForWalletTemplateDetails,
CoinDepositPermission,
@@ -113,7 +112,6 @@ import {
import {
getHttpResponseErrorDetails,
HttpResponse,
- readSuccessResponseJsonOrErrorCode,
readSuccessResponseJsonOrThrow,
readTalerErrorResponse,
throwUnexpectedRequestError,
@@ -185,6 +183,7 @@ import {
EXCHANGE_COINS_LOCK,
getDenomInfo,
WalletExecutionContext,
+ walletMerchantClient,
} from "./wallet.js";
/**
@@ -1024,30 +1023,30 @@ async function processDownloadProposal(
requestBody.token = proposal.claimToken;
}
- const httpResponse = await cancelableFetch(wex, orderClaimUrl, {
- method: "POST",
+ const merchantClient = walletMerchantClient(proposal.merchantBaseUrl, wex);
+
+ const claimResp = await merchantClient.claimOrder({
+ orderId: proposal.orderId,
body: requestBody,
});
- const r = await readSuccessResponseJsonOrErrorCode(
- httpResponse,
- codecForProposal(),
- );
- if (r.isError) {
- switch (r.talerErrorResponse.code) {
- case TalerErrorCode.MERCHANT_POST_ORDERS_ID_CLAIM_ALREADY_CLAIMED:
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED,
- {
- orderId: proposal.orderId,
- claimUrl: orderClaimUrl.href,
- },
- "order already claimed (likely by other wallet)",
- );
- default:
- throwUnexpectedRequestError(httpResponse, r.talerErrorResponse);
- }
+
+ switch (claimResp.case) {
+ case "ok":
+ break;
+ case HttpStatusCode.Conflict:
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED,
+ {
+ orderId: proposal.orderId,
+ claimUrl: orderClaimUrl.href,
+ },
+ "order already claimed (likely by other wallet)",
+ );
+ default:
+ assertUnreachable(claimResp);
}
- const proposalResp = r.response;
+
+ const proposalResp = claimResp.body;
// The proposalResp contains the contract terms as raw JSON,
// as the code to parse them doesn't necessarily round-trip.
@@ -3413,7 +3412,9 @@ async function processPurchasePay(
const slatesLen = slates?.length ?? 0;
const sigsLen = merchantResp.token_sigs?.length ?? 0;
if (slatesLen !== sigsLen) {
- throw Error(`merchant returned mismatching number of token signatures (${slatesLen} vs ${sigsLen})`);
+ throw Error(
+ `merchant returned mismatching number of token signatures (${slatesLen} vs ${sigsLen})`,
+ );
} else if (slates && merchantResp.token_sigs) {
for (let i = 0; i < slates.length; i++) {
const slate = slates[i];
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
@@ -113,8 +113,8 @@ import {
TalerBankIntegrationHttpClient,
TalerError,
TalerErrorCode,
- TalerExchangeHttpClient,
TalerExchangeHttpClient2,
+ TalerMerchantInstanceHttpClient,
TalerProtocolTimestamp,
TalerUriAction,
TestingGetDenomStatsRequest,
@@ -434,6 +434,18 @@ export function walletExchangeClient(
});
}
+export function walletMerchantClient(
+ baseUrl: string,
+ wex: WalletExecutionContext,
+): TalerMerchantInstanceHttpClient {
+ return new TalerMerchantInstanceHttpClient(
+ baseUrl,
+ wex.http,
+ undefined,
+ wex.cancellationToken,
+ );
+}
+
export const EXCHANGE_COINS_LOCK = "exchange-coins-lock";
export const EXCHANGE_RESERVES_LOCK = "exchange-reserves-lock";
@@ -525,9 +537,9 @@ export async function getDenomInfo(
if (d != null) {
return DenominationRecord.toDenomInfo(d);
} else {
- return undefined
+ return undefined;
}
- })
+ });
}
/**
@@ -2467,7 +2479,7 @@ async function dispatchWalletCoreApiRequest(
wex = getObservedWalletExecutionContext(ws, cts.token, cts, oc);
} else {
oc = {
- observe(evt) { },
+ observe(evt) {},
};
wex = getNormalWalletExecutionContext(ws, cts.token, cts, oc);
}
@@ -2623,7 +2635,7 @@ export class Cache<T> {
constructor(
private maxCapacity: number,
private cacheDuration: Duration,
- ) { }
+ ) {}
get(key: string): T | undefined {
const r = this.map.get(key);
@@ -2640,15 +2652,21 @@ export class Cache<T> {
}
async getOrPut(key: string, lambda: () => Promise<T>): Promise<T>;
- async getOrPut(key: string, lambda: () => Promise<T | undefined>): Promise<T | undefined>;
- async getOrPut(key: string, lambda: () => Promise<T | undefined>): Promise<T | undefined> {
+ async getOrPut(
+ key: string,
+ lambda: () => Promise<T | undefined>,
+ ): Promise<T | undefined>;
+ async getOrPut(
+ key: string,
+ lambda: () => Promise<T | undefined>,
+ ): Promise<T | undefined> {
const cached = this.get(key);
if (cached != null) {
- return cached
+ return cached;
} else {
const computed = await lambda();
if (computed != null) {
- this.put(key, computed)
+ this.put(key, computed);
}
return computed;
}
@@ -2674,7 +2692,7 @@ export class Cache<T> {
* Implementation of triggers for the wallet DB.
*/
class WalletDbTriggerSpec implements TriggerSpec {
- constructor(public ws: InternalWalletState) { }
+ constructor(public ws: InternalWalletState) {}
afterCommit(info: AfterCommitInfo): void {
if (info.mode !== "readwrite") {