taler-typescript-core

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

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:
Mpackages/taler-harness/src/harness/fake-challenger.ts | 49++++++-------------------------------------------
Apackages/taler-harness/src/harness/http-server.ts | 142+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/taler-harness/src/integrationtests/test-util-merchant-client.ts | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 2++
Mpackages/taler-util/src/http-client/merchant.ts | 18++++++++++++++----
Mpackages/taler-util/src/types-taler-merchant.ts | 80++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mpackages/taler-wallet-core/src/pay-merchant.ts | 49+++++++++++++++++++++++++------------------------
Mpackages/taler-wallet-core/src/wallet.ts | 38++++++++++++++++++++++++++++----------
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") {