commit 644073c3c1c073cac22ab49e22ea96ba8598191f parent 2929d19cbbfd03fc88d351bddd76c7990450878e Author: Florian Dold <florian@dold.me> Date: Thu, 26 Feb 2026 20:08:02 +0100 result type refactoring Diffstat:
35 files changed, 1010 insertions(+), 817 deletions(-)
diff --git a/packages/aml-backoffice-ui/src/pages/Search.tsx b/packages/aml-backoffice-ui/src/pages/Search.tsx @@ -22,13 +22,14 @@ import { HostPortPath, HttpStatusCode, IbanString, - OperationOk, parseIban, ParseIbanError, PaytoParseError, Paytos, PaytoType, ReservePubParseError, + Result, + ResultError, TalerError, TranslatedString, } from "@gnu-taler/taler-util"; @@ -422,11 +423,13 @@ function WalletForm({ ); const pub = Paytos.parseReservePub(form.status.result.reservePub); - const paytoUri = - form.status.status === "fail" || pub.type === "fail" - ? undefined - : Paytos.createTalerReserve(form.status.result.exchange, pub.body); - + let paytoUri = undefined; + if (form.status.status !== "fail" && Result.isOk(pub)) { + paytoUri = Paytos.createTalerReserve( + form.status.result.exchange, + pub.value, + ); + } return ( <form class="space-y-6" @@ -465,13 +468,10 @@ function GenericForm({ {}, // createGenericPaytoValidator(i18n), ); - let p; - const paytoUri = - form.status.status === "fail" - ? undefined - : (p = Paytos.fromString(form.status.result.payto)).type === "fail" - ? undefined - : p.body; + let paytoUri: Paytos.URI | undefined; + if (form.status.status === "ok") { + paytoUri = Result.orUndefined(Paytos.fromString(form.status.result.payto)); + } return ( <form class="space-y-6" @@ -545,27 +545,23 @@ const paytoTypeField: ( }, ]; -type FailCasesOf<T extends (...args: any) => any> = Exclude< - ReturnType<T>, - OperationOk<any> ->; function translateReservePubError( - result: FailCasesOf<typeof Paytos.parseReservePub>, + result: ResultError<ReservePubParseError, { message: string } | undefined>, i18n: InternationalizationAPI, ): TranslatedString { - switch (result.case) { + switch (result.error) { case ReservePubParseError.WRONG_LENGTH: return i18n.str`Should be 52 characters.`; case ReservePubParseError.DECODE_ERROR: - return i18n.str`Failed to parse: ${result.body.message}`; + return i18n.str`Failed to parse: ${result.detail?.message}`; } } function translateBitcoinError( - result: FailCasesOf<typeof BitcoinBech32.decode>, + result: BitcoinBech32.BitcoinParseError, i18n: InternationalizationAPI, ): TranslatedString { - switch (result.case) { + switch (result) { case BitcoinBech32.BitcoinParseError.WRONG_CHARSET: return i18n.str`Address contains invalid characters.`; case BitcoinBech32.BitcoinParseError.MIXING_UPPER_AND_LOWER: @@ -582,10 +578,10 @@ function translateBitcoinError( } function translateIbanError( - result: FailCasesOf<typeof parseIban>, + error: ParseIbanError, i18n: InternationalizationAPI, ): TranslatedString { - switch (result.case) { + switch (error) { case ParseIbanError.UNSUPPORTED_COUNTRY: return i18n.str`Unsupported country.`; case ParseIbanError.TOO_LONG: @@ -599,11 +595,19 @@ function translateIbanError( } } -function translatePaytoError( - result: FailCasesOf<typeof Paytos.fromString>, +function translatePaytoErrorResult( + result: + | ResultError<PaytoParseError.WRONG_PREFIX> + | ResultError<PaytoParseError.UNSUPPORTED, { targetType: string }> + | ResultError<PaytoParseError.COMPONENTS_LENGTH, { targetType: PaytoType }> + | ResultError< + PaytoParseError.INVALID_TARGET_PATH, + Paytos.TargetPathErrorDetail + > + | ResultError<PaytoParseError.INCOMPLETE, { targetType: string }>, i18n: InternationalizationAPI, ): TranslatedString { - switch (result.case) { + switch (result.error) { case PaytoParseError.WRONG_PREFIX: return i18n.str`The string should start with payto://`; case PaytoParseError.INCOMPLETE: @@ -611,7 +615,8 @@ function translatePaytoError( case PaytoParseError.UNSUPPORTED: return i18n.str`The target type is not supported, only x-taler-bank, taler-reserve or iban`; case PaytoParseError.COMPONENTS_LENGTH: { - switch (result.body.targetType) { + const tt = result.detail.targetType; + switch (tt) { case PaytoType.IBAN: return i18n.str`IBAN is missing`; case PaytoType.Bitcoin: @@ -627,95 +632,100 @@ function translatePaytoError( case PaytoType.Cyclos: return i18n.str`Cyclos address is missing`; default: - assertUnreachable(result.body); + assertUnreachable(tt); } } case PaytoParseError.INVALID_TARGET_PATH: { - switch (result.body.targetType) { + switch (result.detail.targetType) { case PaytoType.IBAN: return i18n.str`Invalid IBAN: ${translateIbanError( - result.body.error, + result.detail.error.error, i18n, )}`; case PaytoType.Bitcoin: { - switch (result.body.pos) { + switch (result.detail.pos) { case 0: return i18n.str`Invalid BTC: ${translateBitcoinError( - result.body.error, + result.detail.error.error, i18n, )}`; case 1: return i18n.str`Invalid reserve: ${translateReservePubError( - result.body.error, + result.detail.error, i18n, )}`; default: - assertUnreachable(result.body); + assertUnreachable(result.detail); } } case PaytoType.TalerBank: { - switch (result.body.pos) { + switch (result.detail.pos) { case 0: return i18n.str`Invalid host`; case 1: return i18n.str`Invalid account`; default: - assertUnreachable(result.body); + assertUnreachable(result.detail); } } case PaytoType.TalerReserve: { - switch (result.body.pos) { + switch (result.detail.pos) { case 0: return i18n.str`Invalid host`; case 1: return i18n.str`Invalid reserve: ${translateReservePubError( - result.body.error, + result.detail.error, i18n, )}`; default: - assertUnreachable(result.body); + assertUnreachable(result.detail); } } case PaytoType.TalerReserveHttp: { - switch (result.body.pos) { + switch (result.detail.pos) { case 0: return i18n.str`Invalid host`; case 1: return i18n.str`Invalid reserve: ${translateReservePubError( - result.body.error, + result.detail.error, i18n, )}`; default: - assertUnreachable(result.body); + assertUnreachable(result.detail); } } case PaytoType.Ethereum: { - switch (result.body.pos) { + switch (result.detail.pos) { case 0: return i18n.str`Invalid address`; default: - assertUnreachable(result.body); + assertUnreachable(result.detail); } } case PaytoType.Cyclos: { - switch (result.body.pos) { + switch (result.detail.pos) { case 0: return i18n.str`Invalid host`; default: - assertUnreachable(result.body); + assertUnreachable(result.detail); } } default: - assertUnreachable(result.body); + assertUnreachable(result.detail); } } } } -function validatePayto(s: string, i18n: InternationalizationAPI) { +function validatePayto( + s: string, + i18n: InternationalizationAPI, +): TranslatedString | undefined { const result = Paytos.fromString(s); - if (result.type === "ok") return undefined; - return translatePaytoError(result, i18n); + if (Result.isError(result)) { + return translatePaytoErrorResult(result, i18n); + } + return undefined; } const genericFields: ( @@ -733,6 +743,7 @@ const genericFields: ( }, }, ]; + const ibanFields: (i18n: InternationalizationAPI) => UIFormElementConfig[] = ( i18n, ) => [ @@ -779,7 +790,7 @@ const walletFields: (i18n: InternationalizationAPI) => UIFormElementConfig[] = ( ? i18n.str`Required` : value.length !== 52 ? i18n.str`Should be 52 characters` - : Paytos.parseReservePub(value).type === "fail" + : Paytos.parseReservePub(value).tag === "error" ? i18n.str`Invalid value` : undefined; }, @@ -823,8 +834,8 @@ function validateIBAN( return i18n.str`Required`; } const result = parseIban(iban); - if (result.type === "ok") { + if (result.tag === "ok") { return undefined; } - return translateIbanError(result, i18n); + return translateIbanError(result.error, i18n); } diff --git a/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx b/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx @@ -17,11 +17,9 @@ import { AbsoluteTime, AmlDecisionRequest, assertUnreachable, - encodeCrock, HttpStatusCode, opEmptySuccess, Paytos, - succeedOrThrow, TalerError, TOPS_AmlEventsName, } from "@gnu-taler/taler-util"; @@ -130,8 +128,8 @@ export function Summary({ } const fullPayto = !newPayto ? undefined : Paytos.fromString(newPayto); - if (fullPayto && fullPayto.type === "ok" && decision.accountName) { - fullPayto.body.params["receiver-name"] = decision.accountName; + if (fullPayto && fullPayto.tag === "ok" && decision.accountName) { + fullPayto.value.params["receiver-name"] = decision.accountName; } const request: undefined | Omit<AmlDecisionRequest, "officer_sig"> = @@ -142,9 +140,9 @@ export function Summary({ decision_time: AbsoluteTime.toProtocolTimestamp(AbsoluteTime.now()), justification: decision.justification!, payto_uri: - !fullPayto || fullPayto.type === "fail" + !fullPayto || fullPayto.tag === "error" ? undefined - : Paytos.toFullString(fullPayto.body), + : Paytos.toFullString(fullPayto.value), keep_investigating: decision.keep_investigating ?? false, new_rules: { expiration_time: AbsoluteTime.toProtocolTimestamp( diff --git a/packages/bank-ui/src/components/Transactions/state.ts b/packages/bank-ui/src/components/Transactions/state.ts @@ -18,7 +18,8 @@ import { AbsoluteTime, Amounts, Paytos, - TalerError + Result, + TalerError, } from "@gnu-taler/taler-util"; import { useTransactions } from "../../hooks/account.js"; import { Props, State, Transaction } from "./index.js"; @@ -53,10 +54,7 @@ export function useComponentState({ const cp = Paytos.fromString( negative ? tx.creditor_payto_uri : tx.debtor_payto_uri, ); - const counterpart = - cp === undefined || cp.type === "fail" - ? undefined - : cp.body.displayName; + const counterpart = Result.orUndefined(cp)?.displayName; const when = AbsoluteTime.fromProtocolTimestamp(tx.date); const amount = Amounts.parse(tx.amount); diff --git a/packages/bank-ui/src/pages/AccountPage/state.ts b/packages/bank-ui/src/pages/AccountPage/state.ts @@ -83,9 +83,10 @@ export function useComponentState({ const payto = Paytos.fromString(data.payto_uri); if ( - payto.type === "fail" || - !payto.body.targetType || - (payto.body.targetType !== PaytoType.IBAN && payto.body.targetType !== PaytoType.TalerBank) + payto.tag === "error" || + !payto.value.targetType || + (payto.value.targetType !== PaytoType.IBAN && + payto.value.targetType !== PaytoType.TalerBank) ) { return { status: "invalid-iban", @@ -94,8 +95,9 @@ export function useComponentState({ } const balanceIsDebit = data.balance.credit_debit_indicator == "debit"; - const limit = IntAmounts.toIntAmount(balance, balanceIsDebit) - .increment(debitThreshold).result; + const limit = IntAmounts.toIntAmount(balance, balanceIsDebit).increment( + debitThreshold, + ).result; const positiveBalance = balanceIsDebit ? Amounts.zeroOfAmount(balance) diff --git a/packages/bank-ui/src/pages/OperationState/state.ts b/packages/bank-ui/src/pages/OperationState/state.ts @@ -197,7 +197,7 @@ export function useComponentState({ ? undefined : Paytos.fromString(data.selected_exchange_account); - if (!account || account.type === "fail" || !account.body.targetType) { + if (!account || account.tag === "error" || !account.value.targetType) { return { status: "invalid-payto", error: undefined, @@ -209,7 +209,7 @@ export function useComponentState({ status: "need-confirmation", error: undefined, details: { - account: account.body, + account: account.value, reserve: data.selected_reserve_pub, username: data.username, amount: !data.amount ? undefined : Amounts.parse(data.amount), diff --git a/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx @@ -26,7 +26,7 @@ import { Paytos, TalerErrorCode, TranslatedString, - assertUnreachable + assertUnreachable, } from "@gnu-taler/taler-util"; import { ButtonBetter, @@ -39,7 +39,7 @@ import { useBankCoreApiContext, useChallengeHandler, useLocalNotificationBetter, - useTranslationContext + useTranslationContext, } from "@gnu-taler/web-util/browser"; import { ComponentChildren, Fragment, Ref, VNode, h } from "preact"; import { useState } from "preact/hooks"; @@ -136,10 +136,10 @@ export function PaytoWireTransferForm({ const errorsPayto = undefinedIfEmpty({ rawPaytoInput: !rawPaytoInput ? i18n.str`Required` - : !parsed || parsed.type === "fail" + : !parsed || parsed.tag === "error" ? i18n.str`Does not follow the pattern` : validateRawPayto( - parsed.body, + parsed.value, limitWithFee, url.host, i18n, @@ -149,15 +149,15 @@ export function PaytoWireTransferForm({ let parsedURI: Paytos.URI | undefined; let sendingAmount: AmountString | undefined; - + if (isRawPayto) { const res = Paytos.fromString(rawPaytoInput!); - if (res && res.type === "ok") { - parsedURI = res.body; + if (res && res.tag === "ok") { + parsedURI = res.value; sendingAmount = parsedURI.params.amount as AmountString; delete parsedURI.params.amount; // we don't want to send twice in the request - } + } } else if (account && subject) { switch (paytoType) { case "x-taler-bank": { @@ -178,14 +178,23 @@ export function PaytoWireTransferForm({ } const sAmount = sendingAmount; - const send = safeFunctionHandler(i18n.str`send transaction`, + const send = safeFunctionHandler( + i18n.str`send transaction`, ( creds: LoggedIn, amount: AmountString, uri: Paytos.URI, challengeIds: string[], - ) => api.createTransaction(creds, {payto_uri: Paytos.toFullString(uri), amount}, { challengeIds }), - (isRawPayto ? !!errorsPayto : !!errorsWire) || !sAmount || !parsedURI || credentials.status !== "loggedIn" + ) => + api.createTransaction( + creds, + { payto_uri: Paytos.toFullString(uri), amount }, + { challengeIds }, + ), + (isRawPayto ? !!errorsPayto : !!errorsWire) || + !sAmount || + !parsedURI || + credentials.status !== "loggedIn" ? undefined : [credentials, sAmount, parsedURI, []], ); @@ -226,10 +235,10 @@ export function PaytoWireTransferForm({ assertUnreachable(fail); } }; - const repeatSend = send.lambda((ids:string[]) => { - return [send.args![0],send.args![1],send.args![2], ids] - }) - + const repeatSend = send.lambda((ids: string[]) => { + return [send.args![0], send.args![1], send.args![2], ids]; + }); + if (mfa.pendingChallenge) { return ( <SolveMFAChallenges @@ -258,8 +267,8 @@ export function PaytoWireTransferForm({ type="radio" name="input-type" onChange={() => { - if (parsed && parsed.type === "ok") { - switch (parsed.body.targetType) { + if (parsed && parsed.tag === "ok") { + switch (parsed.value.targetType) { case PaytoType.Ethereum: case PaytoType.Bitcoin: case undefined: @@ -269,33 +278,33 @@ export function PaytoWireTransferForm({ break; } case PaytoType.IBAN: { - setAccount(parsed.body.iban); + setAccount(parsed.value.iban); break; } case PaytoType.TalerBank: { - setAccount(parsed.body.account); + setAccount(parsed.value.account); break; } case PaytoType.Cyclos: { - setAccount(parsed.body.account); + setAccount(parsed.value.account); break; } default: { - assertUnreachable(parsed.body); + assertUnreachable(parsed.value); } } - const amountStr = !parsed.body.params + const amountStr = !parsed.value.params ? undefined - : parsed.body.params["amount"]; + : parsed.value.params["amount"]; if (amountStr) { const amount = Amounts.parse(amountStr); if (amount) { setAmount(Amounts.stringifyValue(amount)); } } - const subject = !parsed.body.params["message"] - ? parsed.body.params["subject"] - : parsed.body.params["message"]; + const subject = !parsed.value.params["message"] + ? parsed.value.params["subject"] + : parsed.value.params["message"]; if (subject) { setSubject(subject); } diff --git a/packages/bank-ui/src/pages/WalletWithdrawForm.tsx b/packages/bank-ui/src/pages/WalletWithdrawForm.tsx @@ -55,45 +55,6 @@ const TALER_SCREEN_ID = 112; const RefAmount = forwardRef(InputAmount); -function ThereIsAnOperationWarning({ - wopid, - onClose, - focus, - routeOperationDetails, -}: { - focus?: boolean; - wopid: string; - onClose: () => void; - routeOperationDetails: RouteDefinition<{ wopid: string }>; -}): VNode { - const { i18n } = useTranslationContext(); - const url = routeOperationDetails.url({ wopid }); - - return ( - <Attention - type="warning" - title={i18n.str`There is an operation already pending`} - onClose={onClose} - > - <span ref={focus ? doAutoFocus : undefined} /> - <i18n.Translate>Complete the operation in</i18n.Translate>{" "} - <a - class="font-semibold text-yellow-700 hover:text-yellow-600" - name="complete operation" - href={url} - // onClick={(e) => { - // e.preventDefault() - // walletInegrationApi.publishTalerAction(uri, () => { - // navigateTo(url) - // }) - // }} - > - <i18n.Translate>this page</i18n.Translate> - </a> - </Attention> - ); -} - function OldWithdrawalForm({ onOperationCreated, limit, @@ -157,7 +118,7 @@ function OldWithdrawalForm({ start.onSuccess = (success) => { const uri = TalerUris.fromString(success.taler_withdraw_uri); - if (uri.type === "fail" || uri.body.type !== TalerUriAction.Withdraw) { + if (uri.tag === "error" || uri.value.type !== TalerUriAction.Withdraw) { return notifyError( i18n.str`The server replied with an invalid taler://withdraw URI`, i18n.str`Withdraw URI: ${success.taler_withdraw_uri}`, @@ -165,9 +126,9 @@ function OldWithdrawalForm({ } else { updateBankState( "currentWithdrawalOperationId", - uri.body.withdrawalOperationId, + uri.value.withdrawalOperationId, ); - onOperationCreated(uri.body.withdrawalOperationId); + onOperationCreated(uri.value.withdrawalOperationId); } }; diff --git a/packages/bank-ui/src/pages/WithdrawalQRCode.tsx b/packages/bank-ui/src/pages/WithdrawalQRCode.tsx @@ -19,7 +19,7 @@ import { HttpStatusCode, TalerError, WithdrawUriResult, - assertUnreachable + assertUnreachable, } from "@gnu-taler/taler-util"; import { Attention, @@ -202,7 +202,7 @@ export function WithdrawalQRCode({ ? undefined : Paytos.fromString(data.selected_exchange_account); - if (!account || account.type === "fail") { + if (!account || account.tag === "error") { if (!data.selected_reserve_pub) { return ( <Attention @@ -247,7 +247,7 @@ export function WithdrawalQRCode({ withdrawUri={withdrawUri} details={{ username: data.username, - account: account.body, + account: account.value, reserve: data.selected_reserve_pub, amount: !data.amount ? undefined : Amounts.parseOrThrow(data.amount), }} diff --git a/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx b/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx @@ -40,8 +40,7 @@ import { useState } from "preact/hooks"; import { Paytos } from "@gnu-taler/taler-util"; import { useAccountDetails } from "../../hooks/account.js"; -import { usePreferences } from "../../hooks/preferences.js"; -import { LoggedIn, useSessionState } from "../../hooks/session.js"; +import { useSessionState } from "../../hooks/session.js"; import { AccountForm } from "../admin/AccountForm.js"; import { LoginForm } from "../LoginForm.js"; import { ProfileNavigation } from "../ProfileNavigation.js"; @@ -172,7 +171,8 @@ export function ShowAccountDetails({ revenueURL.username = account; revenueURL.password; const ac = Paytos.fromString(result.body.payto_uri); - const payto = ac.type === "fail" || !ac.body.targetType ? undefined : ac.body; + const payto = + ac.tag === "error" || !ac.value.targetType ? undefined : ac.value; if (mfa.pendingChallenge) { return ( diff --git a/packages/bank-ui/src/pages/admin/AccountForm.tsx b/packages/bank-ui/src/pages/admin/AccountForm.tsx @@ -19,6 +19,7 @@ import { HostPortPath, IbanString, PaytoString, + PaytoType, Paytos, TalerCorebankApi, assertUnreachable, @@ -45,7 +46,6 @@ import { doAutoFocus, } from "../PaytoWireTransferForm.js"; import { getRandomPassword } from "../rnd.js"; -import { PaytoType } from "@gnu-taler/taler-util"; const TALER_SCREEN_ID = 120; @@ -781,13 +781,18 @@ function getAccountId( type: "iban" | "x-taler-bank", s: Paytos.FullPaytoString | undefined, ): string | undefined { - if (s === undefined) return undefined; + if (s === undefined) { + return undefined; + } const p = Paytos.fromString(s); - if (p === undefined || p.type === "fail") return undefined; - if (!p.body.targetType === undefined) return "<unknown>"; - if (type === "iban" && p.body.targetType === PaytoType.IBAN) - return p.body.iban; - if (type === "x-taler-bank" && p.body.targetType === PaytoType.TalerBank) - return p.body.account; + if (p.tag === "error") { + return undefined; + } + if (type === "iban" && p.value.targetType === PaytoType.IBAN) { + return p.value.iban; + } + if (type === "x-taler-bank" && p.value.targetType === PaytoType.TalerBank) { + return p.value.account; + } return "<unsupported>"; } diff --git a/packages/bank-ui/src/pages/regional/CreateCashout.tsx b/packages/bank-ui/src/pages/regional/CreateCashout.tsx @@ -405,14 +405,14 @@ function CreateCashoutInternal({ ? undefined : Paytos.fromString(accountData.cashout_payto_uri); const cashoutAccountName = - !cashoutAccount || cashoutAccount.type === "fail" + !cashoutAccount || cashoutAccount.tag === "error" ? undefined - : cashoutAccount.body.displayName; + : cashoutAccount.value.displayName; const cashoutLegalName = - !cashoutAccount || cashoutAccount.type === "fail" + !cashoutAccount || cashoutAccount.tag === "error" ? undefined - : cashoutAccount.body.params["receiver-name"]; + : cashoutAccount.value.params["receiver-name"]; if (mfa.pendingChallenge) { return ( diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx @@ -22,8 +22,8 @@ import { AccessToken, HttpStatusCode, - PaytoParseError, Paytos, + Result, TalerMerchantApi, assertUnreachable, opEmptySuccess, @@ -32,7 +32,6 @@ import { ButtonBetterBulma, LocalNotificationBannerBulma, useChallengeHandler, - useCommonPreferences, useLocalNotificationBetter, useTranslationContext, } from "@gnu-taler/web-util/browser"; @@ -44,18 +43,18 @@ import { TalerForm, } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; +import { InputPassword } from "../../../../components/form/InputPassword.js"; import { InputPaytoForm } from "../../../../components/form/InputPaytoForm.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; import { InputToggle } from "../../../../components/form/InputToggle.js"; +import { FragmentPersonaFlag } from "../../../../components/menu/SideBar.js"; import { CompareAccountsModal } from "../../../../components/modal/index.js"; import { SolveMFAChallenges } from "../../../../components/SolveMFA.js"; import { useSessionContext } from "../../../../context/session.js"; +import { UIElement, usePreference } from "../../../../hooks/preference.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; import { safeConvertURL } from "../update/UpdatePage.js"; import { TestRevenueErrorType, testRevenueAPI } from "./index.js"; -import { FragmentPersonaFlag } from "../../../../components/menu/SideBar.js"; -import { UIElement, usePreference } from "../../../../hooks/preference.js"; -import { InputPassword } from "../../../../components/form/InputPassword.js"; const TALER_SCREEN_ID = 33; @@ -90,9 +89,9 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { undefined, ); const parsed = !state.payto_uri - ? undefined + ? Result.error(undefined) : Paytos.fromString(state.payto_uri); - const safeParsed = parsed?.type === "fail" ? undefined : parsed?.body; + const safeParsed = Result.orUndefined(parsed); const errors = undefinedIfEmpty<FormErrors<Entity>>({ payto_uri: !state.payto_uri ? i18n.str`Required` : undefined, extra_wire_subject_metadata: !state.extra_wire_subject_metadata @@ -155,7 +154,7 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { payto_uri: state.payto_uri!, credit_facade_credentials, credit_facade_url, - extra_wire_subject_metadata: state.extra_wire_subject_metadata + extra_wire_subject_metadata: state.extra_wire_subject_metadata, }; const [notification, safeFunctionHandler] = useLocalNotificationBetter(); @@ -222,11 +221,7 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { return i18n.str`Unauthorized, check credentials.`; case HttpStatusCode.NotFound: return i18n.str`The endpoint does not seem to be a Taler Revenue API.`; - case PaytoParseError.UNSUPPORTED: - case PaytoParseError.COMPONENTS_LENGTH: - case PaytoParseError.INVALID_TARGET_PATH: - case PaytoParseError.WRONG_PREFIX: - case PaytoParseError.INCOMPLETE: + case TestRevenueErrorType.BAD_PAYTO: return i18n.str`Unsupported type of account`; default: assertUnreachable(fail); @@ -329,7 +324,8 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { <ButtonBetterBulma class="button is-info" type="button" - onTouchStart={() => {}} data-tooltip={i18n.str`Verify details with server`} + onTouchStart={() => {}} + data-tooltip={i18n.str`Verify details with server`} onClick={test} > <i18n.Translate>Test</i18n.Translate> @@ -344,7 +340,8 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { </button> )} <ButtonBetterBulma - onTouchStart={() => {}} data-tooltip={ + onTouchStart={() => {}} + data-tooltip={ hasErrors ? i18n.str`Please complete the marked fields` : i18n.str`Confirm operation` diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx @@ -23,15 +23,16 @@ import { AccessToken, BasicOrTokenAuth, FacadeCredentials, + HttpStatusCode, + OperationFail, + OperationOk, Paytos, TalerMerchantApi, TalerRevenueHttpClient, - opFixedSuccess + opFixedSuccess, } from "@gnu-taler/taler-util"; import { type HttpRequestLibrary } from "@gnu-taler/taler-util/http"; -import { - BrowserFetchHttpLib -} from "@gnu-taler/web-util/browser"; +import { BrowserFetchHttpLib } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { CreatePage } from "./CreatePage.js"; @@ -42,7 +43,6 @@ interface Props { } export default function CreateValidator({ onConfirm, onBack }: Props): VNode { - return ( <> <CreatePage onBack={onBack} onCreated={onConfirm} /> @@ -52,12 +52,22 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode { export enum TestRevenueErrorType { CANT_VALIDATE, + BAD_PAYTO, } export async function testRevenueAPI( revenueAPI: URL, creds: FacadeCredentials | undefined, -) { +): Promise< + | OperationOk<Paytos.URI> + | OperationFail<HttpStatusCode.Unauthorized> + | OperationFail<HttpStatusCode.NotFound> + | OperationFail<HttpStatusCode.BadRequest> + // FIXME: This is a misuse of the OperationFail type. + // Should be TalerErrorCode or HttpStatusCode. + | OperationFail<TestRevenueErrorType.BAD_PAYTO> + | OperationFail<TestRevenueErrorType.CANT_VALIDATE> +> { const httpLib: HttpRequestLibrary = new BrowserFetchHttpLib(); const api = new TalerRevenueHttpClient(revenueAPI.href, httpLib); const auth: BasicOrTokenAuth | undefined = @@ -97,10 +107,16 @@ export async function testRevenueAPI( detail: undefined, }; } - const str = resp.body.credit_account as Paytos.FullPaytoString - const uri = Paytos.fromString(str) - if (uri.type === "fail") return uri - return opFixedSuccess(uri.body); + const str = resp.body.credit_account as Paytos.FullPaytoString; + const uri = Paytos.fromString(str); + if (uri.tag === "error") { + return { + type: "fail" as const, + case: TestRevenueErrorType.BAD_PAYTO, + detail: undefined, + }; + } + return opFixedSuccess(uri.value); } catch (err) { // FIXME: should we return some other error code here? throw err; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx @@ -26,7 +26,7 @@ import { Paytos, PaytoType, PaytoUri, - succeedOrValue, + Result, TalerMerchantApi, } from "@gnu-taler/taler-util"; import { @@ -87,7 +87,7 @@ export function CardTable({ accounts, onCreate, onSelect }: Props): VNode { <ConfirmModal label={i18n.str`Delete account`} description={i18n.str`Delete the account "${ - succeedOrValue(Paytos.fromString(deleting.payto_uri), { + Result.orElse(Paytos.fromString(deleting.payto_uri), { displayName: i18n.str`Invalid payto: "${deleting.payto_uri}"`, }).displayName }"`} @@ -102,7 +102,7 @@ export function CardTable({ accounts, onCreate, onSelect }: Props): VNode { <b> " { - succeedOrValue(Paytos.fromString(deleting.payto_uri), { + Result.orElse(Paytos.fromString(deleting.payto_uri), { displayName: i18n.str`Invalid payto: "${deleting.payto_uri}"`, }).displayName } @@ -130,7 +130,8 @@ export function CardTable({ accounts, onCreate, onSelect }: Props): VNode { <div class="card-header-icon" aria-label="more options"> <span class="has-tooltip-left" - onTouchStart={() => {}} data-tooltip={i18n.str`Add new account`} + onTouchStart={() => {}} + data-tooltip={i18n.str`Add new account`} > <button class="button is-info" @@ -246,7 +247,7 @@ function Table({ accounts, onDelete, onSelect }: TableProps): VNode { style={{ cursor: "pointer" }} > { - succeedOrValue(Paytos.fromString(acc.payto_uri), { + Result.orElse(Paytos.fromString(acc.payto_uri), { displayName: i18n.str`Invalid payto: "${acc.payto_uri}"`, }).displayName } @@ -261,7 +262,8 @@ function Table({ accounts, onDelete, onSelect }: TableProps): VNode { <div class="buttons is-right"> <button class="button is-danger is-small has-tooltip-left" - onTouchStart={() => {}} data-tooltip={i18n.str`Delete selected accounts from the database`} + onTouchStart={() => {}} + data-tooltip={i18n.str`Delete selected accounts from the database`} onClick={() => onDelete(acc)} > <i18n.Translate>Delete</i18n.Translate> @@ -285,7 +287,7 @@ function Table({ accounts, onDelete, onSelect }: TableProps): VNode { style={{ cursor: "pointer" }} > { - succeedOrValue(Paytos.fromString(acc.payto_uri), { + Result.orElse(Paytos.fromString(acc.payto_uri), { displayName: i18n.str`Invalid payto: "${acc.payto_uri}"`, }).displayName } @@ -300,7 +302,8 @@ function Table({ accounts, onDelete, onSelect }: TableProps): VNode { <div class="buttons is-right"> <button class="button is-danger is-small has-tooltip-left" - onTouchStart={() => {}} data-tooltip={i18n.str`Delete selected accounts from the database`} + onTouchStart={() => {}} + data-tooltip={i18n.str`Delete selected accounts from the database`} onClick={() => onDelete(acc)} > <i18n.Translate>Delete</i18n.Translate> @@ -324,7 +327,7 @@ function Table({ accounts, onDelete, onSelect }: TableProps): VNode { style={{ cursor: "pointer" }} > { - succeedOrValue(Paytos.fromString(acc.payto_uri), { + Result.orElse(Paytos.fromString(acc.payto_uri), { displayName: i18n.str`Invalid payto: "${acc.payto_uri}"`, }).displayName } @@ -339,7 +342,8 @@ function Table({ accounts, onDelete, onSelect }: TableProps): VNode { <div class="buttons is-right"> <button class="button is-danger is-small has-tooltip-left" - onTouchStart={() => {}} data-tooltip={i18n.str`Delete selected accounts from the database`} + onTouchStart={() => {}} + data-tooltip={i18n.str`Delete selected accounts from the database`} onClick={() => onDelete(acc)} > <i18n.Translate>Delete</i18n.Translate> @@ -363,7 +367,7 @@ function Table({ accounts, onDelete, onSelect }: TableProps): VNode { style={{ cursor: "pointer" }} > { - succeedOrValue(Paytos.fromString(acc.payto_uri), { + Result.orElse(Paytos.fromString(acc.payto_uri), { displayName: i18n.str`Invalid payto: "${acc.payto_uri}"`, }).displayName } @@ -378,7 +382,8 @@ function Table({ accounts, onDelete, onSelect }: TableProps): VNode { <div class="buttons is-right"> <button class="button is-danger is-small has-tooltip-left" - onTouchStart={() => {}} data-tooltip={i18n.str`Delete selected accounts from the database`} + onTouchStart={() => {}} + data-tooltip={i18n.str`Delete selected accounts from the database`} onClick={() => onDelete(acc)} // onClick={() => onDelete(acc,)} > diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx @@ -22,13 +22,12 @@ import { AccessToken, HttpStatusCode, - PaytoParseError, PaytoString, Paytos, + Result, TalerMerchantApi, assertUnreachable, opEmptySuccess, - succeedOrThrow, } from "@gnu-taler/taler-util"; import { ButtonBetterBulma, @@ -98,7 +97,7 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode { const parsed = !state.payto_uri ? undefined : Paytos.fromString(state.payto_uri); - const safeParsed = parsed?.type === "fail" ? undefined : parsed?.body; + const safeParsed = parsed?.tag === "error" ? undefined : parsed?.value; const replacingAccountId = state.payto_uri !== account.payto_uri; @@ -288,11 +287,7 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode { return i18n.str`The endpoint does not seem to be a Taler Revenue API.`; case TestRevenueErrorType.CANT_VALIDATE: return i18n.str`The request was made correctly, but the bank's server did not respond with the appropriate value for 'credit_account', so we cannot confirm that it is the same bank account.`; - case PaytoParseError.UNSUPPORTED: - case PaytoParseError.COMPONENTS_LENGTH: - case PaytoParseError.INVALID_TARGET_PATH: - case PaytoParseError.WRONG_PREFIX: - case PaytoParseError.INCOMPLETE: + case TestRevenueErrorType.BAD_PAYTO: return i18n.str`Unsupported type of account`; default: assertUnreachable(fail); @@ -322,7 +317,7 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode { <i18n.Translate>Account:</i18n.Translate>{" "} <b> { - succeedOrThrow(Paytos.fromString(account.payto_uri)) + Result.unpack(Paytos.fromString(account.payto_uri)) .displayName } </b> @@ -420,7 +415,8 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode { <ButtonBetterBulma type="button" class="button is-info" - onTouchStart={() => {}} data-tooltip={i18n.str`Compare info from server with account form`} + onTouchStart={() => {}} + data-tooltip={i18n.str`Compare info from server with account form`} onClick={test} > <i18n.Translate>Test</i18n.Translate> @@ -437,7 +433,8 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode { </button> )} <ButtonBetterBulma - onTouchStart={() => {}} data-tooltip={ + onTouchStart={() => {}} + data-tooltip={ hasErrors ? i18n.str`Please complete the marked fields` : i18n.str`Confirm operation` diff --git a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx @@ -26,7 +26,10 @@ import { TalerMerchantApi, assertUnreachable, } from "@gnu-taler/taler-util"; -import { useCommonPreferences, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + useCommonPreferences, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; @@ -35,9 +38,8 @@ import { ConfirmModal, ValidBankAccount, } from "../../../../components/modal/index.js"; -import { ListPage } from "./ListPage.js"; import { useInstanceKYCDetails } from "../../../../hooks/instance.js"; -import { usePreference } from "../../../../hooks/preference.js"; +import { ListPage } from "./ListPage.js"; const TALER_SCREEN_ID = 40; @@ -117,7 +119,7 @@ function ShowInstructionForKycRedirect({ switch (e.status) { case TalerMerchantApi.MerchantAccountKycStatus.KYC_WIRE_REQUIRED: const uri = Paytos.fromString(e.payto_uri); - if (uri.type === "fail") { + if (uri.tag === "error") { return ( <ConfirmModal label={i18n.str`Ok`} @@ -142,15 +144,19 @@ function ShowInstructionForKycRedirect({ const tgs = (e.payto_kycauths ?? []) .map((d) => Paytos.fromString(d)) .filter((d) => { - if (d.type !== "ok") { + if (d.tag !== "ok") { // FIXME: maybe this need to be shown to the user more promptly console.error("The server replied with a wrong payto:// URI", d); } - return d.type === "ok"; + return d.tag === "ok"; }) - .map((d) => d.body); + .map((d) => d.value); return ( - <ValidBankAccount origin={uri.body} targets={tgs} onCancel={onCancel} /> + <ValidBankAccount + origin={uri.value} + targets={tgs} + onCancel={onCancel} + /> ); case TalerMerchantApi.MerchantAccountKycStatus.NO_EXCHANGE_KEY: { return ( diff --git a/packages/taler-util/src/amounts.ts b/packages/taler-util/src/amounts.ts @@ -163,7 +163,7 @@ export function codecForAmountString(): Codec<AmountString> { /** * Result of a possibly overflowing operation. */ -export interface Result { +export interface AmountResult { /** * Resulting, possibly saturated amount. */ @@ -279,7 +279,7 @@ export class Amounts { }; } - static sum(amounts: AmountLike[]): Result { + static sum(amounts: AmountLike[]): AmountResult { if (amounts.length <= 0) { throw Error("can't sum zero amounts"); } @@ -287,7 +287,7 @@ export class Amounts { return Amounts.add(jsonAmounts[0], ...jsonAmounts.slice(1)); } - static sumOrZero(currency: string, amounts: AmountLike[]): Result { + static sumOrZero(currency: string, amounts: AmountLike[]): AmountResult { if (amounts.length <= 0) { return { amount: Amounts.zeroOfCurrency(currency), @@ -305,7 +305,7 @@ export class Amounts { * * Throws when currencies don't match. */ - static add(first: AmountLike, ...rest: AmountLike[]): Result { + static add(first: AmountLike, ...rest: AmountLike[]): AmountResult { const firstJ = Amounts.jsonifyAmount(first); const currency = firstJ.currency; let value = @@ -353,7 +353,7 @@ export class Amounts { * * Throws when currencies don't match. */ - static sub(a: AmountLike, ...rest: AmountLike[]): Result { + static sub(a: AmountLike, ...rest: AmountLike[]): AmountResult { const aJ = Amounts.jsonifyAmount(a); const currency = aJ.currency; let value = aJ.value; @@ -589,7 +589,7 @@ export class Amounts { } } - static mult(a: AmountLike, n: number): Result { + static mult(a: AmountLike, n: number): AmountResult { a = this.jsonifyAmount(a); if (!Number.isInteger(n)) { throw Error("amount can only be multiplied by an integer"); diff --git a/packages/taler-util/src/bech32.ts b/packages/taler-util/src/bech32.ts @@ -19,8 +19,8 @@ // THE SOFTWARE. import { assertUnreachable } from "./errors.js"; -import { opFixedSuccess, opKnownFailure } from "./operation.js"; import { BtAddrString } from "./payto.js"; +import { Result } from "./result.js"; var CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; var GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]; @@ -87,11 +87,10 @@ function createChecksum( } export namespace BitcoinBech32 { - export enum Encodings { BECH32 = "bech32", BECH32M = "bech32m", - }; + } export function encode( hrp: string, @@ -133,13 +132,19 @@ export namespace BitcoinBech32 { export function decode( bechString: string, enc?: Encodings, - ) { + ): Result< + { + hrp: string; + data: number[]; + }, + BitcoinParseError + > { let p; let has_lower = false; let has_upper = false; for (p = 0; p < bechString.length; ++p) { if (bechString.charCodeAt(p) < 33 || bechString.charCodeAt(p) > 126) { - return opKnownFailure(BitcoinParseError.WRONG_CHARSET); + return Result.error(BitcoinParseError.WRONG_CHARSET); } if (bechString.charCodeAt(p) >= 97 && bechString.charCodeAt(p) <= 122) { has_lower = true; @@ -149,31 +154,31 @@ export namespace BitcoinBech32 { } } if (has_lower && has_upper) { - return opKnownFailure(BitcoinParseError.MIXING_UPPER_AND_LOWER); + return Result.error(BitcoinParseError.MIXING_UPPER_AND_LOWER); } bechString = bechString.toLowerCase(); const pos = bechString.lastIndexOf("1"); if (pos < 1) { - return opKnownFailure(BitcoinParseError.MISSING_HRP); + return Result.error(BitcoinParseError.MISSING_HRP); } if (pos + 7 > bechString.length) { - return opKnownFailure(BitcoinParseError.TOO_SHORT); + return Result.error(BitcoinParseError.TOO_SHORT); } if (bechString.length > 90) { - return opKnownFailure(BitcoinParseError.TOO_LONG); + return Result.error(BitcoinParseError.TOO_LONG); } const hrp = bechString.substring(0, pos); var data: Array<number> = []; for (p = pos + 1; p < bechString.length; ++p) { var d = CHARSET.indexOf(bechString.charAt(p)); if (d === -1) { - return opKnownFailure(BitcoinParseError.WRONG_CHARSET); + return Result.error(BitcoinParseError.WRONG_CHARSET); } data.push(d); } if (enc && !verifyChecksum(hrp, data, enc)) { - return opKnownFailure(BitcoinParseError.WRONG_CHECKSUM); + return Result.error(BitcoinParseError.WRONG_CHECKSUM); } - return opFixedSuccess({ hrp, data: data.slice(0, data.length - 6) }); + return Result.of({ hrp, data: data.slice(0, data.length - 6) }); } } diff --git a/packages/taler-util/src/bitcoin.test.ts b/packages/taler-util/src/bitcoin.test.ts @@ -21,13 +21,15 @@ import test from "ava"; import { generateFakeSegwitAddress } from "./bitcoin.js"; import { Paytos } from "./payto.js"; -import { succeedOrThrow } from "./operation.js"; +import { Result } from "./result.js"; test("generate testnet", (t) => { - const [addr1, addr2] = succeedOrThrow( + const [addr1, addr2] = Result.unpack( generateFakeSegwitAddress( - succeedOrThrow( - Paytos.parseReservePub("B9E0EXNDKGJX7WFAEVZCZXM0R661T66YWD71N7NRFDEWQEV10XSG"), + Result.unpack( + Paytos.parseReservePub( + "B9E0EXNDKGJX7WFAEVZCZXM0R661T66YWD71N7NRFDEWQEV10XSG", + ), ), "tb1qhxrhccqexg0dv4nltgkuw4fg2ce7muplmjsn0v", ), @@ -38,10 +40,12 @@ test("generate testnet", (t) => { }); test("generate mainnet", (t) => { - const [addr1, addr2] = succeedOrThrow( + const [addr1, addr2] = Result.unpack( generateFakeSegwitAddress( - succeedOrThrow( - Paytos.parseReservePub("B9E0EXNDKGJX7WFAEVZCZXM0R661T66YWD71N7NRFDEWQEV10XSG"), + Result.unpack( + Paytos.parseReservePub( + "B9E0EXNDKGJX7WFAEVZCZXM0R661T66YWD71N7NRFDEWQEV10XSG", + ), ), "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", ), @@ -52,10 +56,12 @@ test("generate mainnet", (t) => { }); test("generate Regtest", (t) => { - const [addr1, addr2] = succeedOrThrow( + const [addr1, addr2] = Result.unpack( generateFakeSegwitAddress( - succeedOrThrow( - Paytos.parseReservePub("B9E0EXNDKGJX7WFAEVZCZXM0R661T66YWD71N7NRFDEWQEV10XSG"), + Result.unpack( + Paytos.parseReservePub( + "B9E0EXNDKGJX7WFAEVZCZXM0R661T66YWD71N7NRFDEWQEV10XSG", + ), ), "bcrtqhxrhccqexg0dv4nltgkuw4fg2ce7muplmjsn0v", ), @@ -67,9 +73,9 @@ test("generate Regtest", (t) => { test("unknown net", (t) => { t.throws(() => { - succeedOrThrow( + Result.unpack( generateFakeSegwitAddress( - succeedOrThrow( + Result.unpack( Paytos.parseReservePub( "B9E0EXNDKGJX7WFAEVZCZXM0R661T66YWD71N7NRFDEWQEV10XSG", ), @@ -84,7 +90,7 @@ test("invalid or no reserve", (t) => { let result = undefined; { const result = Paytos.parseReservePub(""); - t.assert(result.type === "fail"); + t.assert(!Result.isOk(result)); } // empty // result = generateFakeSegwitAddress( @@ -94,7 +100,7 @@ test("invalid or no reserve", (t) => { // t.deepEqual(result, []); { const result = Paytos.parseReservePub("s"); - t.assert(result.type === "fail"); + t.assert(!Result.isOk(result)); } // small // result = generateFakeSegwitAddress( @@ -104,7 +110,7 @@ test("invalid or no reserve", (t) => { // t.deepEqual(result, []); { const result = Paytos.parseReservePub("asdsad"); - t.assert(result.type === "fail"); + t.assert(!Result.isOk(result)); } // result = generateFakeSegwitAddress( @@ -114,7 +120,7 @@ test("invalid or no reserve", (t) => { // t.deepEqual(result, []); { const result = Paytos.parseReservePub("asdasdasdasdasdasd"); - t.assert(result.type === "fail"); + t.assert(!Result.isOk(result)); } // result = generateFakeSegwitAddress( @@ -126,7 +132,7 @@ test("invalid or no reserve", (t) => { const result = Paytos.parseReservePub( "B9E0EXNDKGJX7WFAEVZCZXM0R661T66YWD71N7NRFDEWQEV10XS", ); - t.assert(result.type === "fail"); + t.assert(!Result.isOk(result)); } // result = generateFakeSegwitAddress( @@ -138,7 +144,7 @@ test("invalid or no reserve", (t) => { const result = Paytos.parseReservePub( "B9E0EXNDKGJX7WFAEVZCZXM0R661T66YWD71N7NRFDEWQEV10XSSSS", ); - t.assert(result.type === "fail"); + t.assert(!Result.isOk(result)); } // result = generateFakeSegwitAddress( @@ -148,7 +154,7 @@ test("invalid or no reserve", (t) => { // t.deepEqual(result, []); { const result = Paytos.parseReservePub(undefined); - t.assert(result.type === "fail"); + t.assert(!Result.isOk(result)); } // no reserve @@ -158,8 +164,10 @@ test("invalid or no reserve", (t) => { // ); // t.deepEqual(result, []); { - const result = Paytos.parseReservePub("B9E0EXNDKGJX7WFAEVZCZXM0R661T66YWD71N7NRFDEWQEV10XS-"); - t.assert(result.type === "fail"); + const result = Paytos.parseReservePub( + "B9E0EXNDKGJX7WFAEVZCZXM0R661T66YWD71N7NRFDEWQEV10XS-", + ); + t.assert(!Result.isOk(result)); } // result = generateFakeSegwitAddress( diff --git a/packages/taler-util/src/bitcoin.ts b/packages/taler-util/src/bitcoin.ts @@ -23,20 +23,10 @@ * Imports. */ import { AmountJson, Amounts } from "./amounts.js"; -import { opFixedSuccess, opKnownFailure } from "./operation.js"; import { BtAddrString } from "./payto.js"; +import { Result } from "./result.js"; import { BitcoinSewgit } from "./segwit_addr.js"; -function buf2hex(buffer: Uint8Array) { - // buffer is an ArrayBuffer - return [...new Uint8Array(buffer)] - .map((x) => x.toString(16).padStart(2, "0")) - .join(""); -} - -const hext2buf = (hexString: string) => - new Uint8Array(hexString.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16))); - export enum GenerateSegwitAddrError { /** * The reserve pub used to generate the segwith is invalid @@ -55,7 +45,7 @@ export enum GenerateSegwitAddrError { export function generateFakeSegwitAddress( pub: Uint8Array, addr: string, -){ +): Result<[BtAddrString, BtAddrString], GenerateSegwitAddrError> { const first_rnd = new Uint8Array(4); first_rnd.set(pub.subarray(0, 4)); const second_rnd = new Uint8Array(4); @@ -81,19 +71,19 @@ export function generateFakeSegwitAddress( ? "bc" : undefined; if (prefix === undefined) { - return opKnownFailure(GenerateSegwitAddrError.WRONG_PREFIX); + return Result.error(GenerateSegwitAddrError.WRONG_PREFIX); } const addr1 = BitcoinSewgit.encode(prefix, 0, Array.from(first_part)); if (addr1.type === "fail") { - return opKnownFailure(GenerateSegwitAddrError.INVALID_SEGWIT); + return Result.error(GenerateSegwitAddrError.INVALID_SEGWIT); } const addr2 = BitcoinSewgit.encode(prefix, 0, Array.from(second_part)); if (addr2.type === "fail") { - return opKnownFailure(GenerateSegwitAddrError.INVALID_SEGWIT); + return Result.error(GenerateSegwitAddrError.INVALID_SEGWIT); } const result: [BtAddrString, BtAddrString] = [addr1.body, addr2.body]; - return opFixedSuccess(result); + return Result.of(result); } // https://github.com/bitcoin/bitcoin/blob/master/src/policy/policy.cpp diff --git a/packages/taler-util/src/iban.ts b/packages/taler-util/src/iban.ts @@ -14,13 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { - OperationFail, - OperationOk, - OperationResult, - opFixedSuccess, - opKnownFailure, -} from "./operation.js"; +import { Result } from "./result.js"; /** * IBAN validation. @@ -120,29 +114,30 @@ function mod97(digits: number[]): number { export function convertHUF_BBANtoIBAN( value: string, -): - | OperationOk<IbanString> - | OperationFail<ParseIbanError.TOO_LONG> - | OperationFail<ParseIbanError.TOO_SHORT> - | OperationFail<ParseIbanError.INVALID_CHARSET> { +): Result< + IbanString, + | ParseIbanError.TOO_LONG + | ParseIbanError.TOO_SHORT + | ParseIbanError.INVALID_CHARSET +> { if (value.startsWith("HU")) { const maybeIban = parseIban(value); - if (maybeIban.case === "ok") { - return opFixedSuccess<IbanString>(maybeIban.body); + if (Result.isOk(maybeIban)) { + return Result.of(maybeIban.value); } } if (value.length > 24) { - return opKnownFailure(ParseIbanError.TOO_LONG); + return Result.error(ParseIbanError.TOO_LONG); } if (value.length < 16) { - return opKnownFailure(ParseIbanError.TOO_SHORT); + return Result.error(ParseIbanError.TOO_SHORT); } if (!/[0-9+]/.test(value)) { - return opKnownFailure(ParseIbanError.INVALID_CHARSET); + return Result.error(ParseIbanError.INVALID_CHARSET); } const bban = value.length === 16 ? `${value}00000000` : value; - return opFixedSuccess<IbanString>(constructIban("HU", bban)); + return Result.of(constructIban("HU", bban)); } /** @@ -152,29 +147,30 @@ export function convertHUF_BBANtoIBAN( */ export function convertCHF_BBANtoIBAN( value: string, -): - | OperationOk<IbanString> - | OperationFail<ParseIbanError.TOO_LONG> - | OperationFail<ParseIbanError.TOO_SHORT> - | OperationFail<ParseIbanError.INVALID_CHARSET> { +): Result< + IbanString, + | ParseIbanError.TOO_LONG + | ParseIbanError.TOO_SHORT + | ParseIbanError.INVALID_CHARSET +> { if (value.startsWith("CH")) { const maybeIban = parseIban(value); - if (maybeIban.case === "ok") { - return opFixedSuccess<IbanString>(maybeIban.body); + if (maybeIban.tag === "ok") { + return Result.of(maybeIban.value); } } if (value.length > 24) { - return opKnownFailure(ParseIbanError.TOO_LONG); + return Result.error(ParseIbanError.TOO_LONG); } if (value.length < 16) { - return opKnownFailure(ParseIbanError.TOO_SHORT); + return Result.error(ParseIbanError.TOO_SHORT); } if (!/[0-9+]/.test(value)) { - return opKnownFailure(ParseIbanError.INVALID_CHARSET); + return Result.error(ParseIbanError.INVALID_CHARSET); } const bban = value; - return opFixedSuccess<IbanString>(constructIban("CH", bban)); + return Result.of(constructIban("CH", bban)); } /** @@ -184,12 +180,12 @@ export function convertCHF_BBANtoIBAN( */ export function parseIban( ibanString: string, -): OperationResult<IbanString, ParseIbanError> { +): Result<IbanString, ParseIbanError> { if (ibanString.length < 4) { - return opKnownFailure(ParseIbanError.TOO_SHORT); + return Result.error(ParseIbanError.TOO_SHORT); } if (ibanString.length > 34) { - return opKnownFailure(ParseIbanError.TOO_LONG); + return Result.error(ParseIbanError.TOO_LONG); } const myIban = ibanString.toUpperCase().replace(/[\s-\._]/g, ""); @@ -197,7 +193,7 @@ export function parseIban( const countryInfo = ibanCountryInfoTable[countryCode]; if (!countryInfo) { - return opKnownFailure(ParseIbanError.UNSUPPORTED_COUNTRY); + return Result.error(ParseIbanError.UNSUPPORTED_COUNTRY); } let digits: number[] = []; @@ -205,22 +201,22 @@ export function parseIban( for (let i = 4; i < myIban.length; i++) { const cc = myIban.charCodeAt(i); if (!appendDigit(digits, cc)) { - return opKnownFailure(ParseIbanError.INVALID_CHARSET); + return Result.error(ParseIbanError.INVALID_CHARSET); } } for (let i = 0; i < 4; i++) { const cc = myIban.charCodeAt(i); if (!appendDigit(digits, cc)) { - return opKnownFailure(ParseIbanError.INVALID_CHARSET); + return Result.error(ParseIbanError.INVALID_CHARSET); } } const rem = mod97(digits); if (rem === 1) { - return opFixedSuccess<IbanString>(myIban as IbanString); + return Result.of(myIban as IbanString); } else { - return opKnownFailure(ParseIbanError.INVALID_CHECKSUM); + return Result.error(ParseIbanError.INVALID_CHECKSUM); } } diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts @@ -46,9 +46,9 @@ export { } from "./nacl-fast.js"; export * from "./notifications.js"; export * from "./observability.js"; -export * from "./performance.js"; export * from "./operation.js"; export * from "./payto.js"; +export * from "./performance.js"; export * from "./promises.js"; export * from "./qr.js"; export { RequestThrottler } from "./RequestThrottler.js"; @@ -96,10 +96,12 @@ export * from "./taler-signatures.js"; export * from "./account-restrictions.js"; -export * from "./aml/properties.js"; export * from "./aml/events.js"; +export * from "./aml/properties.js"; export * from "./aml/reporting.js"; export * from "./iso-3166.js"; export * from "./iso-4217.js"; export * from "./iso-639.js"; + +export * from "./result.js"; diff --git a/packages/taler-util/src/operation.ts b/packages/taler-util/src/operation.ts @@ -268,17 +268,6 @@ export function succeedOrThrow<R>(resp: OperationResult<R, unknown>): R { throw TalerError.fromException(resp); } -export function succeedOrValue<R, V>( - resp: OperationResult<R, unknown>, - v: V, -): R | V { - if (isOperationOk(resp)) { - return resp.body; - } - - return v; -} - /** * The operation is expected to fail with a body. * Return the body of the result. @@ -312,36 +301,6 @@ export function alternativeOrThrow<Error, Body, Alt>( return (resp as any).body; } -/** - * The operation is expected to fail. - * Return the error details. - * Throw if the operation didn't fail with expected code. - * - * @param resp - * @param s - * @returns - */ -export function failOrThrow<E>( - resp: OperationResult<unknown, E>, - s: E, -): TalerErrorDetail | undefined { - if (isOperationOk(resp)) { - throw TalerError.fromException( - new Error(`request succeed but failure "${s}" was expected`), - ); - } - if (isOperationFail(resp) && resp.case === s) { - return resp.detail; - } - throw TalerError.fromException( - new Error( - `request failed with "${JSON.stringify( - resp, - )}" but case "${s}" was expected`, - ), - ); -} - export type ResultByMethod< TT extends object, p extends keyof TT, diff --git a/packages/taler-util/src/payto.ts b/packages/taler-util/src/payto.ts @@ -18,13 +18,8 @@ import { BitcoinBech32 } from "./bech32.js"; import { generateFakeSegwitAddress } from "./bitcoin.js"; import { Codec, Context, DecodingError, renderContext } from "./codec.js"; import { assertUnreachable } from "./errors.js"; -import { IbanString, parseIban } from "./iban.js"; -import { - opFixedSuccess, - opKnownFailure, - opKnownFailureWithBody, - succeedOrThrow, -} from "./operation.js"; +import { IbanString, parseIban, ParseIbanError } from "./iban.js"; +import { Result, ResultError, ResultOk } from "./result.js"; import { decodeCrock, encodeCrock, @@ -230,16 +225,20 @@ export namespace Paytos { return url.href as FullPaytoString; } - export function parseReservePub(reserve: string | undefined) { - if (!reserve) return opKnownFailure(ReservePubParseError.WRONG_LENGTH); + export function parseReservePub( + reserve: string | undefined, + ): + | ResultOk<Uint8Array<ArrayBufferLike>> + | ResultError<ReservePubParseError, { message: string } | undefined> { + if (!reserve) return Result.error(ReservePubParseError.WRONG_LENGTH); try { const pub = decodeCrock(reserve); if (!pub || pub.length !== 32) { - return opKnownFailure(ReservePubParseError.WRONG_LENGTH); + return Result.error(ReservePubParseError.WRONG_LENGTH); } - return opFixedSuccess(pub); + return Result.of(pub); } catch (e) { - return opKnownFailureWithBody(ReservePubParseError.DECODE_ERROR, { + return Result.errorWithDetail(ReservePubParseError.DECODE_ERROR, { message: String(e), }); } @@ -373,7 +372,7 @@ export namespace Paytos { ? undefined : generateFakeSegwitAddress(reservePub, address); - const segwitAddrs = !sgRes || sgRes.type === "fail" ? [] : sgRes.body; + const segwitAddrs = !sgRes || !Result.isOk(sgRes) ? [] : sgRes.value; return { targetType: PaytoType.Bitcoin, address, @@ -470,25 +469,90 @@ export namespace Paytos { ////////////////////// // parsing function /////////////////////// + export function asString(p: FullPaytoString): Paytos.URI { - return succeedOrThrow(fromString(p)) + return Result.unpack(fromString(p)); + } + + export interface ParsePaytoOptions { + /** + * do not check path component format + */ + ignoreComponentError?: boolean; + /** + * take unknown target types as valid + */ + allowUnsupported?: boolean; } + export type TargetPathErrorDetail = + | { + targetType: PaytoType.Bitcoin; + pos: 0; + error: ResultError<BitcoinBech32.BitcoinParseError>; + } + | { + targetType: PaytoType.Bitcoin; + pos: 1; + error: ResultError< + ReservePubParseError, + { message: string } | undefined + >; + } + | { + targetType: PaytoType.IBAN; + pos: 0 | 1; + error: ResultError<ParseIbanError>; + } + | { + targetType: PaytoType.TalerBank; + pos: 0 | 1; + } + | { + targetType: PaytoType.TalerReserve; + pos: 0; + } + | { + targetType: PaytoType.TalerReserve; + pos: 1; + error: ResultError< + ReservePubParseError, + { message: string } | undefined + >; + } + | { + targetType: PaytoType.TalerReserveHttp; + pos: 0; + } + | { + targetType: PaytoType.TalerReserveHttp; + pos: 1; + error: ResultError< + ReservePubParseError, + { message: string } | undefined + >; + } + | { + targetType: PaytoType.Ethereum; + pos: 0; + } + | { + targetType: PaytoType.Cyclos; + pos: 0; + }; + export function fromString( s: string, - opts: { - /** - * do not check path component format - */ - ignoreComponentError?: boolean; - /** - * take unknown target types as valid - */ - allowUnsupported?: boolean; - } = {}, - ) { + opts: ParsePaytoOptions = {}, + ): + | ResultOk<Paytos.URI> + | ResultError<PaytoParseError.WRONG_PREFIX> + | ResultError<PaytoParseError.UNSUPPORTED, { targetType: string }> + | ResultError<PaytoParseError.COMPONENTS_LENGTH, { targetType: PaytoType }> + | ResultError<PaytoParseError.INVALID_TARGET_PATH, TargetPathErrorDetail> + | ResultError<PaytoParseError.INCOMPLETE, { targetType: string }> { if (!s.startsWith(PAYTO_PREFIX)) { - return opKnownFailure(PaytoParseError.WRONG_PREFIX); + return Result.error(PaytoParseError.WRONG_PREFIX); } const [acct, search] = s.slice(PAYTO_PREFIX.length).split("?", 2); @@ -499,14 +563,13 @@ export namespace Paytos { firstSlashPos === -1 ? acct : acct.slice(0, firstSlashPos) ) as PaytoType; if (!opts.allowUnsupported && !supported_targets[targetType]) { - const d = opKnownFailureWithBody(PaytoParseError.UNSUPPORTED, { + return Result.errorWithDetail(PaytoParseError.UNSUPPORTED, { targetType, }); - return d; } const targetPath = acct.slice(firstSlashPos + 1); if (firstSlashPos === -1 || !targetPath) { - return opKnownFailureWithBody(PaytoParseError.INCOMPLETE, { targetType }); + return Result.errorWithDetail(PaytoParseError.INCOMPLETE, { targetType }); } const params: { [k: string]: string } = {}; @@ -522,7 +585,7 @@ export namespace Paytos { switch (targetType) { case PaytoType.IBAN: { if (cs.length !== 1 && cs.length !== 2) { - return opKnownFailureWithBody(PaytoParseError.COMPONENTS_LENGTH, { + return Result.errorWithDetail(PaytoParseError.COMPONENTS_LENGTH, { targetType, }); } @@ -531,19 +594,19 @@ export namespace Paytos { const ibaRes = parseIban(iban); - if (!opts.ignoreComponentError && ibaRes.type === "fail") { - return opKnownFailureWithBody(PaytoParseError.INVALID_TARGET_PATH, { + if (!opts.ignoreComponentError && ibaRes.tag === "error") { + return Result.errorWithDetail(PaytoParseError.INVALID_TARGET_PATH, { pos: 0, targetType, error: ibaRes, - }); + } as const); } - return opFixedSuccess<URI>(createIban(iban as IbanString, bic, params)); + return Result.of(createIban(iban as IbanString, bic, params)); } case PaytoType.Bitcoin: { if (cs.length !== 1 && cs.length !== 2) { - return opKnownFailureWithBody(PaytoParseError.COMPONENTS_LENGTH, { + return Result.errorWithDetail(PaytoParseError.COMPONENTS_LENGTH, { targetType, }); } @@ -553,8 +616,8 @@ export namespace Paytos { address, BitcoinBech32.Encodings.BECH32, ); - if (!opts.ignoreComponentError && btRes.type === "fail") { - return opKnownFailureWithBody(PaytoParseError.INVALID_TARGET_PATH, { + if (!opts.ignoreComponentError && Result.isError(btRes)) { + return Result.errorWithDetail(PaytoParseError.INVALID_TARGET_PATH, { pos: 0 as const, targetType, error: btRes, @@ -562,18 +625,18 @@ export namespace Paytos { } const pubRes = cs.length === 1 ? undefined : parseReservePub(cs[1]); - if (!opts.ignoreComponentError && pubRes && pubRes.type === "fail") { - return opKnownFailureWithBody(PaytoParseError.INVALID_TARGET_PATH, { + if (!opts.ignoreComponentError && pubRes && Result.isError(pubRes)) { + return Result.errorWithDetail(PaytoParseError.INVALID_TARGET_PATH, { pos: 1 as const, targetType, error: pubRes, }); } - return opFixedSuccess<URI>( + return Result.of( createBitcoin( address as BtAddrString, - pubRes && pubRes.type === "ok" ? pubRes.body : undefined, + pubRes != null ? Result.orUndefined(pubRes) : undefined, params, ), ); @@ -581,29 +644,29 @@ export namespace Paytos { case PaytoType.TalerBank: { if (cs.length < 2) { - return opKnownFailureWithBody(PaytoParseError.COMPONENTS_LENGTH, { + return Result.errorWithDetail(PaytoParseError.COMPONENTS_LENGTH, { targetType, }); } const host = parseHostPortPath2(cs[0], cs.slice(1, -1).join("/")); if (!opts.ignoreComponentError && !host) { - return opKnownFailureWithBody(PaytoParseError.INVALID_TARGET_PATH, { - pos: 0 as const, + return Result.errorWithDetail(PaytoParseError.INVALID_TARGET_PATH, { + pos: 0, targetType, error: host, - }); + } as const); } const account = parseTalerBankAccount(cs[cs.length - 1]); if (!opts.ignoreComponentError && !account) { - return opKnownFailureWithBody(PaytoParseError.INVALID_TARGET_PATH, { + return Result.errorWithDetail(PaytoParseError.INVALID_TARGET_PATH, { pos: 1 as const, targetType, error: account, }); } - return opFixedSuccess<URI>( + return Result.of( createTalerBank( host ?? (cs[0] as HostPortPath), account ?? cs[1], @@ -613,13 +676,13 @@ export namespace Paytos { } case PaytoType.TalerReserve: { if (cs.length < 2) { - return opKnownFailureWithBody(PaytoParseError.COMPONENTS_LENGTH, { + return Result.errorWithDetail(PaytoParseError.COMPONENTS_LENGTH, { targetType, }); } const exchange = parseHostPortPath2(cs[0], cs.slice(1, -1).join("/")); if (!opts.ignoreComponentError && !exchange) { - return opKnownFailureWithBody(PaytoParseError.INVALID_TARGET_PATH, { + return Result.errorWithDetail(PaytoParseError.INVALID_TARGET_PATH, { pos: 0 as const, targetType, error: exchange, @@ -628,25 +691,25 @@ export namespace Paytos { const reservePub = cs[cs.length - 1]; const pubRes = parseReservePub(reservePub); - if (!opts.ignoreComponentError && pubRes.type === "fail") { - return opKnownFailureWithBody(PaytoParseError.INVALID_TARGET_PATH, { + if (!opts.ignoreComponentError && !Result.isOk(pubRes)) { + return Result.errorWithDetail(PaytoParseError.INVALID_TARGET_PATH, { pos: 1 as const, targetType, error: pubRes, }); } - return opFixedSuccess<URI>( + return Result.of<URI>( createTalerReserve( exchange ?? (cs[0] as HostPortPath), - pubRes.type === "ok" ? pubRes.body : decodeCrock(reservePub), + Result.isOk(pubRes) ? pubRes.value : decodeCrock(reservePub), params, ), ); } case PaytoType.TalerReserveHttp: { if (cs.length < 2) { - return opKnownFailureWithBody(PaytoParseError.COMPONENTS_LENGTH, { + return Result.errorWithDetail(PaytoParseError.COMPONENTS_LENGTH, { targetType, }); } @@ -656,57 +719,57 @@ export namespace Paytos { "http", ); if (!opts.ignoreComponentError && !exchange) { - return opKnownFailureWithBody(PaytoParseError.INVALID_TARGET_PATH, { + return Result.errorWithDetail(PaytoParseError.INVALID_TARGET_PATH, { pos: 0, targetType, error: exchange, - }); + } as const); } const reservePub = cs[cs.length - 1]; const pubRes = parseReservePub(reservePub); - if (!opts.ignoreComponentError && pubRes.type === "fail") { - return opKnownFailureWithBody(PaytoParseError.INVALID_TARGET_PATH, { + if (!opts.ignoreComponentError && !Result.isOk(pubRes)) { + return Result.errorWithDetail(PaytoParseError.INVALID_TARGET_PATH, { pos: 1, targetType, error: pubRes, - }); + } as const); } - return opFixedSuccess<URI>( + return Result.of<URI>( createTalerReserveHttp( exchange ?? (cs[0] as HostPortPath), - pubRes.type === "ok" ? pubRes.body : decodeCrock(reservePub), + Result.isOk(pubRes) ? pubRes.value : decodeCrock(reservePub), params, ), ); } case PaytoType.Ethereum: { if (cs.length !== 1) { - return opKnownFailureWithBody(PaytoParseError.COMPONENTS_LENGTH, { + return Result.errorWithDetail(PaytoParseError.COMPONENTS_LENGTH, { targetType, }); } const address = parseEthereumAddress(cs[0]); if (!opts.ignoreComponentError && !address) { - return opKnownFailureWithBody(PaytoParseError.INVALID_TARGET_PATH, { + return Result.errorWithDetail(PaytoParseError.INVALID_TARGET_PATH, { pos: 0, targetType, error: address, - }); + } as const); } - return opFixedSuccess<URI>( + return Result.of<URI>( createEthereum(address ?? (cs[0] as EthAddrString), params), ); } case PaytoType.Cyclos: { if (cs.length < 2) { - return opKnownFailureWithBody(PaytoParseError.COMPONENTS_LENGTH, { + return Result.errorWithDetail(PaytoParseError.COMPONENTS_LENGTH, { targetType, }); } const host = parseHostPortPath2(cs[0], cs.slice(1, -1).join("/")); if (!opts.ignoreComponentError && !host) { - return opKnownFailureWithBody(PaytoParseError.INVALID_TARGET_PATH, { + return Result.errorWithDetail(PaytoParseError.INVALID_TARGET_PATH, { pos: 0 as const, targetType, error: host, @@ -715,17 +778,13 @@ export namespace Paytos { const accountId = cs[cs.length - 1]; - return opFixedSuccess<URI>( - createCyclos( - host ?? (cs[0] as HostPortPath), - accountId, - params, - ), + return Result.of<URI>( + createCyclos(host ?? (cs[0] as HostPortPath), accountId, params), ); } default: { if (opts.allowUnsupported) { - return opFixedSuccess<URI>( + return Result.of<URI>( createUnsupported(targetType, targetPath, params), ); } @@ -1133,10 +1192,10 @@ export function parsePaytoUri(s: string): PaytoUri | undefined { const reserve = !msg ? params["subject"] : msg[0]; const pubRes = !reserve ? undefined : Paytos.parseReservePub(reserve); const addr = - !pubRes || pubRes.type === "fail" + !pubRes || !Result.isOk(pubRes) ? undefined - : generateFakeSegwitAddress(pubRes.body, targetPath); - const segwitAddrs = !addr || addr.type === "fail" ? [] : addr.body; + : generateFakeSegwitAddress(pubRes.value, targetPath); + const segwitAddrs = !addr || !Result.isOk(addr) ? [] : addr.value; const result: PaytoUriBitcoin = { isKnown: true, @@ -1152,7 +1211,7 @@ export function parsePaytoUri(s: string): PaytoUri | undefined { case "x-taler-bank": { const parts = targetPath.split("/"); const host = parts[0]; - const account = parts[parts.length-1]; + const account = parts[parts.length - 1]; return { targetPath, targetType, @@ -1165,7 +1224,7 @@ export function parsePaytoUri(s: string): PaytoUri | undefined { case "cyclos": { const parts = targetPath.split("/"); const host = parts[0]; - const account = parts[parts.length-1]; + const account = parts[parts.length - 1]; const result: PaytoUriCyclos = { isKnown: true, targetPath, diff --git a/packages/taler-util/src/paytos.test.ts b/packages/taler-util/src/paytos.test.ts @@ -17,33 +17,33 @@ import test from "ava"; import { HostPortPath, PaytoType, Paytos } from "./payto.js"; -import { succeedOrThrow } from "./index.js"; +import { Result } from "./result.js"; test("basic payto parsing", (t) => { const r1 = Paytos.fromString("https://example.com/"); - t.is(r1.type, "fail"); + t.is(r1.tag, "error"); const r2 = Paytos.fromString("payto:blabla"); - t.is(r2.type, "fail"); + t.is(r2.tag, "error"); // this doesn't work because x-taler-bank requires host and account const r3 = Paytos.fromString("payto:://x-taler-bank/12"); - t.is(r3.type, "fail"); + t.is(r3.tag, "error"); const r4 = Paytos.fromString("payto://x-taler-bank/host/account"); - if (r4.type === "fail") { + if (r4.tag === "error") { t.fail(); throw Error(); } - if (r4.body.targetType !== PaytoType.TalerBank) { - t.fail(r4.body.targetType); + if (r4.value.targetType !== PaytoType.TalerBank) { + t.fail(r4.value.targetType); throw Error(); } - t.is(r4.body.normalizedPath, "host/account"); + t.is(r4.value.normalizedPath, "host/account"); }); test("basic x-taler-bank payto string", (t) => { - const result = succeedOrThrow( + const result = Result.unpack( Paytos.fromString("payto://x-taler-bank/bank.demo.taler.net/accountName"), ); @@ -60,29 +60,36 @@ test("basic x-taler-bank payto string", (t) => { test("parsing payto and stringify again on normalized strings are unchanged", (t) => { const payto1 = "payto://iban/DE1231231231?reciever-name=John%20Doe"; t.is( - Paytos.toFullString(succeedOrThrow(Paytos.fromString(payto1))), + Paytos.toFullString(Result.unpack(Paytos.fromString(payto1))), payto1 as Paytos.FullPaytoString, ); const normalized = "payto://iban/DE1231231231"; t.is( - Paytos.toNormalizedString(succeedOrThrow(Paytos.fromString(payto1))), + Paytos.toNormalizedString(Result.unpack(Paytos.fromString(payto1))), normalized as Paytos.NormalizedPaytoString, ); }); test("parsing payto and stringify again converts to the normal form", (t) => { - const fullPayto_not_normalized = "payto://iban/de1231231231?reciever-name=John%20Doe"; + const fullPayto_not_normalized = + "payto://iban/de1231231231?reciever-name=John%20Doe"; // after normalization the country code is uppercased - const fullPayto_normalized = "payto://iban/DE1231231231?reciever-name=John%20Doe" as Paytos.FullPaytoString; + const fullPayto_normalized = + "payto://iban/DE1231231231?reciever-name=John%20Doe" as Paytos.FullPaytoString; t.is( - Paytos.toFullString(succeedOrThrow(Paytos.fromString(fullPayto_not_normalized))), + Paytos.toFullString( + Result.unpack(Paytos.fromString(fullPayto_not_normalized)), + ), fullPayto_normalized, ); const normalized_lowercase = "payto://iban/DE1231231231"; - const normalized_uppercase = "payto://iban/DE1231231231" as Paytos.NormalizedPaytoString; + const normalized_uppercase = + "payto://iban/DE1231231231" as Paytos.NormalizedPaytoString; t.is( - Paytos.toNormalizedString(succeedOrThrow(Paytos.fromString(normalized_lowercase))), + Paytos.toNormalizedString( + Result.unpack(Paytos.fromString(normalized_lowercase)), + ), normalized_uppercase, ); }); @@ -90,13 +97,13 @@ test("parsing payto with % carh", (t) => { const payto1 = "payto://iban/DE7763544441436?receiver-name=Test%20123%2B-%24%25%5E%3Cem%3Ehi%3C%2Fem%3E" as Paytos.FullPaytoString; - t.is(Paytos.toFullString(succeedOrThrow(Paytos.fromString(payto1))), payto1); + t.is(Paytos.toFullString(Result.unpack(Paytos.fromString(payto1))), payto1); }); test("adding payto query params", (t) => { const payto1 = "payto://iban/DE1231231231?receiver-name=John%20Doe" as Paytos.FullPaytoString; - const p = succeedOrThrow(Paytos.fromString(payto1)); + const p = Result.unpack(Paytos.fromString(payto1)); p.params["foo"] = "42"; @@ -108,8 +115,10 @@ test("adding payto query params", (t) => { test("basic cyclos payto string", (t) => { { - const result = succeedOrThrow( - Paytos.fromString("payto://cyclos/demo.cyclos.org/31000163100000000?receiver-name=John%20Doe"), + const result = Result.unpack( + Paytos.fromString( + "payto://cyclos/demo.cyclos.org/31000163100000000?receiver-name=John%20Doe", + ), ); if (result.targetType !== PaytoType.Cyclos) { @@ -123,18 +132,22 @@ test("basic cyclos payto string", (t) => { } { - const result = succeedOrThrow( - Paytos.fromString("payto://cyclos/communities.cyclos.org/utrecht/31000163100000000?receiver-name=John%20Doe"), + const result = Result.unpack( + Paytos.fromString( + "payto://cyclos/communities.cyclos.org/utrecht/31000163100000000?receiver-name=John%20Doe", + ), ); if (result.targetType !== PaytoType.Cyclos) { t.fail(); throw Error(); } - t.is(result.normalizedPath, "communities.cyclos.org/utrecht/31000163100000000"); + t.is( + result.normalizedPath, + "communities.cyclos.org/utrecht/31000163100000000", + ); t.is(result.url, "https://communities.cyclos.org/utrecht/" as HostPortPath); t.is(result.account, "31000163100000000"); t.is(result.params["receiver-name"], "John Doe"); } }); - diff --git a/packages/taler-util/src/result.ts b/packages/taler-util/src/result.ts @@ -14,28 +14,57 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { TalerErrorCode } from "./taler-error-codes.js"; - /** * Result of a generic operation that can fail. */ -export type Result<T, Err> = ResultOk<T> | ResultError<Err>; +export type Result<T, Err> = ResultOk<T> | ResultError<Err, unknown>; export interface ResultOk<T> { readonly tag: "ok"; readonly value: T; } -export interface ResultError<Err> { +export interface ResultError<Err, Detail = unknown> { readonly tag: "error"; readonly error: Err; + readonly detail: Detail; } export const Result = { - of<T>(value: T): Result<T, any> { + of<T>(value: T): ResultOk<T> { return { tag: "ok", value }; }, - error<Err extends TalerErrorCode>(error: Err): ResultError<Err> { - return { tag: "error", error }; + error<Err>(error: Err): ResultError<Err, undefined> { + return { tag: "error", error, detail: undefined }; + }, + errorWithDetail<Err, Detail = void>( + error: Err, + detail: Detail, + ): ResultError<Err, Detail> { + return { tag: "error", error, detail }; + }, + unpack<T, Err>(r: Result<T, Err>): T { + if (r.tag !== "ok") { + throw Error("expected success result"); + } + return r.value; + }, + isOk<T, Err>(r: Result<T, Err>): r is ResultOk<T> { + return r.tag === "ok"; + }, + isError<T, Err>(r: Result<T, Err>): r is ResultError<Err> { + return r.tag === "error"; + }, + orUndefined<T>(r: Result<T, any>): T | undefined { + if (r.tag !== "ok") { + return undefined; + } + return r.value; + }, + orElse<T, TA>(r: Result<T, any>, alt: TA): T | TA { + if (r.tag !== "ok") { + return alt; + } + return r.value; }, }; diff --git a/packages/taler-util/src/segwit_addr.ts b/packages/taler-util/src/segwit_addr.ts @@ -19,8 +19,8 @@ // THE SOFTWARE. import { BitcoinBech32 } from "./bech32.js"; -import { OperationResult, opFixedSuccess, opKnownFailure } from "./operation.js"; -import { BtAddrString } from "./payto.js"; +import { opFixedSuccess, opKnownFailure } from "./operation.js"; +import { Result } from "./result.js"; function convertbits( data: Array<number>, @@ -68,40 +68,39 @@ export namespace BitcoinSewgit { export function decode( addr: string, enc?: BitcoinBech32.Encodings, - ): OperationResult< - { version: number; program: Array<number> }, - BitcoinBech32.BitcoinParseError | BitcoinSewgitParseError + ): Result< + { + version: number; + program: number[]; + }, + BitcoinSewgitParseError | BitcoinBech32.BitcoinParseError > { const decResp = BitcoinBech32.decode(addr, enc); - if (decResp.type === "fail") { + if (decResp.tag === "error") { return decResp; } - const { body: dec } = decResp; + const { value: dec } = decResp; if (dec.data.length < 1 || dec.data[0] > 16) { - return opKnownFailure(BitcoinSewgitParseError.INVALID_DATA); + return Result.error(BitcoinSewgitParseError.INVALID_DATA); } const res = convertbits(dec.data.slice(1), 5, 8, false); if (res === null || res.length < 2 || res.length > 40) { - return opKnownFailure(BitcoinSewgitParseError.DECODING_PROBLEM); + return Result.error(BitcoinSewgitParseError.DECODING_PROBLEM); } if (dec.data[0] === 0 && res.length !== 20 && res.length !== 32) { - return opKnownFailure(BitcoinSewgitParseError.DECODING_PROBLEM); + return Result.error(BitcoinSewgitParseError.DECODING_PROBLEM); } if (dec.data[0] === 0 && enc === BitcoinBech32.Encodings.BECH32) { - return opKnownFailure(BitcoinSewgitParseError.DECODING_PROBLEM); + return Result.error(BitcoinSewgitParseError.DECODING_PROBLEM); } if (dec.data[0] !== 0 && enc === BitcoinBech32.Encodings.BECH32M) { - return opKnownFailure(BitcoinSewgitParseError.DECODING_PROBLEM); + return Result.error(BitcoinSewgitParseError.DECODING_PROBLEM); } - return opFixedSuccess({ version: dec.data[0], program: res }); + return Result.of({ version: dec.data[0], program: res }); } - export function encode( - hrp: string, - version: number, - program: Array<number>, - ) { + export function encode(hrp: string, version: number, program: Array<number>) { const enc = version > 0 ? BitcoinBech32.Encodings.BECH32M diff --git a/packages/taler-util/src/taleruri.ts b/packages/taler-util/src/taleruri.ts @@ -23,19 +23,14 @@ /** * Imports. */ +import { Amounts } from "./amounts.js"; import { Codec, Context, DecodingError, renderContext } from "./codec.js"; +import { assertUnreachable } from "./errors.js"; import { HostPortPath, Paytos } from "./payto.js"; -import { Result } from "./result.js"; +import { Result, ResultError, ResultOk } from "./result.js"; import { TalerErrorCode } from "./taler-error-codes.js"; -import { AmountString, EddsaPublicKeyString, HashCodeString } from "./types-taler-common.js"; +import { AmountString, HashCodeString } from "./types-taler-common.js"; import { URL, URLSearchParams } from "./url.js"; -import { - opFixedSuccess, - opKnownFailure, - opKnownFailureWithBody, -} from "./operation.js"; -import { Amounts } from "./amounts.js"; -import { assertUnreachable } from "./errors.js"; /** * A parsed taler URI. */ @@ -373,7 +368,9 @@ export namespace TalerUris { case TalerUriAction.WithdrawalTransferResult: return `/`; case TalerUriAction.AddContact: - return `/${p.aliasType}/${p.alias}/${asHost(p.mailboxBaseUri as HostPortPath)}/${p.mailboxIdentity}` + return `/${p.aliasType}/${p.alias}/${asHost( + p.mailboxBaseUri as HostPortPath, + )}/${p.mailboxIdentity}`; default: assertUnreachable(p); } @@ -388,22 +385,84 @@ export namespace TalerUris { return url.href as TalerUriString; } + export interface PaytoParseOptions { + /** + * do not check path component format + */ + ignoreComponentError?: boolean; + } + + export type InvalidTargetPathDetail = + | { + uriType: TalerUriAction.Pay; + } + | { + uriType: TalerUriAction.Withdraw; + } + | { + uriType: TalerUriAction.Refund; + pos: 0; + } + | { + uriType: TalerUriAction.Refund; + pos: 1; + } + | { + uriType: TalerUriAction.PayPull; + pos: 0; + } + | { + uriType: TalerUriAction.PayPush; + pos: 0; + } + | { + uriType: TalerUriAction.PayTemplate; + pos: 0; + } + | { + uriType: TalerUriAction.WithdrawExchange; + pos: 0; + } + | { + uriType: TalerUriAction.WithdrawExchange; + pos: 1; + } + | { + uriType: TalerUriAction.AddExchange; + pos: 0; + } + | { + uriType: TalerUriAction.AddContact; + pos: 0; + }; + export function fromString( s: string, - opts: { - /** - * do not check path component format - */ - ignoreComponentError?: boolean; - } = {}, - ) { + opts: PaytoParseOptions = {}, + ): + | ResultOk<TalerUri> + | ResultError<TalerUriParseError.WRONG_PREFIX> + | ResultError<TalerUriParseError.UNSUPPORTED, { uriType: string }> + | ResultError<TalerUriParseError.INCOMPLETE, { uriType: string }> + | ResultError< + TalerUriParseError.COMPONENTS_LENGTH, + { uriType: TalerUriAction } + > + | ResultError< + TalerUriParseError.INVALID_TARGET_PATH, + InvalidTargetPathDetail + > + | ResultError< + TalerUriParseError.INVALID_PARAMETER, + { uriType: TalerUriAction; name: string } + > { // check prefix let isHttp = false; if ( !s.startsWith(TALER_PREFIX) && !(isHttp = s.startsWith(TALER_HTTP_PREFIX)) ) { - return opKnownFailure(TalerUriParseError.WRONG_PREFIX); + return Result.error(TalerUriParseError.WRONG_PREFIX); } const scheme = isHttp ? ("http" as const) : ("https" as const); @@ -418,15 +477,14 @@ export namespace TalerUris { firstSlashPos === -1 ? path : path.slice(0, firstSlashPos) ) as TalerUriAction; if (!supported_targets[uriType]) { - const d = opKnownFailureWithBody(TalerUriParseError.UNSUPPORTED, { + return Result.errorWithDetail(TalerUriParseError.UNSUPPORTED, { uriType, }); - return d; } const targetPath = path.slice(firstSlashPos + 1); if (firstSlashPos === -1 || !targetPath) { - return opKnownFailureWithBody(TalerUriParseError.INCOMPLETE, { + return Result.errorWithDetail(TalerUriParseError.INCOMPLETE, { uriType, }); } @@ -447,7 +505,7 @@ export namespace TalerUris { case TalerUriAction.Pay: { // check number of segments if (cs.length < 3) { - return opKnownFailureWithBody(TalerUriParseError.COMPONENTS_LENGTH, { + return Result.errorWithDetail(TalerUriParseError.COMPONENTS_LENGTH, { uriType, }); } @@ -459,7 +517,7 @@ export namespace TalerUris { scheme, ); if (!opts.ignoreComponentError && !merchant) { - return opKnownFailureWithBody( + return Result.errorWithDetail( TalerUriParseError.INVALID_TARGET_PATH, { pos: 0 as const, @@ -474,7 +532,7 @@ export namespace TalerUris { // get session const sessionId = cs[cs.length - 1]; - return opFixedSuccess<URI>( + return Result.of<URI>( createTalerPay( merchant ?? (cs[0] as HostPortPath), orderId, @@ -489,7 +547,7 @@ export namespace TalerUris { case TalerUriAction.Withdraw: { // check number of segments if (cs.length < 2) { - return opKnownFailureWithBody(TalerUriParseError.COMPONENTS_LENGTH, { + return Result.errorWithDetail(TalerUriParseError.COMPONENTS_LENGTH, { uriType, }); } @@ -501,7 +559,7 @@ export namespace TalerUris { scheme, ); if (!opts.ignoreComponentError && !bank) { - return opKnownFailureWithBody( + return Result.errorWithDetail( TalerUriParseError.INVALID_TARGET_PATH, { pos: 0 as const, @@ -518,7 +576,7 @@ export namespace TalerUris { ? undefined : params["external-confirmation"] === "1"; - return opFixedSuccess<URI>( + return Result.of<URI>( createTalerWithdraw(bank ?? (cs[0] as HostPortPath), operationId, { externalConfirmation, }), @@ -527,13 +585,13 @@ export namespace TalerUris { case TalerUriAction.Refund: { // check number of segments if (cs.length < 3) { - return opKnownFailureWithBody(TalerUriParseError.COMPONENTS_LENGTH, { + return Result.errorWithDetail(TalerUriParseError.COMPONENTS_LENGTH, { uriType, }); } if (cs[cs.length - 1]) { // last must be empty - return opKnownFailureWithBody( + return Result.errorWithDetail( TalerUriParseError.INVALID_TARGET_PATH, { pos: 1 as const, @@ -549,7 +607,7 @@ export namespace TalerUris { scheme, ); if (!opts.ignoreComponentError && !merchant) { - return opKnownFailureWithBody( + return Result.errorWithDetail( TalerUriParseError.INVALID_TARGET_PATH, { pos: 0 as const, @@ -561,14 +619,14 @@ export namespace TalerUris { // get order id const orderId = cs[cs.length - 2]; - return opFixedSuccess<URI>( + return Result.of<URI>( createTalerRefund(merchant ?? (cs[0] as HostPortPath), orderId), ); } case TalerUriAction.PayPull: { // check number of segments if (cs.length < 2) { - return opKnownFailureWithBody(TalerUriParseError.COMPONENTS_LENGTH, { + return Result.errorWithDetail(TalerUriParseError.COMPONENTS_LENGTH, { uriType, }); } @@ -580,7 +638,7 @@ export namespace TalerUris { scheme, ); if (!opts.ignoreComponentError && !exchange) { - return opKnownFailureWithBody( + return Result.errorWithDetail( TalerUriParseError.INVALID_TARGET_PATH, { pos: 0 as const, @@ -592,14 +650,14 @@ export namespace TalerUris { // get contract priv const contractPriv = cs[cs.length - 1]; // FIXME: validate private key - return opFixedSuccess<URI>( + return Result.of<URI>( createTalerPayPull(exchange ?? (cs[0] as HostPortPath), contractPriv), ); } case TalerUriAction.PayPush: { // check number of segments if (cs.length < 2) { - return opKnownFailureWithBody(TalerUriParseError.COMPONENTS_LENGTH, { + return Result.errorWithDetail(TalerUriParseError.COMPONENTS_LENGTH, { uriType, }); } @@ -611,7 +669,7 @@ export namespace TalerUris { scheme, ); if (!opts.ignoreComponentError && !exchange) { - return opKnownFailureWithBody( + return Result.errorWithDetail( TalerUriParseError.INVALID_TARGET_PATH, { pos: 0 as const, @@ -624,14 +682,14 @@ export namespace TalerUris { // get contract priv const contractPriv = cs[cs.length - 1]; // FIXME: validate private key - return opFixedSuccess<URI>( + return Result.of<URI>( createTalerPayPush(exchange ?? (cs[0] as HostPortPath), contractPriv), ); } case TalerUriAction.PayTemplate: { // check number of segments if (cs.length < 2) { - return opKnownFailureWithBody(TalerUriParseError.COMPONENTS_LENGTH, { + return Result.errorWithDetail(TalerUriParseError.COMPONENTS_LENGTH, { uriType, }); } @@ -643,7 +701,7 @@ export namespace TalerUris { scheme, ); if (!opts.ignoreComponentError && !merchant) { - return opKnownFailureWithBody( + return Result.errorWithDetail( TalerUriParseError.INVALID_TARGET_PATH, { pos: 0 as const, @@ -656,7 +714,7 @@ export namespace TalerUris { // get contract priv const contractPriv = cs[cs.length - 1]; // FIXME: validate private key - return opFixedSuccess<URI>( + return Result.of<URI>( createTalerPayTemplate( merchant ?? (cs[0] as HostPortPath), contractPriv, @@ -666,7 +724,7 @@ export namespace TalerUris { case TalerUriAction.Restore: { // check number of segments if (cs.length !== 2) { - return opKnownFailureWithBody(TalerUriParseError.COMPONENTS_LENGTH, { + return Result.errorWithDetail(TalerUriParseError.COMPONENTS_LENGTH, { uriType, }); } @@ -689,22 +747,18 @@ export namespace TalerUris { url === withoutScheme ? scheme : isHttp ? "http" : "https"; const [hostname, path] = withoutScheme.split("/", 1); - const host = Paytos.parseHostPortPath2( - hostname, - path, - thisScheme, - )!; + const host = Paytos.parseHostPortPath2(hostname, path, thisScheme)!; providers.push(host); }); - return opFixedSuccess<URI>( + return Result.of<URI>( createTalerRestore(walletPriv ?? (cs[0] as HostPortPath), providers), ); } case TalerUriAction.DevExperiment: { // check number of segments if (cs.length !== 1) { - return opKnownFailureWithBody(TalerUriParseError.COMPONENTS_LENGTH, { + return Result.errorWithDetail(TalerUriParseError.COMPONENTS_LENGTH, { uriType, }); } @@ -712,14 +766,12 @@ export namespace TalerUris { const experimentId = cs[0]; const query = new URLSearchParams(search); - return opFixedSuccess<URI>( - createTalerDevExperiment(experimentId, query), - ); + return Result.of<URI>(createTalerDevExperiment(experimentId, query)); } case TalerUriAction.WithdrawExchange: { // check number of segments if (cs.length < 1) { - return opKnownFailureWithBody(TalerUriParseError.COMPONENTS_LENGTH, { + return Result.errorWithDetail(TalerUriParseError.COMPONENTS_LENGTH, { uriType, }); } @@ -728,7 +780,7 @@ export namespace TalerUris { // if (cs[cs.length-1]) { if (cs.length > 1 && cs[cs.length - 1]) { // last must be empty - return opKnownFailureWithBody( + return Result.errorWithDetail( TalerUriParseError.INVALID_TARGET_PATH, { pos: 1 as const, @@ -744,7 +796,7 @@ export namespace TalerUris { scheme, ); if (!opts.ignoreComponentError && !exchange) { - return opKnownFailureWithBody( + return Result.errorWithDetail( TalerUriParseError.INVALID_TARGET_PATH, { pos: 0 as const, @@ -763,7 +815,7 @@ export namespace TalerUris { amountRes && amountRes.type === "fail" ) { - return opKnownFailureWithBody(TalerUriParseError.INVALID_PARAMETER, { + return Result.errorWithDetail(TalerUriParseError.INVALID_PARAMETER, { name: "a" as const, uriType, error: amountRes, @@ -774,7 +826,7 @@ export namespace TalerUris { ? Amounts.stringify(amountRes.body) : undefined; - return opFixedSuccess<URI>( + return Result.of<URI>( createTalerWithdrawExchange(exchange ?? (cs[0] as HostPortPath), { amount, }), @@ -783,7 +835,7 @@ export namespace TalerUris { case TalerUriAction.AddExchange: { // check number of segments if (cs.length === 1) { - return opKnownFailureWithBody(TalerUriParseError.COMPONENTS_LENGTH, { + return Result.errorWithDetail(TalerUriParseError.COMPONENTS_LENGTH, { uriType, }); } @@ -795,7 +847,7 @@ export namespace TalerUris { scheme, ); if (!opts.ignoreComponentError && !exchange) { - return opKnownFailureWithBody( + return Result.errorWithDetail( TalerUriParseError.INVALID_TARGET_PATH, { pos: 0 as const, @@ -805,13 +857,13 @@ export namespace TalerUris { ); } - return opFixedSuccess<URI>( + return Result.of<URI>( createTalerAddExchange(exchange ?? (cs[0] as HostPortPath)), ); } case TalerUriAction.WithdrawalTransferResult: { if (cs.length === 0) { - return opKnownFailureWithBody(TalerUriParseError.COMPONENTS_LENGTH, { + return Result.errorWithDetail(TalerUriParseError.COMPONENTS_LENGTH, { uriType, }); } @@ -821,7 +873,7 @@ export namespace TalerUris { ? undefined : params["status"]; - return opFixedSuccess<URI>( + return Result.of<URI>( createTalerWithdrawalTransferResult(ref, { status, }), @@ -830,7 +882,7 @@ export namespace TalerUris { case TalerUriAction.AddContact: { // check number of segments if (cs.length < 4) { - return opKnownFailureWithBody(TalerUriParseError.COMPONENTS_LENGTH, { + return Result.errorWithDetail(TalerUriParseError.COMPONENTS_LENGTH, { uriType, }); } @@ -841,7 +893,7 @@ export namespace TalerUris { scheme, ); if (!mailboxBaseUri) { - return opKnownFailureWithBody( + return Result.errorWithDetail( TalerUriParseError.INVALID_TARGET_PATH, { pos: 0 as const, @@ -852,8 +904,14 @@ export namespace TalerUris { } const mailboxIdentity = cs[cs.length - 1]; - return opFixedSuccess<URI>( - createTalerAddContact(cs[0], cs[1], mailboxBaseUri, mailboxIdentity, params["sourceBaseUrl"]), + return Result.of<URI>( + createTalerAddContact( + cs[0], + cs[1], + mailboxBaseUri, + mailboxIdentity, + params["sourceBaseUrl"], + ), ); } default: { @@ -1060,7 +1118,7 @@ export function parseAddContactUriWithError(s: string) { } const mailboxBaseUri = parts[2]; const pathSegments = parts.slice(3, parts.length - 2); - const lastPart = parts[parts.length-1]; + const lastPart = parts[parts.length - 1]; const q = new URLSearchParams(lastPart ?? ""); const mailboxIdentity = lastPart.split("?")[0]; const sourceBaseUrl = q.get("sourceBaseUrl") ?? ""; @@ -1149,7 +1207,7 @@ export enum TalerUriAction { * FIXME: LSD * Add a contact to the wallet */ - AddContact = "add-contact" + AddContact = "add-contact", } interface TalerUriProtoInfo { diff --git a/packages/taler-util/src/taleruris.test.ts b/packages/taler-util/src/taleruris.test.ts @@ -15,18 +15,27 @@ */ import test from "ava"; -import { TalerUriAction, TalerUriParseError, TalerUris } from "./taleruri.js"; import { HostPortPath } from "./payto.js"; +import { Result } from "./result.js"; +import { TalerUriAction, TalerUriParseError, TalerUris } from "./taleruri.js"; import { AmountString } from "./types-taler-common.js"; -import { failOrThrow, succeedOrThrow } from "./operation.js"; /** * 5.1 action: withdraw https://lsd.gnunet.org/lsd0006/#name-action-withdraw */ +function failOrThrow<T>(r: Result<any, any>, e: T): void { + if (r.tag != "error") { + throw Error("expected error"); + } + if (r.error != e) { + throw Error("unexpected error"); + } +} + test("taler-new withdraw uri parsing", (t) => { const url1 = "taler://withdraw/bank.example.com/12345"; - const r1 = succeedOrThrow(TalerUris.fromString(url1)); + const r1 = Result.unpack(TalerUris.fromString(url1)); if (r1.type !== TalerUriAction.Withdraw) { t.fail(); return; @@ -41,7 +50,7 @@ test("taler-new withdraw uri parsing", (t) => { test("taler-new withdraw uri parsing with external confirmation", (t) => { const url1 = "taler://withdraw/bank.example.com/12345?external-confirmation=1"; - const r1 = succeedOrThrow(TalerUris.fromString(url1)); + const r1 = Result.unpack(TalerUris.fromString(url1)); if (r1.type !== TalerUriAction.Withdraw) { t.fail(); return; @@ -56,7 +65,7 @@ test("taler-new withdraw uri parsing with external confirmation", (t) => { test("taler-new withdraw uri parsing (http)", (t) => { const url1 = "taler+http://withdraw/bank.example.com/12345"; - const r1 = succeedOrThrow(TalerUris.fromString(url1)); + const r1 = Result.unpack(TalerUris.fromString(url1)); if (r1.type !== TalerUriAction.Withdraw) { t.fail(); return; @@ -83,7 +92,7 @@ test("taler-new withdraw URI (stringify)", (t) => { */ test("taler-new pay url parsing: defaults", (t) => { const url1 = "taler://pay/example.com/myorder/"; - const r1 = succeedOrThrow(TalerUris.fromString(url1)); + const r1 = Result.unpack(TalerUris.fromString(url1)); if (r1.type !== TalerUriAction.Pay) { t.fail(); return; @@ -92,7 +101,7 @@ test("taler-new pay url parsing: defaults", (t) => { t.is(r1.sessionId, ""); const url2 = "taler://pay/example.com/myorder/mysession"; - const r2 = succeedOrThrow(TalerUris.fromString(url2)); + const r2 = Result.unpack(TalerUris.fromString(url2)); if (r2.type !== TalerUriAction.Pay) { t.fail(); return; @@ -103,7 +112,7 @@ test("taler-new pay url parsing: defaults", (t) => { test("taler-new pay url parsing: instance", (t) => { const url1 = "taler://pay/example.com/instances/myinst/myorder/"; - const r1 = succeedOrThrow(TalerUris.fromString(url1)); + const r1 = Result.unpack(TalerUris.fromString(url1)); if (r1.type !== TalerUriAction.Pay) { t.fail(); return; @@ -117,7 +126,7 @@ test("taler-new pay url parsing: instance", (t) => { test("taler-new pay url parsing (claim token)", (t) => { const url1 = "taler://pay/example.com/instances/myinst/myorder/?c=ASDF"; - const r1 = succeedOrThrow(TalerUris.fromString(url1)); + const r1 = Result.unpack(TalerUris.fromString(url1)); if (r1.type !== TalerUriAction.Pay) { t.fail(); return; @@ -132,7 +141,7 @@ test("taler-new pay url parsing (claim token)", (t) => { test("taler-new pay uri parsing: non-https", (t) => { const url1 = "taler+http://pay/example.com/myorder/"; - const r1 = succeedOrThrow(TalerUris.fromString(url1)); + const r1 = Result.unpack(TalerUris.fromString(url1)); if (r1.type !== TalerUriAction.Pay) { t.fail(); return; @@ -191,7 +200,7 @@ test("taler-new pay URI (stringify with https)", (t) => { test("taler-new refund uri parsing: non-https #1", (t) => { const url1 = "taler+http://refund/example.com/myorder/"; // const r1 = parseRefundUri(url1); - const r1 = succeedOrThrow(TalerUris.fromString(url1)); + const r1 = Result.unpack(TalerUris.fromString(url1)); if (r1.type !== TalerUriAction.Refund) { t.fail(); return; @@ -202,7 +211,7 @@ test("taler-new refund uri parsing: non-https #1", (t) => { test("taler-new refund uri parsing", (t) => { const url1 = "taler://refund/merchant.example.com/1234/"; - const r1 = succeedOrThrow(TalerUris.fromString(url1)); + const r1 = Result.unpack(TalerUris.fromString(url1)); if (r1.type !== TalerUriAction.Refund) { t.fail(); return; @@ -213,7 +222,7 @@ test("taler-new refund uri parsing", (t) => { test("taler-new refund uri parsing with instance", (t) => { const url1 = "taler://refund/merchant.example.com/instances/myinst/1234/"; - const r1 = succeedOrThrow(TalerUris.fromString(url1)); + const r1 = Result.unpack(TalerUris.fromString(url1)); if (r1.type !== TalerUriAction.Refund) { t.fail(); return; @@ -241,7 +250,7 @@ test("taler-new refund URI (stringify)", (t) => { test("taler-new peer to peer push URI", (t) => { const url1 = "taler://pay-push/exch.example.com/foo"; // const r1 = parsePayPushUri(url1); - const r1 = succeedOrThrow(TalerUris.fromString(url1)); + const r1 = Result.unpack(TalerUris.fromString(url1)); if (r1.type !== TalerUriAction.PayPush) { t.fail(); return; @@ -252,7 +261,7 @@ test("taler-new peer to peer push URI", (t) => { test("taler-new peer to peer push URI (path)", (t) => { const url1 = "taler://pay-push/exch.example.com:123/bla/foo"; - const r1 = succeedOrThrow(TalerUris.fromString(url1)); + const r1 = Result.unpack(TalerUris.fromString(url1)); if (r1.type !== TalerUriAction.PayPush) { t.fail(); return; @@ -263,7 +272,7 @@ test("taler-new peer to peer push URI (path)", (t) => { test("taler-new peer to peer push URI (http)", (t) => { const url1 = "taler+http://pay-push/exch.example.com:123/bla/foo"; - const r1 = succeedOrThrow(TalerUris.fromString(url1)); + const r1 = Result.unpack(TalerUris.fromString(url1)); if (r1.type !== TalerUriAction.PayPush) { t.fail(); return; @@ -288,7 +297,7 @@ test("taler-new peer to peer push URI (stringify)", (t) => { test("taler-new peer to peer pull URI", (t) => { const url1 = "taler://pay-pull/exch.example.com/foo"; // const r1 = parsePayPullUri(url1); - const r1 = succeedOrThrow(TalerUris.fromString(url1)); + const r1 = Result.unpack(TalerUris.fromString(url1)); if (r1.type !== TalerUriAction.PayPull) { t.fail(); @@ -300,7 +309,7 @@ test("taler-new peer to peer pull URI", (t) => { test("taler-new peer to peer pull URI (path)", (t) => { const url1 = "taler://pay-pull/exch.example.com:123/bla/foo"; - const r1 = succeedOrThrow(TalerUris.fromString(url1)); + const r1 = Result.unpack(TalerUris.fromString(url1)); if (r1.type !== TalerUriAction.PayPull) { t.fail(); return; @@ -311,7 +320,7 @@ test("taler-new peer to peer pull URI (path)", (t) => { test("taler-new peer to peer pull URI (http)", (t) => { const url1 = "taler+http://pay-pull/exch.example.com:123/bla/foo"; - const r1 = succeedOrThrow(TalerUris.fromString(url1)); + const r1 = Result.unpack(TalerUris.fromString(url1)); if (r1.type !== TalerUriAction.PayPull) { t.fail(); return; @@ -337,7 +346,7 @@ test("taler-new pay template URI (parsing)", (t) => { const url1 = "taler://pay-template/merchant.example.com/FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY"; // const r1 = parsePayTemplateUri(url1); - const r1 = succeedOrThrow(TalerUris.fromString(url1)); + const r1 = Result.unpack(TalerUris.fromString(url1)); if (r1.type !== TalerUriAction.PayTemplate) { t.fail(); @@ -353,7 +362,7 @@ test("taler-new pay template URI (parsing)", (t) => { test("taler-new pay template URI (parsing, http with port)", (t) => { const url1 = "taler+http://pay-template/merchant.example.com:1234/FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY"; - const r1 = succeedOrThrow(TalerUris.fromString(url1)); + const r1 = Result.unpack(TalerUris.fromString(url1)); if (r1.type !== TalerUriAction.PayTemplate) { t.fail(); return; @@ -383,7 +392,7 @@ test("taler-new pay template URI (stringify)", (t) => { test("taler-new restore URI (parsing, http with port)", (t) => { const url1 = "taler+http://restore/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0/prov1.example.com,prov2.example.com:123"; - const r1 = succeedOrThrow(TalerUris.fromString(url1)); + const r1 = Result.unpack(TalerUris.fromString(url1)); // const r1 = parseRestoreUri( // , // ); @@ -401,7 +410,7 @@ test("taler-new restore URI (parsing, http with port)", (t) => { test("taler-new restore URI (parsing, https with port)", (t) => { const url1 = "taler://restore/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0/prov1.example.com,prov2.example.com:234,https%3A%2F%2Fprov1.example.com%2F"; - const r1 = succeedOrThrow(TalerUris.fromString(url1)); + const r1 = Result.unpack(TalerUris.fromString(url1)); if (r1.type !== TalerUriAction.Restore) { t.fail(); @@ -437,7 +446,7 @@ test("taler-new restore URI (stringify)", (t) => { test("taler-new dev exp URI (parsing)", (t) => { const url1 = "taler://dev-experiment/123"; // const r1 = parseDevExperimentUri(url1); - const r1 = succeedOrThrow(TalerUris.fromString(url1)); + const r1 = Result.unpack(TalerUris.fromString(url1)); if (r1.type !== TalerUriAction.DevExperiment) { t.fail(); @@ -485,7 +494,7 @@ test("taler-new withdraw exchange URI (parse)", (t) => { // Now test well-formed URIs { const url1 = "taler://withdraw-exchange/exchange.demo.taler.net/someroot/"; - const r1 = succeedOrThrow(TalerUris.fromString(url1)); + const r1 = Result.unpack(TalerUris.fromString(url1)); if (r1.type !== TalerUriAction.WithdrawExchange) { t.fail(); return; @@ -499,7 +508,7 @@ test("taler-new withdraw exchange URI (parse)", (t) => { { const url1 = "taler://withdraw-exchange/exchange.demo.taler.net/"; - const r1 = succeedOrThrow(TalerUris.fromString(url1)); + const r1 = Result.unpack(TalerUris.fromString(url1)); if (r1.type !== TalerUriAction.WithdrawExchange) { t.fail(); return; @@ -514,7 +523,7 @@ test("taler-new withdraw exchange URI (parse)", (t) => { { const url1 = "taler://withdraw-exchange/exchange.demo.taler.net"; // No trailing slash, no path component - const r1 = succeedOrThrow(TalerUris.fromString(url1)); + const r1 = Result.unpack(TalerUris.fromString(url1)); if (r1.type !== TalerUriAction.WithdrawExchange) { t.fail(); return; @@ -554,7 +563,7 @@ test("taler-new withdraw exchange URI with amount (stringify)", (t) => { test("taler-new add exchange URI (parse)", (t) => { { const url1 = "taler://add-exchange/exchange.example.com/"; - const r1 = succeedOrThrow(TalerUris.fromString(url1)); + const r1 = Result.unpack(TalerUris.fromString(url1)); if (r1.type !== TalerUriAction.AddExchange) { t.fail(); return; @@ -566,7 +575,7 @@ test("taler-new add exchange URI (parse)", (t) => { } { const url1 = "taler://add-exchange/exchanges.example.com/api/"; - const r1 = succeedOrThrow(TalerUris.fromString(url1)); + const r1 = Result.unpack(TalerUris.fromString(url1)); if (r1.type !== TalerUriAction.AddExchange) { t.fail(); return; @@ -595,7 +604,10 @@ test("taler-new add contact URI (stringify)", (t) => { mailboxIdentity: "SOMEHADDR", sourceBaseUrl: "https://taldir.example.com", }); - t.deepEqual(url, "taler://add-contact/email/bob@example.com/mailbox.example.com/mb/SOMEHADDR?sourceBaseUrl=https%3A%2F%2Ftaldir.example.com"); + t.deepEqual( + url, + "taler://add-contact/email/bob@example.com/mailbox.example.com/mb/SOMEHADDR?sourceBaseUrl=https%3A%2F%2Ftaldir.example.com", + ); }); /** diff --git a/packages/taler-wallet-core/src/mailbox.ts b/packages/taler-wallet-core/src/mailbox.ts @@ -21,43 +21,40 @@ */ import { - EmptyObject, + AbsoluteTime, AddMailboxMessageRequest, DeleteMailboxMessageRequest, - MailboxMessagesResponse, + Duration, + EmptyObject, + HttpStatusCode, Logger, - NotificationType, - createEddsaKeyPair, - encodeCrock, MailboxConfiguration, - TalerMailboxInstanceHttpClient, - decodeCrock, - eddsaGetPublic, - sha512, - succeedOrThrow, MailboxMessageRecord, - TalerProtocolTimestamp, - stringToBytes, - hpkeOpenOneshot, - hpkeCreateSecretKey, - SendTalerUriMailboxMessageRequest, - hpkeSealOneshot, - hpkeSecretKeyGetPublic, + MailboxMessagesResponse, MailboxMetadata, MailboxRegisterRequest, MailboxRegisterResult, - eddsaSign, - Duration, - AbsoluteTime, + NotificationType, + Result, + SendTalerUriMailboxMessageRequest, + TalerMailboxInstanceHttpClient, + TalerProtocolTimestamp, TalerSignaturePurpose, - HttpStatusCode, TalerUris, - succeedOrValue, + createEddsaKeyPair, + decodeCrock, + eddsaGetPublic, + eddsaSign, + encodeCrock, + hpkeCreateSecretKey, + hpkeOpenOneshot, + hpkeSealOneshot, + hpkeSecretKeyGetPublic, + sha512, + stringToBytes, + succeedOrThrow, } from "@gnu-taler/taler-util"; -import { - WalletExecutionContext, -} from "./wallet.js"; - +import { WalletExecutionContext } from "./wallet.js"; const logger = new Logger("mailbox.ts"); @@ -70,9 +67,7 @@ export async function addMailboxMessage( ): Promise<EmptyObject> { await wex.db.runReadWriteTx( { - storeNames: [ - "mailboxMessages", - ], + storeNames: ["mailboxMessages"], }, async (tx) => { tx.mailboxMessages.put(req.message); @@ -82,7 +77,7 @@ export async function addMailboxMessage( }); }, ); - return { }; + return {}; } /** @@ -94,19 +89,20 @@ export async function deleteMailboxMessage( ): Promise<EmptyObject> { await wex.db.runReadWriteTx( { - storeNames: [ - "mailboxMessages", - ], + storeNames: ["mailboxMessages"], }, async (tx) => { - tx.mailboxMessages.delete ([req.message.originMailboxBaseUrl, req.message.talerUri]); + tx.mailboxMessages.delete([ + req.message.originMailboxBaseUrl, + req.message.talerUri, + ]); tx.notify({ type: NotificationType.MailboxMessageDeleted, message: req.message, }); }, ); - return { }; + return {}; } /** @@ -118,9 +114,7 @@ export async function listMailboxMessages( ): Promise<MailboxMessagesResponse> { const messages = await wex.db.runReadOnlyTx( { - storeNames: [ - "mailboxMessages", - ], + storeNames: ["mailboxMessages"], }, async (tx) => { return await tx.mailboxMessages.getAll(); @@ -129,7 +123,6 @@ export async function listMailboxMessages( return { messages: messages }; } - /** * Register or update mailbox */ @@ -139,7 +132,9 @@ export async function registerMailbox( ): Promise<MailboxRegisterResult> { const privateSigningKey = decodeCrock(mailboxConf.privateKey); const signingKey = eddsaGetPublic(privateSigningKey); - const encryptionKey = hpkeSecretKeyGetPublic(decodeCrock(mailboxConf.privateEncryptionKey)); + const encryptionKey = hpkeSecretKeyGetPublic( + decodeCrock(mailboxConf.privateEncryptionKey), + ); // Message header: 8 byte + 64 byte SHA512 digest to sign // We hash ktype|key|encKtype|encKey|expiration const messageHeader = new ArrayBuffer(8); @@ -147,16 +142,21 @@ export async function registerMailbox( const vMsg = new DataView(messageHeader); const vExpNbo = new DataView(expNboBuffer); if (mailboxConf.expiration.t_s == "never") { - throw Error("mailbox can not expire, invalid") + throw Error("mailbox can not expire, invalid"); } vExpNbo.setBigUint64(0, BigInt(mailboxConf.expiration.t_s)); - const digestBuffer = new Uint8Array([...stringToBytes("X25519"), - ...encryptionKey, - ...(new Uint8Array(expNboBuffer))]); + const digestBuffer = new Uint8Array([ + ...stringToBytes("X25519"), + ...encryptionKey, + ...new Uint8Array(expNboBuffer), + ]); const digest = sha512(digestBuffer); vMsg.setUint32(0, messageHeader.byteLength + digest.length); vMsg.setUint32(4, TalerSignaturePurpose.MAILBOX_KEYS_UPDATE); - const msgToSign = new Uint8Array([...(new Uint8Array(messageHeader)), ...digest]); + const msgToSign = new Uint8Array([ + ...new Uint8Array(messageHeader), + ...digest, + ]); const signature = eddsaSign(msgToSign, privateSigningKey); const info: MailboxMetadata = { signing_key: encodeCrock(signingKey), @@ -169,7 +169,10 @@ export async function registerMailbox( mailbox_metadata: info, signature: encodeCrock(signature), }; - const mailboxClient = new TalerMailboxInstanceHttpClient(mailboxConf.mailboxBaseUrl, wex.http); + const mailboxClient = new TalerMailboxInstanceHttpClient( + mailboxConf.mailboxBaseUrl, + wex.http, + ); const resp = await mailboxClient.registerMailbox(req); switch (resp.case) { @@ -191,9 +194,7 @@ export async function getMailbox( ): Promise<MailboxConfiguration | undefined> { return await wex.db.runReadOnlyTx( { - storeNames: [ - "mailboxConfigurations", - ], + storeNames: ["mailboxConfigurations"], }, async (tx) => { return await tx.mailboxConfigurations.get(mailboxBaseUrl); @@ -201,7 +202,6 @@ export async function getMailbox( ); } - /** * Create new mailbox configuration locally and * try to register it. @@ -213,21 +213,30 @@ export async function createNewMailbox( const keys = createEddsaKeyPair(); const hpkeKey = encodeCrock(hpkeCreateSecretKey()); const privKey = encodeCrock(keys.eddsaPriv); - const nowInAYear = AbsoluteTime.addDuration(AbsoluteTime.now(), Duration.fromSpec({ - years: 1})); + const nowInAYear = AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromSpec({ + years: 1, + }), + ); const mailboxConf: MailboxConfiguration = { mailboxBaseUrl: mailboxBaseUrl, privateKey: privKey, privateEncryptionKey: hpkeKey, - expiration: AbsoluteTime.toProtocolTimestamp(nowInAYear) + expiration: AbsoluteTime.toProtocolTimestamp(nowInAYear), }; const resp = await registerMailbox(wex, mailboxConf); if (resp.status == "payment-required") { if (!resp.talerUri) { - throw Error("payment required to register mailbox but no Taler URI given"); + throw Error( + "payment required to register mailbox but no Taler URI given", + ); } - mailboxConf.payUri = succeedOrValue(TalerUris.fromString(resp.talerUri), undefined); + mailboxConf.payUri = Result.orElse( + TalerUris.fromString(resp.talerUri), + undefined, + ); } await wex.db.runReadWriteTx( { @@ -243,22 +252,24 @@ export async function createNewMailbox( function decryptTalerUriMessage( sk: Uint8Array, msg: Uint8Array, -) : string | undefined { +): string | undefined { const header = new Uint8Array(msg.slice(0, 4)); const ct = new Uint8Array(msg.slice(4)); - const uri = hpkeOpenOneshot(sk, - stringToBytes("mailbox-message"), - header, // AAD - ct); + const uri = hpkeOpenOneshot( + sk, + stringToBytes("mailbox-message"), + header, // AAD + ct, + ); if (!uri) { return undefined; } // Find start of padding const padIdx = uri.indexOf(0x00); if (-1 === padIdx) { - return new TextDecoder().decode(uri); + return new TextDecoder().decode(uri); } - return new TextDecoder().decode(uri.slice(0, padIdx)) + return new TextDecoder().decode(uri.slice(0, padIdx)); } /** @@ -268,7 +279,10 @@ export async function refreshMailbox( wex: WalletExecutionContext, mailboxConf: MailboxConfiguration, ): Promise<MailboxMessageRecord[]> { - const mailboxClient = new TalerMailboxInstanceHttpClient(mailboxConf.mailboxBaseUrl, wex.http); + const mailboxClient = new TalerMailboxInstanceHttpClient( + mailboxConf.mailboxBaseUrl, + wex.http, + ); const privKey = decodeCrock(mailboxConf.privateKey); const pubKey = eddsaGetPublic(privKey); const hAddress = encodeCrock(sha512(pubKey)); @@ -282,15 +296,17 @@ export async function refreshMailbox( default: throw Error("unable to get mailbox service config"); } - const res = await mailboxClient.getMessages({hMailbox: hAddress}); + const res = await mailboxClient.getMessages({ hMailbox: hAddress }); switch (res.case) { case "ok": const hpkeSk: Uint8Array = decodeCrock(mailboxConf.privateEncryptionKey); if (res.body) { const messages = res.body.messages; const now = TalerProtocolTimestamp.now(); - if ((messages.byteLength % message_size) !== 0) { - throw Error(`mailbox messages response not a multiple of message size! (${messages.byteLength} % ${message_size} != 0)`); + if (messages.byteLength % message_size !== 0) { + throw Error( + `mailbox messages response not a multiple of message size! (${messages.byteLength} % ${message_size} != 0)`, + ); } // FIXME: if we have reached the maximum number of // messages that the service returns at a time, @@ -300,10 +316,8 @@ export async function refreshMailbox( const records: MailboxMessageRecord[] = []; for (let i = 0; i < numMessages; i++) { const offset = i * message_size; - const msg: Uint8Array = messages.slice(offset, - offset + message_size); - const uri = decryptTalerUriMessage(hpkeSk, - msg); + const msg: Uint8Array = messages.slice(offset, offset + message_size); + const uri = decryptTalerUriMessage(hpkeSk, msg); if (!uri) { logger.warn(`unable to decrypt message number ${i}`); continue; @@ -315,7 +329,7 @@ export async function refreshMailbox( downloadedAt: now, }; records.push(newMessage); - await addMailboxMessage(wex, {message: newMessage}); + await addMailboxMessage(wex, { message: newMessage }); } // Message header: 8 byte + 64 byte SHA512 digest to sign // We hash all messages @@ -326,14 +340,18 @@ export async function refreshMailbox( vMsg.setUint32(8, parseInt(res.body.etag)); vMsg.setUint32(12, numMessages); const msgToSign: Uint8Array = new Uint8Array(messageBuffer); - const privateSigningKey: Uint8Array = decodeCrock(mailboxConf.privateKey); + const privateSigningKey: Uint8Array = decodeCrock( + mailboxConf.privateKey, + ); const signature = eddsaSign(msgToSign, privateSigningKey); - succeedOrThrow(await mailboxClient.deleteMessages({ - mailboxConf: mailboxConf, - matchIf: res.body.etag, - count: numMessages, - signature: encodeCrock(signature), - })); + succeedOrThrow( + await mailboxClient.deleteMessages({ + mailboxConf: mailboxConf, + matchIf: res.body.etag, + count: numMessages, + signature: encodeCrock(signature), + }), + ); return records; } return []; // No new messages; @@ -345,8 +363,8 @@ export async function refreshMailbox( function encryptTalerUriMessage( encryptionKey: Uint8Array, talerUri: string, - paddedMessageSize: number -) : Uint8Array { + paddedMessageSize: number, +): Uint8Array { const headerBuf = new ArrayBuffer(4); // Padding must not include HPKE tag and encapsulation overhead // FIXME CRYPTO-AGILITY size of tag and encapsulation depends on used algos @@ -357,17 +375,23 @@ function encryptTalerUriMessage( const header = new Uint8Array(headerBuf); const padding = new Uint8Array(paddingLength).fill(0); const msg = new Uint8Array([...stringToBytes(talerUri), ...padding]); - const encryptedMessage = hpkeSealOneshot(encryptionKey, - stringToBytes("mailbox-message"), - header, - msg); - return new Uint8Array([...header, ...encryptedMessage]) + const encryptedMessage = hpkeSealOneshot( + encryptionKey, + stringToBytes("mailbox-message"), + header, + msg, + ); + return new Uint8Array([...header, ...encryptedMessage]); } export async function sendTalerUriMessage( wex: WalletExecutionContext, - req: SendTalerUriMailboxMessageRequest) : Promise<EmptyObject> { - const mailboxClient = new TalerMailboxInstanceHttpClient(req.contact.mailboxBaseUri, wex.http); + req: SendTalerUriMailboxMessageRequest, +): Promise<EmptyObject> { + const mailboxClient = new TalerMailboxInstanceHttpClient( + req.contact.mailboxBaseUri, + wex.http, + ); // Get message size var paddedMessageSize; const resConf = await mailboxClient.getConfig(); @@ -378,7 +402,9 @@ export async function sendTalerUriMessage( default: return {}; } - const resKeys = await mailboxClient.getMailboxInfo(req.contact.mailboxAddress); + const resKeys = await mailboxClient.getMailboxInfo( + req.contact.mailboxAddress, + ); var keys; switch (resKeys.case) { case "ok": @@ -387,9 +413,11 @@ export async function sendTalerUriMessage( default: throw Error("unable to get mailbox keys"); } - const encryptedMessage = encryptTalerUriMessage(decodeCrock(keys.encryption_key), - req.talerUri, - paddedMessageSize); + const encryptedMessage = encryptTalerUriMessage( + decodeCrock(keys.encryption_key), + req.talerUri, + paddedMessageSize, + ); const resSend = await mailboxClient.sendMessage({ h_address: req.contact.mailboxAddress, body: encryptedMessage, diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts @@ -289,6 +289,7 @@ import { readSuccessResponseJsonOrThrow, type HttpRequestLibrary, } from "@gnu-taler/taler-util/http"; +import { Result } from "../../taler-util/src/result.js"; import { getUserAttentions, getUserAttentionsUnreadCount, @@ -1977,10 +1978,10 @@ export async function handleConvertIbanAccountFieldToPayto( const strippedInput = req.value.replace(/[- ]/g, ""); if (req.currency === "HUF") { const iban = convertHUF_BBANtoIBAN(strippedInput); - if (iban.type === "ok") { + if (Result.isOk(iban)) { return { ok: true, - paytoUri: `payto://iban/${iban.body}`, + paytoUri: `payto://iban/${iban.value}`, type: "iban", }; } else { @@ -1991,10 +1992,10 @@ export async function handleConvertIbanAccountFieldToPayto( } if (wex.ws.devExperimentState.fakeChfBban && req.currency === "CHF") { const iban = convertCHF_BBANtoIBAN(strippedInput); - if (iban.type === "ok") { + if (Result.isOk(iban)) { return { ok: true, - paytoUri: `payto://iban/${iban.body}`, + paytoUri: `payto://iban/${iban.value}`, type: "iban", }; } else { @@ -2004,17 +2005,16 @@ export async function handleConvertIbanAccountFieldToPayto( } } const parsedIban = parseIban(strippedInput); - switch (parsedIban.case) { - case "ok": - return { - ok: true, - paytoUri: `payto://iban/${strippedInput}`, - type: "iban", - }; - default: - return { - ok: false, - }; + if (Result.isOk(parsedIban)) { + return { + ok: true, + paytoUri: `payto://iban/${strippedInput}`, + type: "iban", + }; + } else { + return { + ok: false, + }; } } @@ -2022,7 +2022,7 @@ export async function handleConvertIbanPaytoToAccountField( wex: WalletExecutionContext, req: ConvertIbanPaytoToAccountFieldRequest, ): Promise<ConvertIbanPaytoToAccountFieldResponse> { - const payto = succeedOrThrow(Paytos.fromString(req.paytoUri)); + const payto = Result.unpack(Paytos.fromString(req.paytoUri)); const iban = payto.normalizedPath; if (iban.startsWith("HU")) { let bban = iban.slice(4); diff --git a/packages/taler-wallet-webextension/src/wallet/Mailbox.tsx b/packages/taler-wallet-webextension/src/wallet/Mailbox.tsx @@ -15,26 +15,28 @@ */ import { - assertUnreachable, - ScopeInfo, + MailboxConfiguration, + MailboxMessageRecord, NotificationType, + Result, + ScopeInfo, + TalerMailboxInstanceHttpClient, + TalerProtocolTimestamp, + TalerUri, + TalerUriAction, + TalerUris, + TranslatedString, + assertUnreachable, decodeCrock, eddsaGetPublic, encodeCrock, - MailboxConfiguration, - TalerMailboxInstanceHttpClient, sha512, - MailboxMessageRecord, - TranslatedString, - succeedOrValue, - TalerProtocolTimestamp, - TalerUris, - TalerUriAction, - TalerUri } from "@gnu-taler/taler-util"; -import { SafeHandler } from "../mui/handlers.js"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { BrowserFetchHttpLib, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + BrowserFetchHttpLib, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useEffect, useState } from "preact/hooks"; import { ErrorAlertView } from "../components/CurrentAlerts.js"; @@ -42,8 +44,8 @@ import { Loading } from "../components/Loading.js"; import { BoldLight, Centered, - SmallText, LightText, + SmallText, } from "../components/styled/index.js"; import { alertFromError, useAlertContext } from "../context/alert.js"; import { useBackendContext } from "../context/backend.js"; @@ -53,7 +55,7 @@ import { Button } from "../mui/Button.js"; import { Grid } from "../mui/Grid.js"; import { Paper } from "../mui/Paper.js"; import { TextField } from "../mui/TextField.js"; -import { TextFieldHandler } from "../mui/handlers.js"; +import { SafeHandler, TextFieldHandler } from "../mui/handlers.js"; interface Props { scope?: ScopeInfo; @@ -75,18 +77,25 @@ export function MailboxPage({ var mailboxClient: TalerMailboxInstanceHttpClient; var activeMailbox: MailboxConfiguration | undefined; const httpClient = new BrowserFetchHttpLib(); - mailboxClient = new TalerMailboxInstanceHttpClient(mailboxBaseUrl, httpClient); + mailboxClient = new TalerMailboxInstanceHttpClient( + mailboxBaseUrl, + httpClient, + ); const state = useAsyncAsHook(async () => { const b = await api.wallet.call(WalletApiOperation.GetMailboxMessages, {}); - activeMailbox = await api.wallet.call(WalletApiOperation.GetMailbox, mailboxBaseUrl); + activeMailbox = await api.wallet.call( + WalletApiOperation.GetMailbox, + mailboxBaseUrl, + ); var mailboxUrl; if (activeMailbox) { - // FIXME put this into the mailbox config directly? + // FIXME put this into the mailbox config directly? const privKey = decodeCrock(activeMailbox.privateKey); const pubKey = eddsaGetPublic(privKey); - mailboxUrl = activeMailbox.mailboxBaseUrl + '/' + encodeCrock(sha512(pubKey)); + mailboxUrl = + activeMailbox.mailboxBaseUrl + "/" + encodeCrock(sha512(pubKey)); } return { messages: b.messages, @@ -103,7 +112,10 @@ export function MailboxPage({ useEffect(() => { return api.listener.onUpdateNotification( - [NotificationType.MailboxMessageAdded, NotificationType.MailboxMessageDeleted], + [ + NotificationType.MailboxMessageAdded, + NotificationType.MailboxMessageDeleted, + ], state?.retry, ); }); @@ -120,7 +132,10 @@ export function MailboxPage({ } const onInitializeMailbox = async () => { try { - await api.wallet.call(WalletApiOperation.InitializeMailbox, mailboxBaseUrl); + await api.wallet.call( + WalletApiOperation.InitializeMailbox, + mailboxBaseUrl, + ); // FIXME the returned mailbox may have a payto URI set // In that case we need to display the option to pay AND // Properly check on reload if we can clear the property (and store the @@ -128,12 +143,16 @@ export function MailboxPage({ // https://bugs.gnunet.org/view.php?id=10605 state?.retry(); } catch (err) { - setError(i18n.str`Unexpected error when trying to initialize mailbox: ${err}`); + setError( + i18n.str`Unexpected error when trying to initialize mailbox: ${err}`, + ); } }; const onDeleteMessage = async (m: MailboxMessageRecord) => { try { - await api.wallet.call(WalletApiOperation.DeleteMailboxMessage, { message: m }); + await api.wallet.call(WalletApiOperation.DeleteMailboxMessage, { + message: m, + }); } catch (e) { setError(i18n.str`Unexpected error when trying to delete message: ${e}`); } @@ -145,9 +164,11 @@ export function MailboxPage({ } await api.wallet.call(WalletApiOperation.RefreshMailbox, mailbox); } catch (err) { - setError(i18n.str`Unexpected error when trying to fetch messages: ${err}`); + setError( + i18n.str`Unexpected error when trying to fetch messages: ${err}`, + ); } - }; + }; return ( <MessagesView search={{ @@ -174,11 +195,13 @@ interface MessageProps { onTalerUri: (url: string) => void; } - function MailboxMessageLayout(props: MessageProps): VNode { const { i18n } = useTranslationContext(); - const uri = succeedOrValue(TalerUris.fromString(props.message.talerUri), undefined); - function toDateString(t: TalerProtocolTimestamp) : string { + const uri = Result.orElse( + TalerUris.fromString(props.message.talerUri), + undefined, + ); + function toDateString(t: TalerProtocolTimestamp): string { if (t.t_s === "never") { return t.t_s; } @@ -187,12 +210,15 @@ function MailboxMessageLayout(props: MessageProps): VNode { return ( <Paper style={{ padding: 8 }}> <p> - <span><i18n.Translate>Date</i18n.Translate>: {toDateString(props.message.downloadedAt)}</span> - <SmallText style={{ marginTop: 5 }}> + <span> + <i18n.Translate>Date</i18n.Translate>:{" "} + {toDateString(props.message.downloadedAt)} + </span> + <SmallText style={{ marginTop: 5 }}> <i18n.Translate>URI</i18n.Translate>: {props.message.talerUri} </SmallText> </p> - {uri && + {uri && ( <Button variant="contained" color="success" @@ -205,9 +231,7 @@ function MailboxMessageLayout(props: MessageProps): VNode { case TalerUriAction.Pay: return <i18n.Translate>Pay invoice</i18n.Translate>; case TalerUriAction.Withdraw: - return ( - <i18n.Translate>Withdrawal from bank</i18n.Translate> - ); + return <i18n.Translate>Withdrawal from bank</i18n.Translate>; case TalerUriAction.Refund: return <i18n.Translate>Claim refund</i18n.Translate>; case TalerUriAction.PayPull: @@ -219,21 +243,13 @@ function MailboxMessageLayout(props: MessageProps): VNode { case TalerUriAction.Restore: return <i18n.Translate>Restore wallet</i18n.Translate>; case TalerUriAction.DevExperiment: - return ( - <i18n.Translate>Enable experiment</i18n.Translate> - ); + return <i18n.Translate>Enable experiment</i18n.Translate>; case TalerUriAction.WithdrawExchange: - return ( - <i18n.Translate> - Withdraw from exchange - </i18n.Translate> - ); + return <i18n.Translate>Withdraw from exchange</i18n.Translate>; case TalerUriAction.AddExchange: return <i18n.Translate>Add exchange</i18n.Translate>; case TalerUriAction.WithdrawalTransferResult: - return ( - <i18n.Translate>Notify transaction</i18n.Translate> - ); + return <i18n.Translate>Notify transaction</i18n.Translate>; case TalerUriAction.AddContact: return <i18n.Translate>Add contact</i18n.Translate>; default: { @@ -242,13 +258,18 @@ function MailboxMessageLayout(props: MessageProps): VNode { } })(uri)} </Button> - } - <Button variant="contained" onClick={() => { return props.onDeleteMessage(props.message)}} color="error"> + )} + <Button + variant="contained" + onClick={() => { + return props.onDeleteMessage(props.message); + }} + color="error" + > <i18n.Translate>Delete</i18n.Translate> </Button> </Paper> - ) - + ); } export function MessagesView({ @@ -287,66 +308,74 @@ export function MessagesView({ <section> <p>{error && <Alert severity="error">{error}</Alert>}</p> {mailboxUrl ? ( - <p> - <Alert severity="info"> - <i18n.Translate> - You may copy your mailbox URI from below and register it at <a href="https://taldir.gnunet.org">the Taler Directory</a> with your alias(es) to make it easier for others to find you. - </i18n.Translate> - {(showing) ? ( - <div> + <p> + <Alert severity="info"> + <i18n.Translate> + You may copy your mailbox URI from below and register it at{" "} + <a href="https://taldir.gnunet.org">the Taler Directory</a> with + your alias(es) to make it easier for others to find you. + </i18n.Translate> + {showing ? ( + <div> + <SmallText style={{ marginTop: 5 }}> + <LightText>{mailboxUrl}</LightText> + </SmallText> + <Button onClick={copy as SafeHandler<void>}> + <i18n.Translate>copy uri</i18n.Translate> + </Button> + <Button onClick={toggle as SafeHandler<void>}> + <i18n.Translate>hide uri</i18n.Translate> + </Button> + </div> + ) : ( + <div> + <Button onClick={copy as SafeHandler<void>}> + <i18n.Translate>copy uri</i18n.Translate> + </Button> + <Button onClick={toggle as SafeHandler<void>}> + <i18n.Translate>show uri</i18n.Translate> + </Button> + </div> + )} + </Alert> + <br /> + <Centered style={{ margin: 10 }}> + <Button + variant="contained" + onClick={() => { + return onFetchMessages(mailbox); + }} + > + <i18n.Translate>Fetch messages</i18n.Translate> + </Button> + </Centered> + </p> + ) : ( + <Centered style={{ margin: 10 }}> + <p> <SmallText style={{ marginTop: 5 }}> <LightText> - { mailboxUrl } + <i18n.Translate>Mailbox not initialized</i18n.Translate> </LightText> </SmallText> - <Button onClick={copy as SafeHandler<void>}> - <i18n.Translate>copy uri</i18n.Translate> - </Button> - <Button onClick={toggle as SafeHandler<void>}> - <i18n.Translate>hide uri</i18n.Translate> - </Button> - </div> - ) : ( - <div> - <Button onClick={copy as SafeHandler<void>}> - <i18n.Translate>copy uri</i18n.Translate> - </Button> - <Button onClick={toggle as SafeHandler<void>}> - <i18n.Translate>show uri</i18n.Translate> - </Button> - </div> - )} - </Alert> - <br/> - <Centered style={{ margin: 10 }}> - <Button variant="contained" onClick={() => { return onFetchMessages(mailbox) }}> - <i18n.Translate>Fetch messages</i18n.Translate> + </p> + <Button + variant="contained" + onClick={() => { + return onInitializeMailbox(); + }} + > + <i18n.Translate>Initialize mailbox</i18n.Translate> </Button> </Centered> - </p> - ) : ( - <Centered style={{ margin: 10 }}> - <p> - <SmallText style={{ marginTop: 5 }}> - <LightText> - <i18n.Translate>Mailbox not initialized</i18n.Translate> - </LightText> - </SmallText> - </p> - <Button variant="contained" onClick={() => { return onInitializeMailbox() }}> - <i18n.Translate>Initialize mailbox</i18n.Translate> - </Button> - </Centered> - )} - {(messages.length == 0) ? ( + {messages.length == 0 ? ( <Centered style={{ marginTop: 20 }}> <BoldLight> <i18n.Translate>No messages yet.</i18n.Translate> </BoldLight> </Centered> - ) : - ( + ) : ( <TextField label="Search" variant="filled" @@ -358,8 +387,7 @@ export function MessagesView({ /> )} <Grid item container columns={1} spacing={1} style={{ marginTop: 20 }}> - { - messages.map((m, i) => ( + {messages.map((m, i) => ( <Grid item xs={1}> <MailboxMessageLayout message={m} @@ -367,12 +395,9 @@ export function MessagesView({ onTalerUri={onTalerUri} /> </Grid> - )) - } + ))} </Grid> - </section> + </section> </Fragment> ); } - - diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx b/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx @@ -28,6 +28,7 @@ import { PaytoUriBitcoin, PaytoUriIBAN, PaytoUriTalerBank, + ResultError, TranslatedString, WalletBankAccountInfo, WireTypeDetails, @@ -532,10 +533,10 @@ type FailCasesOf<T extends (...args: any) => any> = Exclude< >; function translateIbanError( - result: FailCasesOf<typeof parseIban>, + result: ResultError<ParseIbanError>, i18n: InternationalizationAPI, ): TranslatedString { - switch (result.case) { + switch (result.error) { case ParseIbanError.UNSUPPORTED_COUNTRY: return i18n.str`Unsupported country.`; case ParseIbanError.TOO_LONG: @@ -680,7 +681,7 @@ function validateIBAN( details.preferredEntryType === "bban" ? convertHUF_BBANtoIBAN(iban) // FIXME: this only works for HU : parseIban(iban); - if (result.type === "ok") { + if (result.tag === "ok") { return undefined; } return translateIbanError(result, i18n); diff --git a/packages/taler-wallet-webextension/src/wallet/QrReader.tsx b/packages/taler-wallet-webextension/src/wallet/QrReader.tsx @@ -17,9 +17,8 @@ import { assertUnreachable, HttpStatusCode, - OperationOk, - succeedOrThrow, - succeedOrValue, + Result, + ResultOk, TalerCoreBankHttpClient, TalerError, TalerExchangeHttpClient, @@ -28,7 +27,7 @@ import { TalerUriAction, TalerUriParseError, TalerUris, - TranslatedString + TranslatedString, } from "@gnu-taler/taler-util"; import { BrowserFetchHttpLib, @@ -244,14 +243,15 @@ const testValidUriDebounced = debounce(testValidUri); type FailCasesOf<T extends (...args: any) => any> = Exclude< ReturnType<T>, - OperationOk<any> + ResultOk<any> >; +// FIXME: This belongs into some util library, should be tested. function translateTalerUriError( result: FailCasesOf<typeof TalerUris.fromString>, i18n: InternationalizationAPI, ): TranslatedString { - switch (result.case) { + switch (result.error) { case TalerUriParseError.WRONG_PREFIX: return i18n.str`URI is not valid. Taler URI should start with "taler://"`; case TalerUriParseError.INCOMPLETE: @@ -259,7 +259,7 @@ function translateTalerUriError( case TalerUriParseError.UNSUPPORTED: return i18n.str`This URI type is not supported`; case TalerUriParseError.COMPONENTS_LENGTH: { - switch (result.body.uriType) { + switch (result.detail.uriType) { case TalerUriAction.Withdraw: return i18n.str`This URI requires the bank host and operation id separated by /`; case TalerUriAction.Pay: @@ -284,16 +284,18 @@ function translateTalerUriError( return i18n.str`This URI requires the exchange host`; case TalerUriAction.WithdrawalTransferResult: return i18n.str`This URI requires string after the first /`; + default: + return assertUnreachable(result.detail.uriType); } } case TalerUriParseError.INVALID_TARGET_PATH: { - switch (result.body.uriType) { + switch (result.detail.uriType) { case TalerUriAction.Withdraw: return i18n.str`The bank host is invalid`; case TalerUriAction.Pay: return i18n.str`The merchant host is invalid`; case TalerUriAction.Refund: { - switch (result.body.pos) { + switch (result.detail.pos) { case 0: return i18n.str`The merchant host is invalid`; case 1: @@ -311,16 +313,20 @@ function translateTalerUriError( case TalerUriAction.AddContact: return i18n.str`The contact is invalid`; case TalerUriAction.WithdrawExchange: - switch (result.body.pos) { + switch (result.detail.pos) { case 0: return i18n.str`The exchange host is invalid`; case 1: return i18n.str`The URI should end with /`; + default: + return assertUnreachable(result.detail); } + default: + return assertUnreachable(result.detail); } } case TalerUriParseError.INVALID_PARAMETER: { - switch (result.body.uriType) { + switch (result.detail.uriType) { case TalerUriAction.WithdrawExchange: return i18n.str`The amount is invalid`; case TalerUriAction.Withdraw: @@ -331,7 +337,12 @@ function translateTalerUriError( case TalerUriAction.PayTemplate: case TalerUriAction.AddExchange: case TalerUriAction.AddContact: + case TalerUriAction.DevExperiment: + case TalerUriAction.Restore: + case TalerUriAction.WithdrawalTransferResult: return i18n.str`A parameter is invalid`; + default: + return assertUnreachable(result.detail.uriType); } } } @@ -351,12 +362,12 @@ export function QrReaderPage({ onDetected }: Props): VNode { const lstr = str.toLowerCase(); const uriResp = TalerUris.fromString(lstr); - if (uriResp.type === "fail") { + if (uriResp.tag === "error") { setError(translateTalerUriError(uriResp, i18n)); setValue(str); return; } - const { body: uri } = uriResp; + const { value: uri } = uriResp; setError(i18n.str`checking...`); const errorMsg = await testValidUriDebounced(uri, i18n); if (errorMsg) { @@ -378,12 +389,12 @@ export function QrReaderPage({ onDetected }: Props): VNode { const lstr = str.toLowerCase(); const uriResp = TalerUris.fromString(lstr); - if (uriResp.type === "fail") { + if (uriResp.tag === "error") { setError(translateTalerUriError(uriResp, i18n)); setValue(str); return; } - const { body: uri } = uriResp; + const { value: uri } = uriResp; setError(i18n.str`checking...`); setValue(str); @@ -447,7 +458,10 @@ export function QrReaderPage({ onDetected }: Props): VNode { setError(i18n.str`Unexpected error happen reading the file: ${error}`); } } - const uri = succeedOrValue(TalerUris.fromString(value.toLowerCase()), undefined); + const uri = Result.orElse( + TalerUris.fromString(value.toLowerCase()), + undefined, + ); return ( <Container> diff --git a/packages/web-util/src/hooks/useNotifications.ts b/packages/web-util/src/hooks/useNotifications.ts @@ -16,8 +16,6 @@ import { memoryMap, useTranslationContext, } from "../index.browser.js"; -import { VNode } from "preact"; -import { format } from "date-fns"; export type NotificationMessage = ErrorNotification | InfoNotification; @@ -159,21 +157,13 @@ export type FunctionThatMayFail<T extends any[]> = ( ...args: T ) => Promise<NotificationMessage | undefined>; -type FunctionWrapperForButton = <T extends any[], R>( - fn: FunctionThatMayFail<T>, -) => FunctionThatMayFail<T>; - -type ReplaceReturnType<T extends (...a: any) => any, TNewReturn> = ( - ...a: Parameters<T> -) => TNewReturn; - /** * Initialize a notification handler. - * @returns a tuple of notification and setter -1) notification that may be set by a function when it fails. + * @returns a tuple of notification and setter + * 1) notification that may be set by a function when it fails. * 2) a error handling function that converts a function that returns a message * into a function that will set the notification. - * + * */ export function useLocalNotificationBetter(): [ Notification | undefined,