taler-typescript-core

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

commit 20432a25b713c876b5109bfdbe458280227ae3a2
parent e7aca513025f2626396147b774dc38837e0dff40
Author: Florian Dold <florian@dold.me>
Date:   Wed, 23 Apr 2025 20:46:44 +0200

refactor request result type, use client lib for p2p push merge

Diffstat:
Mpackages/aml-backoffice-ui/src/hooks/decisions.ts | 4+++-
Mpackages/aml-backoffice-ui/src/hooks/transfers.ts | 6++----
Mpackages/aml-backoffice-ui/src/pages/CaseDetails.tsx | 17+++++------------
Mpackages/bank-ui/src/components/Cashouts/views.tsx | 2+-
Mpackages/bank-ui/src/hooks/account.ts | 9+++++++--
Mpackages/bank-ui/src/hooks/regional.ts | 4++--
Mpackages/bank-ui/src/pages/SolveChallengePage.tsx | 10+++++-----
Mpackages/bank-ui/src/pages/admin/AccountList.tsx | 23+++++++++++++----------
Mpackages/bank-ui/src/pages/admin/AdminHome.tsx | 2+-
Mpackages/bank-ui/src/pages/regional/CreateCashout.tsx | 2+-
Mpackages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/hooks/webhooks.ts | 24++++++++++++++++--------
Mpackages/merchant-backoffice-ui/src/paths/admin/list/index.tsx | 2+-
Mpackages/taler-harness/src/integrationtests/test-known-accounts.ts | 2+-
Mpackages/taler-harness/src/integrationtests/test-peer-push.ts | 12++++++++++--
Mpackages/taler-util/src/http-client/bank-core.ts | 5+----
Mpackages/taler-util/src/http-client/exchange.ts | 44++++++++++++++++++++++++++++++++++++++++++--
Mpackages/taler-util/src/http-client/merchant.ts | 6++----
Mpackages/taler-util/src/operation.ts | 30+++++++++++++-----------------
Mpackages/taler-util/src/types-taler-exchange.ts | 31+++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/common.ts | 46++++++++++++++++++++--------------------------
Mpackages/taler-wallet-core/src/deposits.ts | 8++++----
Mpackages/taler-wallet-core/src/exchanges.ts | 6+++---
Mpackages/taler-wallet-core/src/pay-merchant.ts | 6+++---
Mpackages/taler-wallet-core/src/pay-peer-pull-credit.ts | 6+++---
Mpackages/taler-wallet-core/src/pay-peer-pull-debit.ts | 4++--
Mpackages/taler-wallet-core/src/pay-peer-push-credit.ts | 144++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Mpackages/taler-wallet-core/src/pay-peer-push-debit.ts | 4++--
Mpackages/taler-wallet-core/src/withdraw.ts | 8++++----
Mpackages/taler-wallet-webextension/src/wallet/QrReader.tsx | 6+++---
30 files changed, 278 insertions(+), 197 deletions(-)

diff --git a/packages/aml-backoffice-ui/src/hooks/decisions.ts b/packages/aml-backoffice-ui/src/hooks/decisions.ts @@ -203,7 +203,8 @@ type PaginatedResult<T> = OperationOk<T> & { loadFirst(): void; }; -//TODO: consider sending this to web-util +// FIXME: consider moving this to web-util +// FIXME: reconsider return type, this is not an HTTP response! export function buildPaginatedResult<R, OffId>( data: R[], offset: OffId | undefined, @@ -219,6 +220,7 @@ export function buildPaginatedResult<R, OffId>( } return { type: "ok", + case: "ok", body: result, isLastPage, isFirstPage, diff --git a/packages/aml-backoffice-ui/src/hooks/transfers.ts b/packages/aml-backoffice-ui/src/hooks/transfers.ts @@ -18,17 +18,14 @@ import { useState } from "preact/hooks"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 import { AmountJson, - Amounts, - AmountString, OfficerAccount, OperationOk, opFixedSuccess, - opSuccessFromHttp, TalerExchangeResultByMethod, TalerHttpError, } from "@gnu-taler/taler-util"; import { useExchangeApiContext } from "@gnu-taler/web-util/browser"; -import _useSWR, { SWRHook, mutate } from "swr"; +import _useSWR, { mutate, SWRHook } from "swr"; import { useOfficer } from "./officer.js"; const useSWR = _useSWR as unknown as SWRHook; @@ -212,6 +209,7 @@ export function buildPaginatedResult<R, OffId>( } return { type: "ok", + case: "ok", body: result, isLastPage, isFirstPage, diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx @@ -16,8 +16,6 @@ import { AbsoluteTime, AmlDecisionRequest, - AmountJson, - Amounts, assertUnreachable, buildCodecForObject, Codec, @@ -29,11 +27,11 @@ import { LimitOperationType, OperationFail, OperationOk, + opFixedSuccess, TalerError, TalerErrorDetail, TalerExchangeApi, TalerFormAttributes, - TranslatedString, } from "@gnu-taler/taler-util"; import { Attention, @@ -42,12 +40,10 @@ import { FormDesign, FormMetadata, FormUI, - InternationalizationAPI, Loading, LocalNotificationBanner, RouteDefinition, Time, - UIHandlerId, useExchangeApiContext, useForm, useLocalNotificationHandler, @@ -975,13 +971,10 @@ function parseJustification( detail: {} as TalerErrorDetail, }; } - return { - type: "ok", - body: { - justification, - metadata: found, - }, - }; + return opFixedSuccess({ + justification, + metadata: found, + }); } catch (e) { return { type: "fail", diff --git a/packages/bank-ui/src/components/Cashouts/views.tsx b/packages/bank-ui/src/components/Cashouts/views.tsx @@ -79,7 +79,7 @@ export function ReadyView({ ); } default: - assertUnreachable(resp.case); + assertUnreachable(resp); } } diff --git a/packages/bank-ui/src/hooks/account.ts b/packages/bank-ui/src/hooks/account.ts @@ -139,7 +139,10 @@ export function useWithdrawalDetails(wid: string | undefined) { // retry if error let retry = err !== undefined; // retry if still pending - retry = r !== undefined && r.type === "ok" && (r.body.status === "pending" || r.body.status === "selected"); + retry = + r !== undefined && + r.type === "ok" && + (r.body.status === "pending" || r.body.status === "selected"); return retry; }); @@ -267,7 +270,8 @@ type PaginatedResult<T> = OperationOk<T> & { loadNext(): void; loadFirst(): void; }; -//TODO: consider sending this to web-util + +// TODO: consider sending this to web-util export function buildPaginatedResult<DataType, OffsetId>( data: DataType[], offset: OffsetId | undefined, @@ -284,6 +288,7 @@ export function buildPaginatedResult<DataType, OffsetId>( } return { type: "ok", + case: "ok", body: result, isLastPage, isFirstPage, diff --git a/packages/bank-ui/src/hooks/regional.ts b/packages/bank-ui/src/hooks/regional.ts @@ -34,8 +34,8 @@ import { import { useBankCoreApiContext } from "@gnu-taler/web-util/browser"; import { useState } from "preact/hooks"; import _useSWR, { SWRHook, mutate } from "swr"; -import { buildPaginatedResult } from "./account.js"; import { PAGINATED_LIST_REQUEST } from "../utils.js"; +import { buildPaginatedResult } from "./account.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 const useSWR = _useSWR as unknown as SWRHook; @@ -396,7 +396,7 @@ export function useCashouts(account: string) { }), ); const cashouts = all.filter(notUndefined); - return { type: "ok" as const, body: { cashouts } }; + return opFixedSuccess({ cashouts }); } const { data, error } = useSWR< | OperationOk<{ cashouts: CashoutWithId[] }> diff --git a/packages/bank-ui/src/pages/SolveChallengePage.tsx b/packages/bank-ui/src/pages/SolveChallengePage.tsx @@ -22,8 +22,12 @@ import { TalerCorebankApi, TalerError, TalerErrorCode, + TokenSuccessResponse, TranslatedString, + UserAndPassword, + UserAndToken, assertUnreachable, + createRFC8959AccessTokenEncoded, parsePaytoUri, } from "@gnu-taler/taler-util"; import { @@ -48,10 +52,6 @@ import { useSessionState } from "../hooks/session.js"; import { undefinedIfEmpty } from "../utils.js"; import { RenderAmount } from "./PaytoWireTransferForm.js"; import { OperationNotFound } from "./WithdrawalQRCode.js"; -import { UserAndToken } from "@gnu-taler/taler-util"; -import { UserAndPassword } from "@gnu-taler/taler-util"; -import { TokenSuccessResponse } from "@gnu-taler/taler-util"; -import { createRFC8959AccessTokenEncoded } from "@gnu-taler/taler-util"; const TALER_SCREEN_ID = 111; @@ -899,7 +899,7 @@ function ShowCashoutDetails({ ); } default: - assertUnreachable(info.case); + assertUnreachable(info); } } diff --git a/packages/bank-ui/src/pages/admin/AccountList.tsx b/packages/bank-ui/src/pages/admin/AccountList.tsx @@ -19,12 +19,15 @@ import { TalerError, assertUnreachable, } from "@gnu-taler/taler-util"; -import { Loading, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + Loading, + RouteDefinition, + useBankCoreApiContext, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js"; -import { useBankCoreApiContext } from "@gnu-taler/web-util/browser"; import { useBusinessAccounts } from "../../hooks/regional.js"; -import { RouteDefinition } from "@gnu-taler/web-util/browser"; import { RenderAmount } from "../PaytoWireTransferForm.js"; const TALER_SCREEN_ID = 121; @@ -53,13 +56,13 @@ export function AccountList({ if (result instanceof TalerError) { return <ErrorLoadingWithDebug error={result} />; } - if (result.type === "fail") { - switch (result.case) { - case HttpStatusCode.Unauthorized: - return <Fragment />; - default: - assertUnreachable(result.case); - } + switch (result.case) { + case "ok": + break; + case HttpStatusCode.Unauthorized: + return <Fragment />; + default: + assertUnreachable(result); } const onGoStart = result.isFirstPage ? undefined : result.loadFirst; diff --git a/packages/bank-ui/src/pages/admin/AdminHome.tsx b/packages/bank-ui/src/pages/admin/AdminHome.tsx @@ -257,7 +257,7 @@ function Metrics({ ); } default: { - assertUnreachable(respInfo.case); + assertUnreachable(respInfo); } } } diff --git a/packages/bank-ui/src/pages/regional/CreateCashout.tsx b/packages/bank-ui/src/pages/regional/CreateCashout.tsx @@ -159,7 +159,7 @@ export function CreateCashout({ ); } default: - assertUnreachable(info.case); + assertUnreachable(info); } } diff --git a/packages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx b/packages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx @@ -103,7 +103,7 @@ export function ShowCashoutDetails({ id, routeClose }: Props): VNode { ); } default: - assertUnreachable(info.case); + assertUnreachable(info); } } diff --git a/packages/merchant-backoffice-ui/src/hooks/webhooks.ts b/packages/merchant-backoffice-ui/src/hooks/webhooks.ts @@ -16,13 +16,17 @@ import { PAGINATED_LIST_REQUEST } from "../utils/constants.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import { AccessToken, OperationOk, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util"; +import { + AccessToken, + OperationOk, + TalerHttpError, + TalerMerchantManagementResultByMethod, +} from "@gnu-taler/taler-util"; import _useSWR, { SWRHook, mutate } from "swr"; import { useSessionContext } from "../context/session.js"; const useSWR = _useSWR as unknown as SWRHook; -export interface InstanceWebhookFilter { -} +export interface InstanceWebhookFilter {} export function revalidateInstanceWebhooks() { return mutate( @@ -62,11 +66,15 @@ type PaginatedResult<T> = OperationOk<T> & { isFirstPage: boolean; loadNext(): void; loadFirst(): void; -} +}; //TODO: consider sending this to web-util -export function buildPaginatedResult<R, OffId>(data: R[], offset: OffId | undefined, setOffset: (o: OffId | undefined) => void, getId: (r: R) => OffId): PaginatedResult<R[]> { - +export function buildPaginatedResult<R, OffId>( + data: R[], + offset: OffId | undefined, + setOffset: (o: OffId | undefined) => void, + getId: (r: R) => OffId, +): PaginatedResult<R[]> { const isLastPage = data.length < PAGINATED_LIST_REQUEST; const isFirstPage = offset === undefined; @@ -76,12 +84,13 @@ export function buildPaginatedResult<R, OffId>(data: R[], offset: OffId | undefi } return { type: "ok", + case: "ok", body: result, isLastPage, isFirstPage, loadNext: () => { if (!result.length) return; - const id = getId(result[result.length - 1]) + const id = getId(result[result.length - 1]); setOffset(id); }, loadFirst: () => { @@ -90,7 +99,6 @@ export function buildPaginatedResult<R, OffId>(data: R[], offset: OffId | undefi }; } - export function revalidateWebhookDetails() { return mutate( (key) => Array.isArray(key) && key[key.length - 1] === "getWebhookDetails", diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx @@ -66,7 +66,7 @@ export default function Instances({ onCreate, onUpdate }: Props): VNode { return <LoginPage />; } default: { - assertUnreachable(result.case); + assertUnreachable(result); } } } diff --git a/packages/taler-harness/src/integrationtests/test-known-accounts.ts b/packages/taler-harness/src/integrationtests/test-known-accounts.ts @@ -17,7 +17,7 @@ /** * Imports. */ -import { j2s, TalerCorebankApiClient, TalerCoreBankHttpClient } from "@gnu-taler/taler-util"; +import { j2s, TalerCorebankApiClient } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { useSharedTestkudosEnvironment, diff --git a/packages/taler-harness/src/integrationtests/test-peer-push.ts b/packages/taler-harness/src/integrationtests/test-peer-push.ts @@ -175,14 +175,14 @@ export async function runPeerPushTest(t: GlobalTestState) { // Idempotent // FIXME support conflict during POST /merge - /*await Promise.all([ + await Promise.all([ wallet2.call(WalletApiOperation.ConfirmPeerPushCredit, { transactionId: prepare2.transactionId, }), wallet3.call(WalletApiOperation.ConfirmPeerPushCredit, { transactionId: prepare3.transactionId, }), - ]);*/ + ]); await Promise.all([ wallet1.call(WalletApiOperation.TestingWaitTransactionState, { @@ -190,12 +190,16 @@ export async function runPeerPushTest(t: GlobalTestState) { txState: { major: TransactionMajorState.Done, }, + logId: `confirm-w1`, + timeout: { seconds: 20 }, }), wallet2.call(WalletApiOperation.TestingWaitTransactionState, { transactionId: prepare2.transactionId, txState: { major: TransactionMajorState.Done, }, + logId: `confirm-w2`, + timeout: { seconds: 20 }, }), // FIXME should be aborted wallet3.call(WalletApiOperation.TestingWaitTransactionState, { @@ -204,12 +208,16 @@ export async function runPeerPushTest(t: GlobalTestState) { major: TransactionMajorState.Pending, minor: TransactionMinorState.Merge, }, + logId: `confirm-w3`, + timeout: { seconds: 20 }, }), wallet4.call(WalletApiOperation.TestingWaitTransactionState, { transactionId: prepare4.transactionId, txState: { major: TransactionMajorState.Aborted, }, + logId: `confirm-w4`, + timeout: { seconds: 20 }, }), ]); diff --git a/packages/taler-util/src/http-client/bank-core.ts b/packages/taler-util/src/http-client/bank-core.ts @@ -263,10 +263,7 @@ export class TalerCoreBankHttpClient { resp, codecForCoreBankConfig(), ); - return { - type: "ok", - body, - }; + return opFixedSuccess(body); } case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); diff --git a/packages/taler-util/src/http-client/exchange.ts b/packages/taler-util/src/http-client/exchange.ts @@ -25,6 +25,7 @@ import { createPlatformHttpLib } from "../http.js"; import { LibtoolVersion } from "../libtool-version.js"; import { FailCasesByMethod, + OperationAlternative, OperationFail, OperationOk, ResultByMethod, @@ -49,8 +50,11 @@ import { BatchWithdrawResponse, ExchangeKycUploadFormRequest, ExchangeLegacyBatchWithdrawRequest, + ExchangeMergeSuccessResponse, + ExchangePurseMergeRequest, ExchangeVersionResponse, KycRequirementInformationId, + LegitimizationNeededResponse, WalletKycRequest, codecForAccountKycStatus, codecForAmlDecisionsResponse, @@ -60,6 +64,7 @@ import { codecForEventCounter, codecForExchangeConfig, codecForExchangeKeysResponse, + codecForExchangeMergeSuccessResponse, codecForExchangeTransferList, codecForKycProcessClientInformation, codecForKycProcessStartInformation, @@ -71,6 +76,7 @@ import { TalerError } from "../errors.js"; import { AmountJson, Amounts, + CancellationToken, signAmlDecision, signAmlQuery, signKycAuth, @@ -135,7 +141,10 @@ export class TalerExchangeHttpClient { * https://docs.taler.net/core/api-exchange.html#get--seed * */ - async getSeed() { + async getSeed(): Promise< + | OperationOk<Uint8Array<ArrayBuffer>> + | OperationFail<HttpStatusCode.NotFound> + > { const url = new URL(`seed`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "GET", @@ -194,6 +203,7 @@ export class TalerExchangeHttpClient { ); return { type: "ok", + case: "ok", body, }; } @@ -571,10 +581,40 @@ export class TalerExchangeHttpClient { } /** + * POST /purses/$PURSE_PUB/merge + * * https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge * */ - async mergePurse(): Promise<never> { + async postPurgeMerge(args: { + pursePub: string; + body: ExchangePurseMergeRequest; + cancellationToken?: CancellationToken; + }): Promise< + | OperationOk<ExchangeMergeSuccessResponse> + | OperationAlternative< + HttpStatusCode.UnavailableForLegalReasons, + LegitimizationNeededResponse + > + > { + const mergePurseUrl = new URL( + `purses/${args.pursePub}/merge`, + this.baseUrl, + ); + const resp = await this.httpLib.fetch(mergePurseUrl.href, { + method: "POST", + body: args.body, + }); + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForExchangeMergeSuccessResponse()); + case HttpStatusCode.UnavailableForLegalReasons: + return opKnownAlternativeFailure( + resp, + resp.status, + codecForLegitimizationNeededResponse(), + ); + } throw Error("not yet implemented"); } diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts @@ -69,6 +69,7 @@ import { codecForWebhookDetails, codecForWebhookSummaryResponse, opEmptySuccess, + opFixedSuccess, opKnownAlternativeFailure, opKnownHttpFailure, } from "@gnu-taler/taler-util"; @@ -203,10 +204,7 @@ export class TalerMerchantInstanceHttpClient { resp, codecForTalerMerchantConfigResponse(), ); - return { - type: "ok", - body, - }; + return opFixedSuccess(body); } case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); diff --git a/packages/taler-util/src/operation.ts b/packages/taler-util/src/operation.ts @@ -31,14 +31,10 @@ import { TalerErrorDetail, } from "./index.js"; -// type OperationFailWithBodyOrNever<ErrorEnum, ErrorMap> = -// ErrorEnum extends keyof ErrorMap ? OperationFailWithBody<ErrorMap> : never; - export type OperationResult<Body, ErrorEnum, K = never> = | OperationOk<Body> | OperationAlternative<ErrorEnum, any> | OperationFail<ErrorEnum>; -// | OperationFailWithBodyOrNever<ErrorEnum, K>; export function isOperationOk<T, E>( c: OperationResult<T, E>, @@ -58,6 +54,8 @@ export function isOperationFail<T, E>( export interface OperationOk<BodyT> { type: "ok"; + case: "ok"; + /** * Parsed response body. */ @@ -84,23 +82,21 @@ export interface OperationFail<T> { export interface OperationAlternative<T, B> { type: "fail"; + /** + * Either a HTTP status code or Taler error code to distinguish + * the response type. + */ case: T; + body: B; } -// export interface OperationFailWithBody<B> { -// type: "fail"; - -// case: keyof B; -// body: B[OperationFailWithBody<B>["case"]]; -// } - export async function opSuccessFromHttp<T>( resp: HttpResponse, codec: Codec<T>, ): Promise<OperationOk<T>> { const body = await readSuccessResponseJsonOrThrow(resp, codec); - return { type: "ok" as const, body }; + return { type: "ok" as const, case: "ok", body }; } /** @@ -108,11 +104,11 @@ export async function opSuccessFromHttp<T>( * to the client. */ export function opFixedSuccess<T>(body: T): OperationOk<T> { - return { type: "ok" as const, body }; + return { type: "ok" as const, case: "ok", body }; } export function opEmptySuccess(resp: HttpResponse): OperationOk<void> { - return { type: "ok" as const, body: void 0 }; + return { type: "ok" as const, case: "ok", body: void 0 }; } export function opKnownFailure<T>(case_: T): OperationFail<T> { @@ -179,9 +175,9 @@ export function succeedOrThrow<R>(resp: OperationResult<R, unknown>): R { export function alternativeOrThrow<Error, Body, Alt>( resp: - | OperationOk<Body> - | OperationAlternative<Error, Alt> - | OperationFail<Error>, + | OperationOk<Body> + | OperationAlternative<Error, Alt> + | OperationFail<Error>, s: Error, ): Promise<Alt> { if (isOperationOk(resp)) { diff --git a/packages/taler-util/src/types-taler-exchange.ts b/packages/taler-util/src/types-taler-exchange.ts @@ -1237,6 +1237,37 @@ export const codecForExchangeGetContractResponse = .build("ExchangeGetContractResponse"); /** + * Doc name: api-exchange/MergeSuccess. + */ +export interface ExchangeMergeSuccessResponse { + // Amount merged (excluding deposit fees). + merge_amount: Amount; + + // Time at which the merge came into effect. + // Maximum of the "payment_timestamp" and the + // "merge_timestamp". + exchange_timestamp: Timestamp; + + // EdDSA signature of the exchange affirming the merge of + // purpose TALER_SIGNATURE_PURSE_MERGE_SUCCESS + // over TALER_PurseMergeSuccessSignaturePS. + // Signs over the above and the account public key. + exchange_sig: EddsaSignatureString; + + // public key used to create the signature. + exchange_pub: EddsaPublicKeyString; +} + +export const codecForExchangeMergeSuccessResponse = + (): Codec<ExchangeMergeSuccessResponse> => + buildCodecForObject<ExchangeMergeSuccessResponse>() + .property("merge_amount", codecForAmountString()) + .property("exchange_timestamp", codecForTimestamp) + .property("exchange_sig", codecForString()) + .property("exchange_pub", codecForString()) + .build("ExchangeMergeSuccessResponse"); + +/** * Contract terms between two wallets (as opposed to a merchant and wallet). */ export interface PeerContractTerms { diff --git a/packages/taler-wallet-core/src/common.ts b/packages/taler-wallet-core/src/common.ts @@ -44,6 +44,7 @@ import { checkLogicInvariant, durationMul, } from "@gnu-taler/taler-util"; +import { HttpRequestOptions, HttpResponse } from "@gnu-taler/taler-util/http"; import { BackupProviderRecord, CoinHistoryRecord, @@ -69,7 +70,6 @@ import { ReadyExchangeSummary } from "./exchanges.js"; import { createRefreshGroup } from "./refresh.js"; import { BalanceEffect } from "./transactions.js"; import { WalletExecutionContext, getDenomInfo } from "./wallet.js"; -import { HttpRequestOptions, HttpResponse } from "@gnu-taler/taler-util/http"; const logger = new Logger("operations/common.ts"); @@ -84,7 +84,7 @@ export interface CoinsSpendInfo { } export interface TokensSpendInfo { - tokenPubs: string[], + tokenPubs: string[]; transactionId: TransactionIdStr; } @@ -261,12 +261,7 @@ export async function spendCoins( } export async function spendTokens( - tx: WalletDbReadWriteTransaction< - [ - "tokens", - "purchases", - ] - >, + tx: WalletDbReadWriteTransaction<["tokens", "purchases"]>, tsi: TokensSpendInfo, ): Promise<void> { if (tsi.tokenPubs.length === 0) { @@ -374,9 +369,9 @@ export function getExchangeState(r: ExchangeEntryRecord): ExchangeEntryState { export type ParsedTombstone = | { - tag: TombstoneTag.DeleteWithdrawalGroup; - withdrawalGroupId: string; - } + tag: TombstoneTag.DeleteWithdrawalGroup; + withdrawalGroupId: string; + } | { tag: TombstoneTag.DeleteRefund; refundGroupId: string } | { tag: TombstoneTag.DeleteReserve; reservePub: string } | { tag: TombstoneTag.DeleteRefreshGroup; refreshGroupId: string } @@ -657,9 +652,9 @@ export enum PendingTaskType { */ export type ParsedTaskIdentifier = | { - tag: PendingTaskType.Withdraw; - withdrawalGroupId: string; - } + tag: PendingTaskType.Withdraw; + withdrawalGroupId: string; + } | { tag: PendingTaskType.ExchangeUpdate; exchangeBaseUrl: string } | { tag: PendingTaskType.ExchangeWalletKyc; exchangeBaseUrl: string } | { tag: PendingTaskType.Backup; backupProviderBaseUrl: string } @@ -813,10 +808,10 @@ export enum TransitionResultType { export type TransitionResult<R> = | { type: TransitionResultType.Stay } | { - type: TransitionResultType.Transition; - rec: R; - balanceEffect: BalanceEffect; - } + type: TransitionResultType.Transition; + rec: R; + balanceEffect: BalanceEffect; + } | { type: TransitionResultType.Delete }; export const TransitionResult = { @@ -1037,16 +1032,16 @@ export async function runWithClientCancellation<R, T>( /** * Run a queued longpool fetch with cancellation token and timeout_ms */ -export async function cancelableLongPool( +export async function cancelableLongPoll( wex: WalletExecutionContext, url: URL, - opt?: HttpRequestOptions + opt?: HttpRequestOptions, ): Promise<HttpResponse> { - const longPool = async (timeoutMs: number) => { + const longPoll = async (timeoutMs: number) => { url.searchParams.set("timeout_ms", `${timeoutMs}`); - return cancelableFetch(wex, url, opt) + return cancelableFetch(wex, url, opt); }; - return wex.ws.longpollQueue.queue(wex.cancellationToken, url, longPool) + return wex.ws.longpollQueue.queue(wex.cancellationToken, url, longPoll); } /** @@ -1055,10 +1050,10 @@ export async function cancelableLongPool( export async function cancelableFetch( wex: WalletExecutionContext, url: URL, - opt?: HttpRequestOptions + opt?: HttpRequestOptions, ): Promise<HttpResponse> { return wex.http.fetch(url.href, { ...opt, cancellationToken: wex.cancellationToken, }); -} -\ No newline at end of file +} diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts @@ -91,7 +91,7 @@ import { TaskRunResult, TransactionContext, cancelableFetch, - cancelableLongPool, + cancelableLongPoll, constructTaskIdentifier, runWithClientCancellation, spendCoins, @@ -1022,7 +1022,7 @@ async function processDepositGroupPendingKyc( kycInfo.exchangeBaseUrl, ); logger.info(`kyc url ${url.href}`); - const kycStatusRes = await cancelableLongPool(wex, url, { + const kycStatusRes = await cancelableLongPoll(wex, url, { headers: { ["Account-Owner-Signature"]: sigResp.sig, }, @@ -1104,7 +1104,7 @@ async function processDepositGroupPendingKycAuth( // lpt=1 => wait for the KYC auth transfer (access token available) url.searchParams.set("lpt", "1"); logger.info(`kyc url ${url.href}`); - const kycStatusRes = await cancelableLongPool(wex, url,{ + const kycStatusRes = await cancelableLongPoll(wex, url,{ headers: { ["Account-Owner-Signature"]: sigResp.sig, }, @@ -1820,7 +1820,7 @@ async function trackDeposit( url.searchParams.set("merchant_sig", sigResp.sig); // wait for the a 202 state where kyc_ok is false or a 200 OK response url.searchParams.set("lpt", `1`); - const httpResp = await cancelableLongPool(wex, url); + const httpResp = await cancelableLongPoll(wex, url); logger.trace(`deposits response status: ${httpResp.status}`); switch (httpResp.status) { case HttpStatusCode.Accepted: { diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts @@ -127,7 +127,7 @@ import { TaskRunResultType, TransactionContext, cancelableFetch, - cancelableLongPool, + cancelableLongPoll, computeDbBackoff, constructTaskIdentifier, genericWaitForState, @@ -3442,7 +3442,7 @@ async function handleExchangeKycPendingWallet( reserve_sig: sigResp.sig, }; logger.info(`kyc-wallet request body: ${j2s(body)}`); - const res = await cancelableLongPool(wex, requestUrl, { + const res = await cancelableLongPoll(wex, requestUrl, { method: "POST", body, }); @@ -3662,7 +3662,7 @@ async function handleExchangeKycPendingLegitimization( const reqUrl = new URL(`kyc-check/${paytoHash}`, exchange.baseUrl); logger.info(`long-polling wallet KYC status at ${reqUrl.href}`); - const resp = await cancelableLongPool(wex, reqUrl, { + const resp = await cancelableLongPoll(wex, reqUrl, { headers: { ["Account-Owner-Signature"]: sigResp.sig, }, diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts @@ -139,7 +139,7 @@ import { TaskRunResultType, TransactionContext, TransitionResultType, - cancelableLongPool, + cancelableLongPoll, cancelableFetch, } from "./common.js"; import { EddsaKeyPairStrings } from "./crypto/cryptoImplementation.js"; @@ -3949,7 +3949,7 @@ async function checkIfOrderIsAlreadyPaid( let resp: HttpResponse; if (doLongPolling) { - resp = await cancelableLongPool(wex, requestUrl); + resp = await cancelableLongPoll(wex, requestUrl); } else { resp = await cancelableFetch(wex, requestUrl); } @@ -4116,7 +4116,7 @@ async function processPurchaseAutoRefund( requestUrl.searchParams.set("refund", Amounts.stringify(totalKnownRefund)); - const resp = await cancelableLongPool(wex, requestUrl); + const resp = await cancelableLongPoll(wex, requestUrl); // FIXME: Check other status codes! diff --git a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts @@ -67,7 +67,7 @@ import { TransactionContext, TransitionResultType, cancelableFetch, - cancelableLongPool, + cancelableLongPoll, constructTaskIdentifier, genericWaitForStateVal, requireExchangeTosAcceptedOrThrow, @@ -599,7 +599,7 @@ async function queryPurseForPeerPullCredit( pullIni.exchangeBaseUrl, ); logger.info(`querying purse status via ${purseDepositUrl.href}`); - const resp = await cancelableLongPool(wex, purseDepositUrl, { + const resp = await cancelableLongPoll(wex, purseDepositUrl, { timeout: { d_ms: 60000 }, }); const ctx = new PeerPullCreditTransactionContext(wex, pullIni.pursePub); @@ -679,7 +679,7 @@ async function longpollKycStatus( const ctx = new PeerPullCreditTransactionContext(wex, pursePub); const url = new URL(`kyc-check/${kycPaytoHash}`, exchangeUrl); logger.info(`kyc url ${url.href}`); - const kycStatusRes = await cancelableLongPool(wex, url, { + const kycStatusRes = await cancelableLongPoll(wex, url, { headers: { ["Account-Owner-Signature"]: sigResp.sig, } diff --git a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts @@ -78,7 +78,7 @@ import { TransactionContext, TransitionResultType, cancelableFetch, - cancelableLongPool, + cancelableLongPoll, constructTaskIdentifier, spendCoins, } from "./common.js"; @@ -527,7 +527,7 @@ async function processPeerPullDebitDialogProposed( pullIni.exchangeBaseUrl, ); logger.info(`querying purse status via ${purseDepositUrl.href}`); - const resp = await cancelableLongPool(wex, purseDepositUrl); + const resp = await cancelableLongPoll(wex, purseDepositUrl); const ctx = new PeerPullDebitTransactionContext(wex, pullIni.peerPullDebitId); logger.info(`purse status code: HTTP ${resp.status}`); diff --git a/packages/taler-wallet-core/src/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/pay-peer-push-credit.ts @@ -30,6 +30,7 @@ import { PreparePeerPushCreditRequest, PreparePeerPushCreditResponse, TalerErrorDetail, + TalerExchangeHttpClient, TalerPreciseTimestamp, TalerProtocolTimestamp, Transaction, @@ -44,9 +45,7 @@ import { assertUnreachable, checkDbInvariant, codecForAccountKycStatus, - codecForAny, codecForExchangeGetContractResponse, - codecForLegitimizationNeededResponse, codecForPeerContractTerms, decodeCrock, eddsaGetPublic, @@ -68,7 +67,7 @@ import { TransactionContext, TransitionResultType, cancelableFetch, - cancelableLongPool, + cancelableLongPoll, constructTaskIdentifier, genericWaitForStateVal, requireExchangeTosAcceptedOrThrow, @@ -141,26 +140,19 @@ export class PeerPushCreditTransactionContext implements TransactionContext { } /** - * Transition an existing peer-push-credit transaction. - * Extra object stores may be accessed during the transition. - */ + * Transition an existing peer-push-credit transaction. + * Extra object stores may be accessed during the transition. + */ async transition<StoreNameArray extends WalletDbStoresArr>( opts: { extraStores?: StoreNameArray }, f: ( rec: PeerPushPaymentIncomingRecord, tx: WalletDbReadWriteTransaction< - [ - "peerPushCredit", - "transactionsMeta", - ...StoreNameArray, - ] + ["peerPushCredit", "transactionsMeta", ...StoreNameArray] >, ) => Promise<TransitionResultType>, ): Promise<TransitionInfo | undefined> { - const baseStores = [ - "peerPushCredit" as const, - "transactionsMeta" as const - ]; + const baseStores = ["peerPushCredit" as const, "transactionsMeta" as const]; const stores = opts.extraStores ? [...baseStores, ...opts.extraStores] : baseStores; @@ -213,16 +205,13 @@ export class PeerPushCreditTransactionContext implements TransactionContext { /** * Transition an existing peer-push-credit transaction status */ - async transitionStatus( - from: PeerPushCreditStatus, - to: PeerPushCreditStatus - ) { + async transitionStatus(from: PeerPushCreditStatus, to: PeerPushCreditStatus) { await this.transition({}, async (rec) => { if (rec.status !== from) { - return TransitionResultType.Stay + return TransitionResultType.Stay; } else { rec.status = to; - return TransitionResultType.Transition + return TransitionResultType.Transition; } }); } @@ -328,7 +317,7 @@ export class PeerPushCreditTransactionContext implements TransactionContext { amountEffective: isUnsuccessfulTransaction(txState) ? Amounts.stringify(Amounts.zeroOfAmount(peerContractTerms.amount)) : // FIXME: This is wrong, needs to consider fees! - Amounts.stringify(peerContractTerms.amount), + Amounts.stringify(peerContractTerms.amount), amountRaw: Amounts.stringify(peerContractTerms.amount), exchangeBaseUrl: pushInc.exchangeBaseUrl, info: { @@ -361,7 +350,7 @@ export class PeerPushCreditTransactionContext implements TransactionContext { "transactionsMeta", ], }, - async (tx) => this.deleteTransactionInTx(tx) + async (tx) => this.deleteTransactionInTx(tx), ); for (const notif of res.notifs) { this.wex.ws.notify(notif); @@ -439,8 +428,8 @@ export class PeerPushCreditTransactionContext implements TransactionContext { default: assertUnreachable(rec.status); } - }) - this.wex.taskScheduler.stopShepherdTask(this.taskId) + }); + this.wex.taskScheduler.stopShepherdTask(this.taskId); } async abortTransaction(): Promise<void> { @@ -526,7 +515,7 @@ export class PeerPushCreditTransactionContext implements TransactionContext { case PeerPushCreditStatus.PendingBalanceKycInit: case PeerPushCreditStatus.SuspendedBalanceKycInit: rec.status = PeerPushCreditStatus.Failed; - rec.failReason = reason + rec.failReason = reason; return TransitionResultType.Transition; default: assertUnreachable(rec.status); @@ -736,7 +725,7 @@ async function longpollKycStatus( const url = new URL(`kyc-check/${kycPaytoHash}`, exchangeUrl); logger.info(`kyc url ${url.href}`); - const kycStatusRes = await cancelableLongPool(wex, url, { + const kycStatusRes = await cancelableLongPoll(wex, url, { headers: { ["Account-Owner-Signature"]: sigResp.sig, }, @@ -746,7 +735,10 @@ async function longpollKycStatus( kycStatusRes.status === HttpStatusCode.Ok || kycStatusRes.status === HttpStatusCode.NoContent ) { - await ctx.transitionStatus(PeerPushCreditStatus.PendingMergeKycRequired, PeerPushCreditStatus.PendingMerge); + await ctx.transitionStatus( + PeerPushCreditStatus.PendingMergeKycRequired, + PeerPushCreditStatus.PendingMerge, + ); return TaskRunResult.progress(); } else if (kycStatusRes.status === HttpStatusCode.Accepted) { // Access token / URL stays the same, just long-poll again. @@ -790,7 +782,7 @@ async function processPeerPushCreditKycRequired( const kycStatusRes = await cancelableFetch(wex, url, { headers: { ["Account-Owner-Signature"]: sigResp.sig, - } + }, }); logger.info(`kyc result status ${kycStatusRes.status}`); @@ -861,7 +853,10 @@ async function handlePendingMerge( amount: kycCheckRes.nextThreshold, exchangeBaseUrl: peerInc.exchangeBaseUrl, }); - await ctx.transitionStatus(PeerPushCreditStatus.PendingMerge, PeerPushCreditStatus.PendingBalanceKycInit); + await ctx.transitionStatus( + PeerPushCreditStatus.PendingMerge, + PeerPushCreditStatus.PendingBalanceKycInit, + ); return TaskRunResult.progress(); } @@ -896,9 +891,9 @@ async function handlePendingMerge( reservePriv: mergeReserveInfo.reservePriv, }); - const mergePurseUrl = new URL( - `purses/${peerInc.pursePub}/merge`, + const exchangeClient = new TalerExchangeHttpClient( peerInc.exchangeBaseUrl, + wex.http, ); const mergeReq: ExchangePurseMergeRequest = { @@ -908,28 +903,24 @@ async function handlePendingMerge( reserve_sig: sigRes.accountSig, }; - const mergeHttpResp = await cancelableFetch(wex, mergePurseUrl, { - method: "POST", + const mergeResp = await exchangeClient.postPurgeMerge({ + pursePub: peerInc.pursePub, + cancellationToken: wex.cancellationToken, body: mergeReq, }); - if (mergeHttpResp.status === HttpStatusCode.UnavailableForLegalReasons) { - const kycLegiNeededResp = await readResponseJsonOrThrow( - mergeHttpResp, - codecForLegitimizationNeededResponse(), - ); - logger.info( - `kyc legitimization needed response: ${j2s(kycLegiNeededResp)}`, - ); - return processPeerPushCreditKycRequired(wex, peerInc, kycLegiNeededResp); - } - logger.trace(`merge request: ${j2s(mergeReq)}`); - const res = await readSuccessResponseJsonOrThrow( - mergeHttpResp, - codecForAny(), - ); - logger.trace(`merge response: ${j2s(res)}`); + + switch (mergeResp.case) { + case "ok": + logger.trace(`merge response: ${j2s(mergeResp.body)}`); + break; + case HttpStatusCode.UnavailableForLegalReasons: { + const kycLegiNeededResp = mergeResp.body; + logger.info(`kyc legitimization needed response: ${j2s(mergeResp.body)}`); + return processPeerPushCreditKycRequired(wex, peerInc, kycLegiNeededResp); + } + } const withdrawalGroupPrep = await internalPrepareCreateWithdrawalGroup(wex, { amount, @@ -1073,15 +1064,21 @@ async function processPeerPushDebitDialogProposed( pullIni.exchangeBaseUrl, ); logger.info(`querying purse status via ${purseDepositUrl.href}`); - const resp = await cancelableLongPool(wex, purseDepositUrl); - const ctx = new PeerPushCreditTransactionContext(wex, pullIni.peerPushCreditId); + const resp = await cancelableLongPoll(wex, purseDepositUrl); + const ctx = new PeerPushCreditTransactionContext( + wex, + pullIni.peerPushCreditId, + ); logger.info(`purse status code: HTTP ${resp.status}`); switch (resp.status) { case HttpStatusCode.Gone: { // Exchange says that purse doesn't exist anymore => expired! - await ctx.transitionStatus(PeerPushCreditStatus.DialogProposed, PeerPushCreditStatus.Aborted); + await ctx.transitionStatus( + PeerPushCreditStatus.DialogProposed, + PeerPushCreditStatus.Aborted, + ); return TaskRunResult.finished(); } case HttpStatusCode.NotFound: @@ -1098,16 +1095,21 @@ async function processPeerPushDebitDialogProposed( const mergeTimestamp = result.merge_timestamp; - if (mergeTimestamp != null && !TalerProtocolTimestamp.isNever(mergeTimestamp)) { + if ( + mergeTimestamp != null && + !TalerProtocolTimestamp.isNever(mergeTimestamp) + ) { logger.info("purse completed by another wallet"); - await ctx.transitionStatus(PeerPushCreditStatus.DialogProposed, PeerPushCreditStatus.Aborted); + await ctx.transitionStatus( + PeerPushCreditStatus.DialogProposed, + PeerPushCreditStatus.Aborted, + ); return TaskRunResult.finished(); } return TaskRunResult.longpollReturnedPending(); } - export async function processPeerPushCredit( wex: WalletExecutionContext, peerPushCreditId: string, @@ -1121,17 +1123,17 @@ export async function processPeerPushCredit( { storeNames: ["contractTerms", "peerPushCredit", "transactionsMeta"] }, async (tx) => { const rec = await tx.peerPushCredit.get(peerPushCreditId); - let contractTerms = null + let contractTerms = null; if (rec != null) { - const contract = await tx.contractTerms.get(rec.contractTermsHash) + const contract = await tx.contractTerms.get(rec.contractTermsHash); if (contract != null) { - contractTerms = contract.contractTermsRaw + contractTerms = contract.contractTermsRaw; } } return { peerInc: rec, - contractTerms - } + contractTerms, + }; }, ); @@ -1223,7 +1225,10 @@ async function processPeerPushCreditBalanceKyc( }); if (ret.result === "ok") { - await ctx.transitionStatus(PeerPushCreditStatus.PendingBalanceKycRequired, PeerPushCreditStatus.PendingMerge); + await ctx.transitionStatus( + PeerPushCreditStatus.PendingBalanceKycRequired, + PeerPushCreditStatus.PendingMerge, + ); return TaskRunResult.progress(); } else if ( peerInc.status === PeerPushCreditStatus.PendingBalanceKycInit && @@ -1231,13 +1236,13 @@ async function processPeerPushCreditBalanceKyc( ) { await ctx.transition({}, async (rec) => { if (rec.status === PeerPushCreditStatus.PendingBalanceKycInit) { - rec.status = PeerPushCreditStatus.PendingBalanceKycRequired - rec.kycAccessToken = ret.walletKycAccessToken - return TransitionResultType.Transition + rec.status = PeerPushCreditStatus.PendingBalanceKycRequired; + rec.kycAccessToken = ret.walletKycAccessToken; + return TransitionResultType.Transition; } else { - return TransitionResultType.Stay + return TransitionResultType.Stay; } - }) + }); return TaskRunResult.progress(); } else { throw Error("not reached"); @@ -1295,7 +1300,10 @@ export async function confirmPeerPushCredit( throw Error("peer credit would exceed hard KYC limit"); } - await ctx.transitionStatus(PeerPushCreditStatus.DialogProposed, PeerPushCreditStatus.PendingMerge); + await ctx.transitionStatus( + PeerPushCreditStatus.DialogProposed, + PeerPushCreditStatus.PendingMerge, + ); wex.taskScheduler.stopShepherdTask(ctx.taskId); wex.taskScheduler.startShepherdTask(ctx.taskId); diff --git a/packages/taler-wallet-core/src/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/pay-peer-push-debit.ts @@ -70,7 +70,7 @@ import { TransactionContext, TransitionResultType, cancelableFetch, - cancelableLongPool, + cancelableLongPoll, constructTaskIdentifier, runWithClientCancellation, spendCoins, @@ -956,7 +956,7 @@ async function processPeerPushDebitReady( peerPushInitiation.exchangeBaseUrl, ); logger.info(`long-polling on purse status at ${mergeUrl.href}`); - const resp = await cancelableLongPool(wex, mergeUrl); + const resp = await cancelableLongPoll(wex, mergeUrl); if (resp.status === HttpStatusCode.Ok) { const purseStatus = await readSuccessResponseJsonOrThrow( resp, diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts @@ -121,7 +121,7 @@ import { TransitionResult, TransitionResultType, cancelableFetch, - cancelableLongPool, + cancelableLongPoll, constructTaskIdentifier, genericWaitForState, genericWaitForStateVal, @@ -1149,7 +1149,7 @@ async function processWithdrawalGroupDialogProposed( url.searchParams.set("old_state", "pending"); - const resp = await cancelableLongPool(ctx.wex, url); + const resp = await cancelableLongPoll(ctx.wex, url); switch (resp.status) { case HttpStatusCode.NotFound: { @@ -2147,7 +2147,7 @@ async function processQueryReserve( ); logger.trace(`querying reserve status via ${reserveUrl.href}`); - const resp = await cancelableLongPool(wex, reserveUrl, { + const resp = await cancelableLongPoll(wex, reserveUrl, { timeout: getReserveRequestTimeout(withdrawalGroup), }); @@ -2307,7 +2307,7 @@ async function processWithdrawalGroupPendingKyc( ); url.searchParams.set("lpt", "3"); // wait for the KYC status to be OK logger.info(`long-polling for withdrawal KYC status via ${url.href}`); - const kycStatusRes = await cancelableLongPool(wex, url, { + const kycStatusRes = await cancelableLongPoll(wex, url, { headers: { ["Account-Owner-Signature"]: sigResp.sig, }, diff --git a/packages/taler-wallet-webextension/src/wallet/QrReader.tsx b/packages/taler-wallet-webextension/src/wallet/QrReader.tsx @@ -553,7 +553,7 @@ async function checkExchangeUrl( return i18n.str`Couldn't found an exchange in the URL specified.`; } default: { - assertUnreachable(config.case); + assertUnreachable(config); } } } @@ -591,7 +591,7 @@ async function checkMerchantUrl( return i18n.str`Couldn't found an merchant in the URL specified.`; } default: { - assertUnreachable(config.case); + assertUnreachable(config); } } } @@ -629,7 +629,7 @@ async function checkBankUrl( return i18n.str`Couldn't found an bank in the URL specified.`; } default: { - assertUnreachable(config.case); + assertUnreachable(config); } } }