taler-typescript-core

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

commit d8b695258e97411d1921f03b83703419aa0bfd88
parent d6a78b960c60d748d1155c51ea86058ec2e6097c
Author: Florian Dold <florian@dold.me>
Date:   Tue, 12 May 2026 11:41:43 +0200

fix typing issue in HTTP clients

For an empty response body, we should not return OperationOk<void>, as
void will be absorbed by the type union operator, leading to missed
checks for the "no result" case.

Diffstat:
Mpackages/taler-harness/src/integrationtests/test-wallet-tokens-discount.ts | 52+++++++++++++++++++++++++++-------------------------
Mpackages/taler-harness/src/integrationtests/test-web-merchant-login.ts | 2+-
Mpackages/taler-util/src/http-client/bank-core.ts | 2+-
Mpackages/taler-util/src/http-client/exchange-client.ts | 18+++++++++---------
Mpackages/taler-util/src/http-client/mailbox.ts | 137+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mpackages/taler-util/src/http-client/merchant.ts | 13++++++++++---
Mpackages/taler-util/src/operation.ts | 4++--
Mpackages/taler-util/src/types-taler-exchange.ts | 27+++++++++++++++++++++++++++
8 files changed, 165 insertions(+), 90 deletions(-)

diff --git a/packages/taler-harness/src/integrationtests/test-wallet-tokens-discount.ts b/packages/taler-harness/src/integrationtests/test-wallet-tokens-discount.ts @@ -86,9 +86,7 @@ export async function runWalletTokensDiscountTest(t: GlobalTestState) { Duration.fromSpec({ years: 1 }), ), ), - duration: Duration.toTalerProtocolDuration( - Duration.fromSpec({ days: 90 }), - ), + duration: Duration.toTalerProtocolDuration(Duration.fromSpec({ days: 90 })), validity_granularity: Duration.toTalerProtocolDuration( Duration.fromSpec({ days: 1 }), ), @@ -149,9 +147,9 @@ export async function runWalletTokensDiscountTest(t: GlobalTestState) { merchantAdminAccessToken, }); - const {discounts} = await walletClient.call( + const { discounts } = await walletClient.call( WalletApiOperation.ListDiscounts, - {} + {}, ); t.assertTrue(discounts.length === 1); @@ -168,9 +166,9 @@ export async function runWalletTokensDiscountTest(t: GlobalTestState) { merchantAdminAccessToken, }); - let {discounts} = await walletClient.call( + let { discounts } = await walletClient.call( WalletApiOperation.ListDiscounts, - {} + {}, ); t.assertTrue(discounts.length === 1); @@ -181,7 +179,7 @@ export async function runWalletTokensDiscountTest(t: GlobalTestState) { // change name of token family tokenFamilyJson.name = "Test discount, but different name"; - succeedOrThrow<TokenFamilyDetails | void>( + succeedOrThrow<TokenFamilyDetails | undefined>( await merchantApi.updateTokenFamily( merchantAdminAccessToken, tokenFamilyJson.slug, @@ -204,16 +202,18 @@ export async function runWalletTokensDiscountTest(t: GlobalTestState) { merchantAdminAccessToken, }); - const {discounts} = await walletClient.call( + const { discounts } = await walletClient.call( WalletApiOperation.ListDiscounts, - {} + {}, ); - const d1 = discounts.find(d => d.name === "Test discount"); + const d1 = discounts.find((d) => d.name === "Test discount"); t.assertTrue(d1 !== undefined); t.assertTrue(d1.tokensAvailable === 2); - d2 = discounts.find(d => d.name === "Test discount, but different name"); + d2 = discounts.find( + (d) => d.name === "Test discount, but different name", + ); t.assertTrue(d2 !== undefined); t.assertTrue(d2.tokensAvailable === 1); } @@ -222,14 +222,13 @@ export async function runWalletTokensDiscountTest(t: GlobalTestState) { logger.info(`Deleting token family with hash ${d2.tokenFamilyHash}`); // delete token family with different name - await walletClient.call( - WalletApiOperation.DeleteDiscount, - { tokenFamilyHash: d2.tokenFamilyHash }, - ); + await walletClient.call(WalletApiOperation.DeleteDiscount, { + tokenFamilyHash: d2.tokenFamilyHash, + }); - const {discounts} = await walletClient.call( + const { discounts } = await walletClient.call( WalletApiOperation.ListDiscounts, - {} + {}, ); t.assertTrue(discounts.length === 1); @@ -237,12 +236,12 @@ export async function runWalletTokensDiscountTest(t: GlobalTestState) { } async function createAndPayOrder(req: { - orderJson: Order, - choiceIndex: number, - t: GlobalTestState, - walletClient: WalletClient, - merchantApi: TalerMerchantInstanceHttpClient, - merchantAdminAccessToken: AccessToken, + orderJson: Order; + choiceIndex: number; + t: GlobalTestState; + walletClient: WalletClient; + merchantApi: TalerMerchantInstanceHttpClient; + merchantAdminAccessToken: AccessToken; }) { const orderResp = succeedOrThrow( await req.merchantApi.createOrder(req.merchantAdminAccessToken, { @@ -277,7 +276,10 @@ async function createAndPayOrder(req: { choiceIndex: req.choiceIndex, }); - await req.walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); + await req.walletClient.call( + WalletApiOperation.TestingWaitTransactionsFinal, + {}, + ); } runWalletTokensDiscountTest.suites = ["merchant", "wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-web-merchant-login.ts b/packages/taler-harness/src/integrationtests/test-web-merchant-login.ts @@ -39,7 +39,7 @@ export async function runWebMerchantLoginTest(t: GlobalTestState) { const title = await browser.getTitle(); - t.assertDeepEqual("GNU Taler Merchant Backoffice", title); + t.assertDeepEqual("Taler Merchant Portal", title); await browser.manage().setTimeouts({ implicit: 2000 }); diff --git a/packages/taler-util/src/http-client/bank-core.ts b/packages/taler-util/src/http-client/bank-core.ts @@ -818,7 +818,7 @@ export class TalerCoreBankHttpClient { ): Promise< | OperationFail<HttpStatusCode.NotFound> | OperationAlternative<HttpStatusCode.Accepted, ChallengeResponse> - | OperationOk<void> + | OperationOk<undefined> | OperationFail<HttpStatusCode.BadRequest> | OperationFail<TalerErrorCode.BANK_UNALLOWED_DEBIT> | OperationFail<TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT> diff --git a/packages/taler-util/src/http-client/exchange-client.ts b/packages/taler-util/src/http-client/exchange-client.ts @@ -80,6 +80,7 @@ import { LegitimizationNeededResponse, PurseConflict, PurseConflictPartial, + PurseCreateSuccessResponse, WalletKycCheckResponse, WalletKycRequest, codecForAccountKycStatus, @@ -104,6 +105,7 @@ import { codecForLegitimizationNeededResponse, codecForPurseConflict, codecForPurseConflictPartial, + codecForPurseCreateSuccessResponse, } from "../types-taler-exchange.js"; import { CacheEvictor, @@ -322,7 +324,7 @@ export class TalerExchangeHttpClient { pursePub: string, body: any, // FIXME ): Promise< - | OperationOk<void> + | OperationOk<PurseCreateSuccessResponse> | OperationFail<HttpStatusCode.Forbidden> | OperationFail<HttpStatusCode.NotFound> | OperationAlternative<HttpStatusCode.Conflict, PurseConflict> @@ -334,8 +336,7 @@ export class TalerExchangeHttpClient { }); switch (resp.status) { case HttpStatusCode.Ok: - // FIXME: parse PurseCreateSuccessResponse - return opSuccessFromHttp(resp, codecForAny()); + return opSuccessFromHttp(resp, codecForPurseCreateSuccessResponse()); case HttpStatusCode.Conflict: return opKnownAlternativeHttpFailure( resp, @@ -359,7 +360,7 @@ export class TalerExchangeHttpClient { pursePub: string, purseSig: string, ): Promise< - | OperationOk<void> + | OperationOk<undefined> | OperationFail<HttpStatusCode.NotFound> | OperationFail<HttpStatusCode.Conflict> | OperationFail<HttpStatusCode.Forbidden> @@ -442,7 +443,7 @@ export class TalerExchangeHttpClient { pursePub: string, body: ExchangeReservePurseRequest, ): Promise< - | OperationOk<void> + | OperationOk<PurseCreateSuccessResponse> | OperationFail<HttpStatusCode.PaymentRequired> | OperationFail<HttpStatusCode.Forbidden> | OperationFail<HttpStatusCode.NotFound> @@ -459,8 +460,7 @@ export class TalerExchangeHttpClient { }); switch (resp.status) { case HttpStatusCode.Ok: - // FIXME: parse PurseCreateSuccessResponse - return opSuccessFromHttp(resp, codecForAny()); + return opSuccessFromHttp(resp, codecForPurseCreateSuccessResponse()); case HttpStatusCode.PaymentRequired: case HttpStatusCode.Forbidden: case HttpStatusCode.NotFound: @@ -571,7 +571,7 @@ export class TalerExchangeHttpClient { body: WalletKycRequest, ): Promise< | OperationOk<WalletKycCheckResponse> - | OperationOk<void> + | OperationOk<undefined> | OperationFail<HttpStatusCode.Forbidden> | OperationAlternative< HttpStatusCode.UnavailableForLegalReasons, @@ -828,7 +828,7 @@ export class TalerExchangeHttpClient { requirement: KycRequirementInformationId, body: T, ): Promise< - | OperationOk<void> + | OperationOk<undefined> | OperationFail<HttpStatusCode.Conflict> | OperationFail<HttpStatusCode.InternalServerError> | OperationFail<HttpStatusCode.NotFound> diff --git a/packages/taler-util/src/http-client/mailbox.ts b/packages/taler-util/src/http-client/mailbox.ts @@ -16,33 +16,33 @@ import { CancellationToken, + EddsaSignatureString, FailCasesByMethod, HttpStatusCode, LibtoolVersion, + MailboxConfiguration, + MailboxMetadata, + MailboxRegisterRequest, + MailboxRegisterResult, + OperationAlternative, OperationFail, OperationOk, ResultByMethod, TalerMailboxApi, carefullyParseConfig, + codecForEmptyObject, codecForTalerMailboxConfigResponse, + codecForTalerMailboxMetadata, codecForTalerMailboxRateLimitedResponse, + decodeCrock, + eddsaGetPublic, + encodeCrock, opEmptySuccess, opFixedSuccess, opKnownAlternativeHttpFailure, opKnownHttpFailure, - opUnknownHttpFailure, - MailboxMetadata, - codecForTalerMailboxMetadata, opSuccessFromHttp, - MailboxRegisterRequest, - encodeCrock, - decodeCrock, - codecForEmptyObject, - MailboxConfiguration, - eddsaGetPublic, - EddsaSignatureString, - MailboxRegisterResult, - OperationAlternative, + opUnknownHttpFailure, } from "@gnu-taler/taler-util"; import { HttpRequestLibrary, @@ -96,11 +96,10 @@ export class TalerMailboxInstanceHttpClient { /** * https://docs.taler.net/core/api-mailbox.html#get--config */ - async getConfig() : - Promise< - | OperationOk<TalerMailboxApi.TalerMailboxConfigResponse> + async getConfig(): Promise< + | OperationOk<TalerMailboxApi.TalerMailboxConfigResponse> | OperationFail<HttpStatusCode.NotFound> - >{ + > { const url = new URL(`/config`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "GET", @@ -127,7 +126,7 @@ export class TalerMailboxInstanceHttpClient { h_address: string; body: Uint8Array; }): Promise< - | OperationOk<void> + | OperationOk<undefined> | OperationFail<TalerMailboxApi.MailboxRateLimitedResponse> | OperationFail<HttpStatusCode.PaymentRequired> | OperationFail<HttpStatusCode.TooManyRequests> @@ -153,7 +152,11 @@ export class TalerMailboxInstanceHttpClient { return opKnownHttpFailure(resp.status, resp); } case HttpStatusCode.TooManyRequests: { - return opKnownAlternativeHttpFailure(resp, resp.status, codecForTalerMailboxRateLimitedResponse()); + return opKnownAlternativeHttpFailure( + resp, + resp.status, + codecForTalerMailboxRateLimitedResponse(), + ); } default: return opUnknownHttpFailure(resp); @@ -167,9 +170,8 @@ export class TalerMailboxInstanceHttpClient { hMailbox: string; }): Promise< | OperationOk<MailboxMessagesResponseRaw> - | OperationOk<void> | OperationFail<HttpStatusCode.TooManyRequests> - >{ + > { const { hMailbox: hMailbox } = args; const url = new URL(`${hMailbox.toUpperCase()}`, this.baseUrl); @@ -180,16 +182,22 @@ export class TalerMailboxInstanceHttpClient { switch (resp.status) { case HttpStatusCode.Ok: { - const uintar = await resp.bytes() as Uint8Array; - const etag = resp.headers.get("etag") - const index = etag? etag : "0"; - return opFixedSuccess({messages: uintar, etag: index}); + const uintar = (await resp.bytes()) as Uint8Array; + const etag = resp.headers.get("etag"); + const index = etag ? etag : "0"; + return opFixedSuccess({ messages: uintar, etag: index }); } case HttpStatusCode.NoContent: { - return opEmptySuccess(); + const etag = resp.headers.get("etag"); + const index = etag ? etag : "0"; + return opFixedSuccess({ messages: new Uint8Array(), etag: index }); } case HttpStatusCode.TooManyRequests: { - return opKnownAlternativeHttpFailure(resp, resp.status, codecForTalerMailboxRateLimitedResponse()); + return opKnownAlternativeHttpFailure( + resp, + resp.status, + codecForTalerMailboxRateLimitedResponse(), + ); } default: return opUnknownHttpFailure(resp); @@ -205,20 +213,30 @@ export class TalerMailboxInstanceHttpClient { count: number; signature: EddsaSignatureString; }): Promise< - | OperationOk<void> + | OperationOk<undefined> | OperationFail<HttpStatusCode.Forbidden> | OperationFail<HttpStatusCode.NotFound> - >{ - const { mailboxConf, matchIf: etag, count: count, signature: signature } = args; - const mailboxPubkeyString = encodeCrock(eddsaGetPublic(decodeCrock(mailboxConf.privateKey))); - const url = new URL(`${mailboxPubkeyString.toUpperCase()}?count=${count}`, this.baseUrl); - + > { + const { + mailboxConf, + matchIf: etag, + count: count, + signature: signature, + } = args; + const mailboxPubkeyString = encodeCrock( + eddsaGetPublic(decodeCrock(mailboxConf.privateKey)), + ); + const url = new URL( + `${mailboxPubkeyString.toUpperCase()}?count=${count}`, + this.baseUrl, + ); const resp = await this.httpLib.fetch(url.href, { method: "DELETE", headers: { - "If-Match" : etag, - "Taler-Mailbox-Delete-Signature" : signature}, + "If-Match": etag, + "Taler-Mailbox-Delete-Signature": signature, + }, cancellationToken: this.cancellationToken, }); @@ -229,7 +247,7 @@ export class TalerMailboxInstanceHttpClient { case HttpStatusCode.Forbidden: case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); - default: + default: return opUnknownHttpFailure(resp); } } @@ -237,11 +255,13 @@ export class TalerMailboxInstanceHttpClient { /** * https://docs.taler.net/core/api-mailbox.html#get--info-$H_MAILBOX */ - async getMailboxInfo(hMailbox: string) : Promise< + async getMailboxInfo( + hMailbox: string, + ): Promise< | OperationOk<MailboxMetadata> - | OperationFail<HttpStatusCode.NotFound> - | OperationFail<HttpStatusCode.TooManyRequests> - >{ + | OperationFail<HttpStatusCode.NotFound> + | OperationFail<HttpStatusCode.TooManyRequests> + > { const url = new URL(`info/${hMailbox.toUpperCase()}`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { @@ -251,16 +271,23 @@ export class TalerMailboxInstanceHttpClient { switch (resp.status) { case HttpStatusCode.Ok: { - return opSuccessFromHttp(resp, - codecForTalerMailboxMetadata()); + return opSuccessFromHttp(resp, codecForTalerMailboxMetadata()); } case HttpStatusCode.NotFound: { - return opKnownAlternativeHttpFailure(resp, resp.status, codecForEmptyObject()); + return opKnownAlternativeHttpFailure( + resp, + resp.status, + codecForEmptyObject(), + ); } case HttpStatusCode.TooManyRequests: { - return opKnownAlternativeHttpFailure(resp, resp.status, codecForTalerMailboxRateLimitedResponse()); + return opKnownAlternativeHttpFailure( + resp, + resp.status, + codecForTalerMailboxRateLimitedResponse(), + ); } - default: + default: return opUnknownHttpFailure(resp); } } @@ -268,11 +295,16 @@ export class TalerMailboxInstanceHttpClient { /** * https://docs.taler.net/core/api-mailbox.html#post--register */ - async registerMailbox(req: MailboxRegisterRequest) : Promise< + async registerMailbox( + req: MailboxRegisterRequest, + ): Promise< | OperationOk<MailboxRegisterResult> | OperationFail<HttpStatusCode.Forbidden> - | OperationAlternative<HttpStatusCode.PaymentRequired, MailboxRegisterResult> - >{ + | OperationAlternative< + HttpStatusCode.PaymentRequired, + MailboxRegisterResult + > + > { const url = new URL(`register`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { @@ -283,13 +315,20 @@ export class TalerMailboxInstanceHttpClient { switch (resp.status) { case HttpStatusCode.NoContent: { - return opFixedSuccess({status: "ok"} as MailboxRegisterResult); + return opFixedSuccess({ status: "ok" } as MailboxRegisterResult); } case HttpStatusCode.Forbidden: { return opKnownHttpFailure(resp.status, resp); } case HttpStatusCode.PaymentRequired: { - return { type: "fail", case: resp.status, body: { status: "payment-required", talerUri: resp.headers.get("Taler")} as MailboxRegisterResult}; + return { + type: "fail", + case: resp.status, + body: { + status: "payment-required", + talerUri: resp.headers.get("Taler"), + } as MailboxRegisterResult, + }; } default: return opUnknownHttpFailure(resp); diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts @@ -32,8 +32,8 @@ import { PaginationParams, ResultByMethod, TalerErrorCode, - TalerErrorDetail, TalerMerchantApi, + TokenFamilyDetails, assertUnreachable, carefullyParseConfig, codecForAbortResponse, @@ -2718,7 +2718,12 @@ export class TalerMerchantInstanceHttpClient { token: AccessToken, tokenSlug: string, body: TalerMerchantApi.TokenFamilyUpdateRequest, - ) { + ): Promise< + | OperationOk<undefined> + | OperationOk<TokenFamilyDetails> + | OperationFail<HttpStatusCode.NotFound> + | OperationFail<HttpStatusCode.Unauthorized> + > { const url = new URL(`private/tokenfamilies/${tokenSlug}`, this.baseUrl); const headers: Record<string, string> = {}; @@ -2995,7 +3000,9 @@ export class TalerMerchantInstanceHttpClient { async postDonau(args: { body: MerchantPostDonauBody; token?: AccessToken; - }): Promise<OperationOk<void> | OperationFail<HttpStatusCode.BadGateway>> { + }): Promise< + OperationOk<undefined> | OperationFail<HttpStatusCode.BadGateway> + > { const headers: Record<string, string> = {}; if (args.token) { headers.Authorization = makeBearerTokenAuthHeader(args.token); diff --git a/packages/taler-util/src/operation.ts b/packages/taler-util/src/operation.ts @@ -107,8 +107,8 @@ export function opFixedSuccess<T>(body: T): OperationOk<T> { return { type: "ok" as const, case: "ok", body }; } -export function opEmptySuccess(): OperationOk<void> { - return { type: "ok" as const, case: "ok", body: void 0 }; +export function opEmptySuccess(): OperationOk<undefined> { + return { type: "ok" as const, case: "ok", body: undefined }; } export function opKnownFailure<const T>(case_: T): OperationFail<T> { diff --git a/packages/taler-util/src/types-taler-exchange.ts b/packages/taler-util/src/types-taler-exchange.ts @@ -1239,6 +1239,33 @@ export const codecForExchangeMergeSuccessResponse = .property("exchange_pub", codecForEddsaPublicKey()) .build("ExchangeMergeSuccessResponse"); +export interface PurseCreateSuccessResponse { + // Total amount deposited into the purse so far (without fees). + total_deposited: AmountString; + + // Time at the exchange. + exchange_timestamp: Timestamp; + + // EdDSA signature of the exchange affirming the payment, + // of purpose TALER_SIGNATURE_PURSE_DEPOSIT_CONFIRMED + // over a TALER_PurseDepositConfirmedSignaturePS. + // Signs over the above and the purse public key and + // the hash of the contract terms. + exchange_sig: EddsaSignature; + + // public key used to create the signature. + exchange_pub: EddsaPublicKey; +} + +export const codecForPurseCreateSuccessResponse = + (): Codec<PurseCreateSuccessResponse> => + buildCodecForObject<PurseCreateSuccessResponse>() + .property("total_deposited", codecForAmountString()) + .property("exchange_timestamp", codecForTimestamp) + .property("exchange_sig", codecForEddsaSignature()) + .property("exchange_pub", codecForEddsaPublicKey()) + .build("PurseCreateSuccessResponse"); + /** * Doc name: api-exchange/MergeConflict */