aboutsummaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/aml-backoffice-ui/package.json2
-rw-r--r--packages/anastasis-cli/package.json2
-rw-r--r--packages/anastasis-core/package.json2
-rw-r--r--packages/anastasis-webui/package.json2
-rw-r--r--packages/auditor-backoffice-ui/package.json2
-rw-r--r--packages/bank-ui/package.json2
-rw-r--r--packages/bank-ui/src/hooks/preferences.ts2
-rw-r--r--packages/bank-ui/src/pages/BankFrame.tsx1
-rw-r--r--packages/challenger-ui/package.json2
-rw-r--r--packages/challenger-ui/src/Routing.tsx2
-rw-r--r--packages/challenger-ui/src/context/preferences.ts87
-rw-r--r--packages/challenger-ui/src/declaration.d.ts35
-rw-r--r--packages/challenger-ui/src/hooks/session.ts17
-rw-r--r--packages/challenger-ui/src/pages/AnswerChallenge.tsx114
-rw-r--r--packages/challenger-ui/src/pages/AskChallenge.tsx58
-rw-r--r--packages/challenger-ui/src/pages/Frame.tsx151
-rw-r--r--packages/idb-bridge/package.json2
-rw-r--r--packages/merchant-backend-ui/package.json2
-rw-r--r--packages/merchant-backoffice-ui/package.json2
-rw-r--r--packages/merchant-backoffice-ui/src/i18n/es.po244
-rw-r--r--packages/pogen/package.json2
-rw-r--r--packages/taler-harness/debian/changelog6
-rw-r--r--packages/taler-harness/package.json2
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-external.ts101
-rw-r--r--packages/taler-harness/src/integrationtests/testrunner.ts4
-rw-r--r--packages/taler-util/package.json2
-rw-r--r--packages/taler-util/src/http-client/challenger.ts4
-rw-r--r--packages/taler-util/src/http-client/types.ts61
-rw-r--r--packages/taler-util/src/operation.ts51
-rw-r--r--packages/taler-util/src/qr.ts7
-rw-r--r--packages/taler-util/src/taler-types.ts3
-rw-r--r--packages/taler-util/src/taleruri.test.ts12
-rw-r--r--packages/taler-util/src/taleruri.ts10
-rw-r--r--packages/taler-util/src/transactions-types.ts5
-rw-r--r--packages/taler-util/src/wallet-types.ts24
-rw-r--r--packages/taler-wallet-cli/debian/changelog12
-rw-r--r--packages/taler-wallet-cli/package.json2
-rw-r--r--packages/taler-wallet-cli/src/index.ts9
-rw-r--r--packages/taler-wallet-core/package.json2
-rw-r--r--packages/taler-wallet-core/src/db.ts103
-rw-r--r--packages/taler-wallet-core/src/exchanges.ts18
-rw-r--r--packages/taler-wallet-core/src/transactions.ts5
-rw-r--r--packages/taler-wallet-core/src/wallet-api-types.ts16
-rw-r--r--packages/taler-wallet-core/src/wallet.ts991
-rw-r--r--packages/taler-wallet-core/src/withdraw.ts36
-rw-r--r--packages/taler-wallet-embedded/package.json2
-rw-r--r--packages/taler-wallet-webextension/manifest-common.json4
-rw-r--r--packages/taler-wallet-webextension/package.json2
-rw-r--r--packages/web-util/package.json2
49 files changed, 1569 insertions, 660 deletions
diff --git a/packages/aml-backoffice-ui/package.json b/packages/aml-backoffice-ui/package.json
index 565d1c68a..c3549ef52 100644
--- a/packages/aml-backoffice-ui/package.json
+++ b/packages/aml-backoffice-ui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/aml-backoffice-ui",
- "version": "0.12.1",
+ "version": "0.12.2",
"author": "sebasjm",
"license": "AGPL-3.0-OR-LATER",
"description": "Back-office SPA for GNU Taler Exchange.",
diff --git a/packages/anastasis-cli/package.json b/packages/anastasis-cli/package.json
index 01ff9fc69..47d1505d1 100644
--- a/packages/anastasis-cli/package.json
+++ b/packages/anastasis-cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/anastasis-cli",
- "version": "0.12.1",
+ "version": "0.12.2",
"description": "",
"engines": {
"node": ">=0.18.0"
diff --git a/packages/anastasis-core/package.json b/packages/anastasis-core/package.json
index 3e0f49ccb..c89b8eecc 100644
--- a/packages/anastasis-core/package.json
+++ b/packages/anastasis-core/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/anastasis-core",
- "version": "0.12.1",
+ "version": "0.12.2",
"description": "",
"main": "./lib/index.js",
"module": "./lib/index.js",
diff --git a/packages/anastasis-webui/package.json b/packages/anastasis-webui/package.json
index ee7b3bf4e..17e8e74fc 100644
--- a/packages/anastasis-webui/package.json
+++ b/packages/anastasis-webui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/anastasis-webui",
- "version": "0.12.1",
+ "version": "0.12.2",
"license": "MIT",
"type": "module",
"scripts": {
diff --git a/packages/auditor-backoffice-ui/package.json b/packages/auditor-backoffice-ui/package.json
index c64110357..bbebabf39 100644
--- a/packages/auditor-backoffice-ui/package.json
+++ b/packages/auditor-backoffice-ui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/auditor-backoffice-ui",
- "version": "0.12.1",
+ "version": "0.12.2",
"license": "AGPL-3.0-or-later",
"type": "module",
"scripts": {
diff --git a/packages/bank-ui/package.json b/packages/bank-ui/package.json
index 21abbc3e9..65281bf2b 100644
--- a/packages/bank-ui/package.json
+++ b/packages/bank-ui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/bank-ui",
- "version": "0.12.1",
+ "version": "0.12.2",
"license": "AGPL-3.0-OR-LATER",
"type": "module",
"scripts": {
diff --git a/packages/bank-ui/src/hooks/preferences.ts b/packages/bank-ui/src/hooks/preferences.ts
index fadbbc8c1..a03234634 100644
--- a/packages/bank-ui/src/hooks/preferences.ts
+++ b/packages/bank-ui/src/hooks/preferences.ts
@@ -43,7 +43,7 @@ export const codecForPreferences = (): Codec<Preferences> =>
.property("showDebugInfo", codecForBoolean())
.property("fastWithdrawalForm", codecForBoolean())
.property("showCopyAccount", codecForBoolean())
- .build("Settings");
+ .build("Preferences");
const defaultPreferences: Preferences = {
showWithdrawalSuccess: true,
diff --git a/packages/bank-ui/src/pages/BankFrame.tsx b/packages/bank-ui/src/pages/BankFrame.tsx
index 62b5c6f90..e969caaa7 100644
--- a/packages/bank-ui/src/pages/BankFrame.tsx
+++ b/packages/bank-ui/src/pages/BankFrame.tsx
@@ -162,7 +162,6 @@ export function BankFrame({
<div class="fixed z-20 top-14 w-full">
<div class="mx-auto w-4/5">
<ToastBanner />
- {/* <Attention type="success" title={"hola" as TranslatedString} onClose={() => { }} /> */}
</div>
</div>
diff --git a/packages/challenger-ui/package.json b/packages/challenger-ui/package.json
index 370051498..d4d047326 100644
--- a/packages/challenger-ui/package.json
+++ b/packages/challenger-ui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/challenger-ui",
- "version": "0.12.1",
+ "version": "0.12.2",
"author": "sebasjm",
"license": "AGPL-3.0-OR-LATER",
"description": "UI for GNU Challenger.",
diff --git a/packages/challenger-ui/src/Routing.tsx b/packages/challenger-ui/src/Routing.tsx
index 7f9f52a19..f7488cb8d 100644
--- a/packages/challenger-ui/src/Routing.tsx
+++ b/packages/challenger-ui/src/Routing.tsx
@@ -23,6 +23,7 @@ import {
import { Fragment, VNode, h } from "preact";
import { assertUnreachable } from "@gnu-taler/taler-util";
+import { useErrorBoundary } from "preact/hooks";
import { CheckChallengeIsUpToDate } from "./components/CheckChallengeIsUpToDate.js";
import { SessionId, useSessionState } from "./hooks/session.js";
import { AnswerChallenge } from "./pages/AnswerChallenge.js";
@@ -32,7 +33,6 @@ import { Frame } from "./pages/Frame.js";
import { MissingParams } from "./pages/MissingParams.js";
import { NonceNotFound } from "./pages/NonceNotFound.js";
import { Setup } from "./pages/Setup.js";
-import { useErrorBoundary } from "preact/hooks";
export function Routing(): VNode {
// check session and defined if this is
diff --git a/packages/challenger-ui/src/context/preferences.ts b/packages/challenger-ui/src/context/preferences.ts
new file mode 100644
index 000000000..3188bd71c
--- /dev/null
+++ b/packages/challenger-ui/src/context/preferences.ts
@@ -0,0 +1,87 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ Codec,
+ TranslatedString,
+ buildCodecForObject,
+ codecForBoolean,
+} from "@gnu-taler/taler-util";
+import {
+ buildStorageKey,
+ useLocalStorage,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+
+interface Preferences {
+ showChallangeSetup: boolean;
+ showDebugInfo: boolean;
+}
+
+export const codecForPreferences = (): Codec<Preferences> =>
+ buildCodecForObject<Preferences>()
+ .property("showChallangeSetup", codecForBoolean())
+ .property("showDebugInfo", codecForBoolean())
+ .build("Preferences");
+
+const defaultPreferences: Preferences = {
+ showChallangeSetup: false,
+ showDebugInfo: false,
+};
+
+const PREFERENCES_KEY = buildStorageKey(
+ "challenger-preferences",
+ codecForPreferences(),
+);
+/**
+ * User preferences.
+ *
+ * @returns tuple of [state, update()]
+ */
+export function usePreferences(): [
+ Readonly<Preferences>,
+ <T extends keyof Preferences>(key: T, value: Preferences[T]) => void,
+] {
+ const { value, update } = useLocalStorage(
+ PREFERENCES_KEY,
+ defaultPreferences,
+ );
+
+ function updateField<T extends keyof Preferences>(k: T, v: Preferences[T]) {
+ const newValue = { ...value, [k]: v };
+ update(newValue);
+ }
+ return [value, updateField];
+}
+
+export function getAllBooleanPreferences(): Array<keyof Preferences> {
+ return [
+ "showChallangeSetup",
+ "showDebugInfo",
+ ];
+}
+
+export function getLabelForPreferences(
+ k: keyof Preferences,
+ i18n: ReturnType<typeof useTranslationContext>["i18n"],
+): TranslatedString {
+ switch (k) {
+ case "showChallangeSetup":
+ return i18n.str`Show challenger setup screen`;
+ case "showDebugInfo":
+ return i18n.str`Show debug info`;
+ }
+}
diff --git a/packages/challenger-ui/src/declaration.d.ts b/packages/challenger-ui/src/declaration.d.ts
new file mode 100644
index 000000000..581cbcd07
--- /dev/null
+++ b/packages/challenger-ui/src/declaration.d.ts
@@ -0,0 +1,35 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+declare module "*.css" {
+ const mapping: Record<string, string>;
+ export default mapping;
+}
+declare module "*.svg" {
+ const content: string;
+ export default content;
+}
+declare module "*.jpeg" {
+ const content: string;
+ export default content;
+}
+declare module "*.png" {
+ const content: string;
+ export default content;
+}
+
+declare const __VERSION__: string;
+declare const __GIT_HASH__: string;
diff --git a/packages/challenger-ui/src/hooks/session.ts b/packages/challenger-ui/src/hooks/session.ts
index eeb330493..4dc7e0dc1 100644
--- a/packages/challenger-ui/src/hooks/session.ts
+++ b/packages/challenger-ui/src/hooks/session.ts
@@ -15,9 +15,11 @@
*/
import {
+ AbsoluteTime,
ChallengerApi,
Codec,
buildCodecForObject,
+ codecForAbsoluteTime,
codecForBoolean,
codecForChallengeStatus,
codecForNumber,
@@ -39,8 +41,10 @@ export type SessionId = {
};
export type LastChallengeResponse = {
- attemptsLeft: number;
- nextSend: string;
+ sendCodeLeft: number;
+ changeTargetLeft: number;
+ checkPinLeft: number;
+ nextSend: AbsoluteTime;
transmitted: boolean;
};
@@ -51,8 +55,10 @@ export type SessionState = SessionId & {
};
export const codecForLastChallengeResponse = (): Codec<LastChallengeResponse> =>
buildCodecForObject<LastChallengeResponse>()
- .property("attemptsLeft", codecForNumber())
- .property("nextSend", codecForString())
+ .property("sendCodeLeft", codecForNumber())
+ .property("changeTargetLeft", codecForNumber())
+ .property("checkPinLeft", codecForNumber())
+ .property("nextSend", codecForAbsoluteTime)
.property("transmitted", codecForBoolean())
.build("LastChallengeResponse");
@@ -125,7 +131,8 @@ export function useSessionState(): SessionStateHandler {
const ls = state.lastStatus;
if (
ls.changes_left !== st.changes_left ||
- ls.fix_address !== st.fix_address || ls.last_address !== st.last_address
+ ls.fix_address !== st.fix_address ||
+ ls.last_address !== st.last_address
) {
update({
...state,
diff --git a/packages/challenger-ui/src/pages/AnswerChallenge.tsx b/packages/challenger-ui/src/pages/AnswerChallenge.tsx
index 5fe2d9743..2740e1bdb 100644
--- a/packages/challenger-ui/src/pages/AnswerChallenge.tsx
+++ b/packages/challenger-ui/src/pages/AnswerChallenge.tsx
@@ -14,8 +14,10 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import {
+ AbsoluteTime,
ChallengerApi,
HttpStatusCode,
+ TalerProtocolTimestamp,
assertUnreachable,
} from "@gnu-taler/taler-util";
import {
@@ -52,8 +54,7 @@ export function AnswerChallenge({
const { state, accepted, completed } = useSessionState();
const [notification, withErrorHandler] = useLocalNotificationHandler();
const [pin, setPin] = useState<string | undefined>();
- const [lastTryError, setLastTryError] =
- useState<ChallengerApi.InvalidPinResponse>();
+
const errors = undefinedIfEmpty({
pin: !pin ? i18n.str`Can't be empty` : undefined,
});
@@ -67,7 +68,9 @@ export function AnswerChallenge({
: state.lastStatus.last_address["email"];
const onSendAgain =
- !state || lastEmail === undefined
+ lastEmail === undefined ||
+ state?.lastStatus == undefined ||
+ state?.lastStatus.changes_left === 0
? undefined
: withErrorHandler(
async () => {
@@ -79,8 +82,12 @@ export function AnswerChallenge({
completed(new URL(ok.body.redirect_url));
} else {
accepted({
- attemptsLeft: ok.body.attempts_left,
- nextSend: ok.body.next_tx_time,
+ changeTargetLeft: ok.body.attempts_left,
+ checkPinLeft: state.lastStatus?.auth_attempts_left ?? 0,
+ sendCodeLeft: state.lastStatus?.pin_transmissions_left ?? 0,
+ nextSend: AbsoluteTime.fromProtocolTimestamp(
+ ok.body.retransmission_time,
+ ),
transmitted: ok.body.transmitted,
});
}
@@ -89,23 +96,23 @@ export function AnswerChallenge({
(fail) => {
switch (fail.case) {
case HttpStatusCode.BadRequest:
- return i18n.str``;
- case HttpStatusCode.Forbidden:
- return i18n.str``;
+ return i18n.str`The request was not accepted, try reloading the app.`;
case HttpStatusCode.NotFound:
- return i18n.str``;
+ return i18n.str`Challenge not found.`;
case HttpStatusCode.NotAcceptable:
- return i18n.str``;
+ return i18n.str`Server templates are missing due to misconfiguration.`;
case HttpStatusCode.TooManyRequests:
- return i18n.str``;
+ return i18n.str`There have been too many attempts to request challenge transmissions.`;
case HttpStatusCode.InternalServerError:
- return i18n.str``;
+ return i18n.str`Server is not able to respond due to internal problems.`;
}
},
);
const onCheck =
- errors !== undefined || (lastTryError && lastTryError.exhausted)
+ errors !== undefined ||
+ state?.lastStatus == undefined ||
+ state?.lastStatus.auth_attempts_left === 0
? undefined
: withErrorHandler(
async () => {
@@ -115,25 +122,34 @@ export function AnswerChallenge({
if (ok.body.type === "completed") {
completed(new URL(ok.body.redirect_url));
} else {
- setLastTryError(ok.body);
+ accepted({
+ changeTargetLeft: ok.body.addresses_left,
+ checkPinLeft: ok.body.auth_attempts_left,
+ sendCodeLeft: ok.body.pin_transmissions_left,
+ nextSend: AbsoluteTime.fromProtocolTimestamp(
+ state?.lastStatus?.retransmission_time ??
+ TalerProtocolTimestamp.now(),
+ ),
+ transmitted: state?.lastTry?.transmitted ?? false,
+ });
}
onComplete();
},
(fail) => {
switch (fail.case) {
case HttpStatusCode.BadRequest:
- return i18n.str`Invalid request`;
+ return i18n.str`The request was not accepted, try reloading the app.`;
case HttpStatusCode.Forbidden: {
- return i18n.str`Too many attemps where made`;
+ return i18n.str`Invalid pin.`;
}
case HttpStatusCode.NotFound:
- return i18n.str``;
+ return i18n.str`Challenge not found.`;
case HttpStatusCode.NotAcceptable:
- return i18n.str``;
+ return i18n.str`Server templates are missing due to misconfiguration.`;
case HttpStatusCode.TooManyRequests:
- return i18n.str``;
+ return i18n.str`There have been too many attempts to request challenge transmissions.`;
case HttpStatusCode.InternalServerError:
- return i18n.str``;
+ return i18n.str`Server is not able to respond due to internal problems.`;
default:
assertUnreachable(fail);
}
@@ -174,11 +190,11 @@ export function AnswerChallenge({
</Attention>
)}
</p>
- {!lastTryError ? undefined : (
+ {!state.lastStatus ? undefined : (
<p class="mt-2 text-lg leading-8 text-gray-600">
<i18n.Translate>
You can try another PIN but just{" "}
- {lastTryError.auth_attempts_left} times more.
+ {state.lastStatus.auth_attempts_left} times more.
</i18n.Translate>
</p>
)}
@@ -222,8 +238,21 @@ export function AnswerChallenge({
<p class="mt-3 text-sm leading-6 text-gray-400">
<i18n.Translate>
- You have {state.lastTry.attemptsLeft} attempts left.
+ We send the code {state.lastTry.checkPinLeft} more times.
</i18n.Translate>
+ {state.lastTry.checkPinLeft < 1 ? (
+ <i18n.Translate>
+ You can&#39;t check the PIN anymore.
+ </i18n.Translate>
+ ) : state.lastTry.checkPinLeft === 1 ? (
+ <i18n.Translate>
+ You can check the PIN one last time.
+ </i18n.Translate>
+ ) : (
+ <i18n.Translate>
+ You can check the PIN {state.lastTry.checkPinLeft} more times.
+ </i18n.Translate>
+ )}
</p>
</div>
@@ -240,12 +269,31 @@ export function AnswerChallenge({
<div class="mt-10 flex justify-between">
<div>
<a
+ data-disabled={!state.lastStatus || state.lastStatus.changes_left < 1}
href={routeAsk.url({ nonce })}
- class="relative disabled:bg-gray-100 disabled:text-gray-500 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0"
+ class="relative data-[disabled=true]:bg-gray-300 data-[disabled=true]:text-white data-[disabled=true]:cursor-default inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0"
>
<i18n.Translate>Change email</i18n.Translate>
</a>
- </div>
+ {state.lastStatus === undefined ? undefined :
+ <p class="mt-2 text-sm leading-6 text-gray-400">
+ {state.lastStatus.changes_left < 1 ? (
+ <i18n.Translate>
+ You can&#39;t change the email anymore.
+ </i18n.Translate>
+ ) : state.lastStatus.changes_left === 1 ? (
+ <i18n.Translate>
+ You can change the email one last time.
+ </i18n.Translate>
+ ) : (
+ <i18n.Translate>
+ You can change the email {state.lastStatus.changes_left}{" "}
+ more times.
+ </i18n.Translate>
+ )}
+ </p>
+ }
+ </div>
<div>
<Button
type="submit"
@@ -255,6 +303,22 @@ export function AnswerChallenge({
>
<i18n.Translate>Send code again</i18n.Translate>
</Button>
+ <p class="mt-2 text-sm leading-6 text-gray-400">
+ {state.lastTry.sendCodeLeft < 1 ? (
+ <i18n.Translate>
+ We can&#39;t send you the code anymore.
+ </i18n.Translate>
+ ) : state.lastTry.sendCodeLeft === 1 ? (
+ <i18n.Translate>
+ We can send the code one last time.
+ </i18n.Translate>
+ ) : (
+ <i18n.Translate>
+ We can send the code {state.lastTry.sendCodeLeft} more
+ times.
+ </i18n.Translate>
+ )}
+ </p>
</div>
</div>
</form>
diff --git a/packages/challenger-ui/src/pages/AskChallenge.tsx b/packages/challenger-ui/src/pages/AskChallenge.tsx
index 829cdaccc..dc60562b7 100644
--- a/packages/challenger-ui/src/pages/AskChallenge.tsx
+++ b/packages/challenger-ui/src/pages/AskChallenge.tsx
@@ -13,7 +13,7 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { AbsoluteTime, HttpStatusCode } from "@gnu-taler/taler-util";
import {
Attention,
Button,
@@ -48,13 +48,15 @@ export function AskChallenge({
focus,
}: Props): VNode {
const { state, accepted, completed } = useSessionState();
+ const { lib, config } = useChallengerApiContext();
+
const status = state?.lastStatus;
const prevEmail =
!status || !status.last_address ? undefined : status.last_address["email"];
- const regexEmail =
- !status || !status.restrictions ? undefined : status.restrictions["email"];
+ const regexEmail = !config.restrictions
+ ? undefined
+ : config.restrictions["email"];
- const { lib } = useChallengerApiContext();
const { i18n } = useTranslationContext();
const [notification, withErrorHandler] = useLocalNotificationHandler();
const [email, setEmail] = useState<string | undefined>();
@@ -91,8 +93,12 @@ export function AskChallenge({
completed(new URL(ok.body.redirect_url));
} else {
accepted({
- attemptsLeft: ok.body.attempts_left,
- nextSend: ok.body.next_tx_time,
+ changeTargetLeft: ok.body.attempts_left,
+ checkPinLeft: state?.lastStatus?.auth_attempts_left ?? 0,
+ sendCodeLeft: state?.lastStatus?.pin_transmissions_left ?? 0,
+ nextSend: AbsoluteTime.fromProtocolTimestamp(
+ ok.body.retransmission_time,
+ ),
transmitted: ok.body.transmitted,
});
}
@@ -101,17 +107,15 @@ export function AskChallenge({
(fail) => {
switch (fail.case) {
case HttpStatusCode.BadRequest:
- return i18n.str``;
- case HttpStatusCode.Forbidden:
- return i18n.str``;
+ return i18n.str`The request was not accepted, try reloading the app.`;
case HttpStatusCode.NotFound:
- return i18n.str``;
+ return i18n.str`Challenge not found.`;
case HttpStatusCode.NotAcceptable:
- return i18n.str``;
+ return i18n.str`Server templates are missing due to misconfiguration.`;
case HttpStatusCode.TooManyRequests:
- return i18n.str``;
+ return i18n.str`There have been too many attempts to request challenge transmissions.`;
case HttpStatusCode.InternalServerError:
- return i18n.str``;
+ return i18n.str`Server is not able to respond due to internal problems.`;
}
},
);
@@ -122,7 +126,7 @@ export function AskChallenge({
return (
<Fragment>
- <LocalNotificationBanner notification={notification} />
+ <LocalNotificationBanner notification={notification} showDebug={true} />
<div class="isolate bg-white px-6 py-12">
<div class="mx-auto max-w-2xl text-center">
@@ -213,16 +217,22 @@ export function AskChallenge({
</div>
)}
- {!status.changes_left ? (
- <p class="mt-3 text-sm leading-6 text-gray-400">
- <i18n.Translate>No more changes left</i18n.Translate>
- </p>
- ) : (
- <p class="mt-3 text-sm leading-6 text-gray-400">
- <i18n.Translate>
- You can change your email address another{" "}
- {status.changes_left} times.
- </i18n.Translate>
+ {state.lastStatus === undefined ? undefined : (
+ <p class="mt-2 text-sm leading-6 text-gray-400">
+ {state.lastStatus.changes_left < 1 ? (
+ <i18n.Translate>
+ You can&#39;t change the email anymore.
+ </i18n.Translate>
+ ) : state.lastStatus.changes_left === 1 ? (
+ <i18n.Translate>
+ You can change the email one last time.
+ </i18n.Translate>
+ ) : (
+ <i18n.Translate>
+ You can change the email {state.lastStatus.changes_left}{" "}
+ more times.
+ </i18n.Translate>
+ )}
</p>
)}
</div>
diff --git a/packages/challenger-ui/src/pages/Frame.tsx b/packages/challenger-ui/src/pages/Frame.tsx
index 612eced0b..dd2a13d8c 100644
--- a/packages/challenger-ui/src/pages/Frame.tsx
+++ b/packages/challenger-ui/src/pages/Frame.tsx
@@ -14,56 +14,121 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import {
+ Footer,
+ Header,
+ ToastBanner,
+ notifyError,
+ notifyException,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
import { ComponentChildren, Fragment, h, VNode } from "preact";
+import { useSettingsContext } from "../context/settings.js";
+import { useEffect, useErrorBoundary } from "preact/hooks";
+import { TranslatedString } from "@gnu-taler/taler-util";
+import {
+ getAllBooleanPreferences,
+ getLabelForPreferences,
+ usePreferences,
+} from "../context/preferences.js";
+
+const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
+const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
export function Frame({ children }: { children: ComponentChildren }): VNode {
+ const settings = useSettingsContext();
+ const [preferences, updatePreferences] = usePreferences();
+
+ const [error, resetError] = useErrorBoundary();
+ const { i18n } = useTranslationContext();
+ useEffect(() => {
+ if (error) {
+ if (error instanceof Error) {
+ console.log("Internal error, please report", error);
+ notifyException(i18n.str`Internal error, please report.`, error);
+ } else {
+ console.log("Internal error, please report", error);
+ notifyError(
+ i18n.str`Internal error, please report.`,
+ String(error) as TranslatedString,
+ );
+ }
+ resetError();
+ }
+ }, [error]);
+
return (
- <Fragment>
- <header class="bg-indigo-600 w-full mx-auto px-2 border-b border-opacity-25 border-indigo-400">
- <div class="flex flex-row h-16 items-center ">
- <div class="flex px-2 justify-start">
- <div class="flex-shrink-0 bg-white rounded-lg">
- <a href="#">
- <img
- class="h-8 w-auto"
- src='data:image/svg+xml,<?xml version="1.0" encoding="UTF-8" standalone="no"?>%0A<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 201 90">%0A <g fill="%230042b3" fill-rule="evenodd" stroke-width=".3">%0A <path d="M86.7 1.1c15.6 0 29 9.4 36 23.2h-5.9A35.1 35.1 0 0086.7 6.5C67 6.5 51 23.6 51 44.7c0 10.4 3.8 19.7 10 26.6a31.4 31.4 0 01-4.2 3A45.2 45.2 0 0146 44.7c0-24 18.2-43.6 40.7-43.6zm35.8 64.3a40.4 40.4 0 01-39 22.8c3-1.5 6-3.5 8.6-5.7a35.6 35.6 0 0024.6-17.1z" />%0A <path d="M64.2 1.1l3.1.1c-3 1.6-5.9 3.5-8.5 5.8a37.5 37.5 0 00-30.2 37.7c0 14.3 7.3 26.7 18 33.3a29.6 29.6 0 01-8.5.2c-9-8-14.6-20-14.6-33.5 0-24 18.2-43.6 40.7-43.6zm5.4 81.4a35.6 35.6 0 0024.6-17.1h5.9a40.4 40.4 0 01-39 22.8c3-1.5 5.9-3.5 8.5-5.7zm24.8-58.2a37 37 0 00-12.6-12.8 29.6 29.6 0 018.5-.2c4 3.6 7.4 8 9.9 13z" />%0A <path d="M41.8 1.1c1 0 2 0 3.1.2-3 1.5-5.9 3.4-8.5 5.6A37.5 37.5 0 006.1 44.7c0 21.1 16 38.3 35.7 38.3 12.6 0 23.6-7 30-17.6h5.8a40.4 40.4 0 01-35.8 23C19.3 88.4 1 68.8 1 44.7c0-24 18.2-43.6 40.7-43.6zm30.1 23.2a38.1 38.1 0 00-4.5-6.1c1.3-1.2 2.7-2.2 4.3-3 2.3 2.7 4.4 5.8 6 9.1z" />%0A </g>%0A <path d="M76.1 34.4h9.2v-5H61.9v5H71v26h5.1zM92.6 52.9h13.7l3 7.4h5.3l-12.7-31.2h-4.7L84.5 60.3h5.2zm11.8-4.9h-9.9l5-12.4zM123.8 29.4h-4.6v31h20.6v-5h-16zM166.5 29.4H145v31h21.6v-5H150v-8.3h14.5v-4.9h-14.5v-8h16.4zM191.2 39.5c0 1.6-.5 2.8-1.6 3.8s-2.6 1.4-4.4 1.4h-7.4V34.3h7.4c1.9 0 3.4.4 4.4 1.3 1 .9 1.6 2.2 1.6 3.9zm6 20.8l-7.7-11.7c1-.3 1.9-.7 2.7-1.3a8.8 8.8 0 003.6-4.6c.4-1 .5-2.2.5-3.5 0-1.5-.2-2.9-.7-4.1a8.4 8.4 0 00-2.1-3.1c-1-.8-2-1.5-3.4-2-1.3-.4-2.8-.6-4.5-.6h-12.9v31h5V49.4h6.5l7 10.8z" />%0A</svg>'
- alt="GNU Taler"
- style="height: 1.5rem; margin: 0.5rem;"
- />
- </a>
- </div>
- <span class="flex items-center text-white text-lg font-bold ml-4">
- Challenger
- </span>
+ <div
+ class="min-h-full flex flex-col m-0 bg-slate-200"
+ style="min-height: 100vh;"
+ >
+ <Header
+ title="Challenger"
+ onLogout={undefined}
+ iconLinkURL="#"
+ sites={preferences.showChallangeSetup ? [
+ ["New challenge","#/setup/1"]
+ ] :[]}
+ supportedLangs={["en"]}
+ >
+ <li>
+ <div class="text-xs font-semibold leading-6 text-gray-400">
+ <i18n.Translate>Preferences</i18n.Translate>
</div>
- <div class="block flex-1 ml-6 "></div>
- <div class="flex justify-end"></div>
+ <ul role="list" class="space-y-4">
+ {getAllBooleanPreferences().map((set) => {
+ const isOn: boolean = !!preferences[set];
+ return (
+ <li key={set} class="pl-2">
+ <div class="flex items-center justify-between">
+ <span class="flex flex-grow flex-col">
+ <span
+ class="text-sm text-black font-medium leading-6 "
+ id="availability-label"
+ >
+ {getLabelForPreferences(set, i18n)}
+ </span>
+ </span>
+ <button
+ type="button"
+ name={`${set} switch`}
+ data-enabled={isOn}
+ class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
+ role="switch"
+ aria-checked="false"
+ aria-labelledby="availability-label"
+ aria-describedby="availability-description"
+ onClick={() => {
+ updatePreferences(set, !isOn);
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={isOn}
+ class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
+ ></span>
+ </button>
+ </div>
+ </li>
+ );
+ })}
+ </ul>
+ </li>
+ </Header>
+
+ <div class="fixed z-20 top-14 w-full">
+ <div class="mx-auto w-4/5">
+ <ToastBanner />
</div>
- </header>
+ </div>
<main class="flex-1">{children}</main>
-
- <footer class="bottom-4 mb-4">
- <div class="mt-8 mx-8 md:order-1 md:mt-0">
- <div>
- <p class="text-xs leading-5 text-gray-400">
- Learn more about{" "}
- <a
- target="_blank"
- rel="noreferrer noopener"
- class="font-semibold text-gray-500 hover:text-gray-400"
- href="https://taler.net"
- >
- GNU Taler
- </a>
- </p>
- </div>
- <div style="flex-grow: 1;"></div>
- <p class="text-xs leading-5 text-gray-400">
- Copyright © 2014—2023 Taler Systems SA.{" "}
- </p>
- </div>
- </footer>
- </Fragment>
+
+ <Footer
+ testingUrlKey="challenger-base-url"
+ GIT_HASH={GIT_HASH}
+ VERSION={VERSION}
+ />
+ </div>
);
}
diff --git a/packages/idb-bridge/package.json b/packages/idb-bridge/package.json
index 3c581d537..a176fadc9 100644
--- a/packages/idb-bridge/package.json
+++ b/packages/idb-bridge/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/idb-bridge",
- "version": "0.12.1",
+ "version": "0.12.2",
"description": "IndexedDB implementation that uses SQLite3 as storage",
"main": "./dist/idb-bridge.js",
"module": "./lib/index.js",
diff --git a/packages/merchant-backend-ui/package.json b/packages/merchant-backend-ui/package.json
index c611f61c2..683329245 100644
--- a/packages/merchant-backend-ui/package.json
+++ b/packages/merchant-backend-ui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/merchant-backend-ui",
- "version": "0.12.1",
+ "version": "0.12.2",
"license": "AGPL-3.0-or-later",
"scripts": {
"compile": "tsc && ./build.mjs",
diff --git a/packages/merchant-backoffice-ui/package.json b/packages/merchant-backoffice-ui/package.json
index 0701a12e5..babadbb6a 100644
--- a/packages/merchant-backoffice-ui/package.json
+++ b/packages/merchant-backoffice-ui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/merchant-backoffice-ui",
- "version": "0.12.1",
+ "version": "0.12.2",
"license": "AGPL-3.0-or-later",
"type": "module",
"scripts": {
diff --git a/packages/merchant-backoffice-ui/src/i18n/es.po b/packages/merchant-backoffice-ui/src/i18n/es.po
index 42cb6a76f..58a3745ac 100644
--- a/packages/merchant-backoffice-ui/src/i18n/es.po
+++ b/packages/merchant-backoffice-ui/src/i18n/es.po
@@ -17,7 +17,7 @@ msgstr ""
"Project-Id-Version: Taler Wallet\n"
"Report-Msgid-Bugs-To: taler@gnu.org\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: 2024-06-26 08:05+0000\n"
+"PO-Revision-Date: 2024-06-28 00:57+0000\n"
"Last-Translator: Luis Avalos <avalos.diaz.0577@gmail.com>\n"
"Language-Team: Spanish <https://weblate.taler.net/projects/gnu-taler/"
"merchant-backoffice/es/>\n"
@@ -746,8 +746,8 @@ msgid ""
"Time until which the wallet will automatically check for refunds without "
"user interaction."
msgstr ""
-"Tiempo hasta el cual la billetera será automáticamente revisada por "
-"reembolsos win interación por parte del usuario."
+"Tiempo hasta el cual la cartera será automáticamente revisada por reembolsos "
+"win interación por parte del usuario."
#: src/paths/instance/orders/create/CreatePage.tsx:502
#, c-format
@@ -841,7 +841,7 @@ msgstr ""
#: src/paths/instance/orders/create/CreatePage.tsx:541
#, c-format
msgid "You must enter a value in JavaScript Object Notation (JSON)."
-msgstr ""
+msgstr "Debes introducir un valor en JavaScript Object Notation (JSON)."
#: src/components/picker/DurationPicker.tsx:55
#, c-format
@@ -1037,6 +1037,7 @@ msgstr "Máxima comisión"
#, c-format
msgid "maximum total deposit fee accepted by the merchant for this contract"
msgstr ""
+"tasa máxima total de depósito aceptada por el comerciante para este contrato"
#: src/paths/instance/orders/details/DetailPage.tsx:93
#, c-format
@@ -1046,7 +1047,7 @@ msgstr "Impuesto de transferencia máximo"
#: src/paths/instance/orders/details/DetailPage.tsx:94
#, c-format
msgid "maximum wire fee accepted by the merchant"
-msgstr ""
+msgstr "comisión máxima por transferencia aceptada por el comerciante"
#: src/paths/instance/orders/details/DetailPage.tsx:100
#, c-format
@@ -1063,23 +1064,23 @@ msgstr "Creado en"
#: src/paths/instance/orders/details/DetailPage.tsx:106
#, c-format
msgid "time when this contract was generated"
-msgstr ""
+msgstr "momento en que se generó este contrato"
#: src/paths/instance/orders/details/DetailPage.tsx:112
#, c-format
msgid "after this deadline has passed no refunds will be accepted"
-msgstr ""
+msgstr "pasado este plazo no se aceptarán devoluciones"
#: src/paths/instance/orders/details/DetailPage.tsx:118
#, c-format
msgid ""
"after this deadline, the merchant won't accept payments for the contract"
-msgstr ""
+msgstr "pasado este plazo, el comerciante no aceptará pagos por el contrato"
#: src/paths/instance/orders/details/DetailPage.tsx:124
#, c-format
msgid "transfer deadline for the exchange"
-msgstr ""
+msgstr "plazo de transferencia para el intercambio"
#: src/paths/instance/orders/details/DetailPage.tsx:130
#, c-format
@@ -1089,7 +1090,7 @@ msgstr ""
#: src/paths/instance/orders/details/DetailPage.tsx:136
#, c-format
msgid "where the order will be delivered"
-msgstr ""
+msgstr "dónde se entregará el pedido"
#: src/paths/instance/orders/details/DetailPage.tsx:144
#, fuzzy, c-format
@@ -1101,6 +1102,8 @@ msgstr "Plazo de reembolso automático"
msgid ""
"how long the wallet should try to get an automatic refund for the purchase"
msgstr ""
+"cuánto tiempo debe intentar la cartera obtener el reembolso automático de la "
+"compra"
#: src/paths/instance/orders/details/DetailPage.tsx:150
#, fuzzy, c-format
@@ -1111,6 +1114,7 @@ msgstr "Información extra"
#, c-format
msgid "extra data that is only interpreted by the merchant frontend"
msgstr ""
+"datos adicionales que solo son interpretados por la interfaz del comerciante"
#: src/paths/instance/orders/details/DetailPage.tsx:219
#, c-format
@@ -1163,9 +1167,9 @@ msgid "refunded"
msgstr "reembolzado"
#: src/paths/instance/orders/details/DetailPage.tsx:480
-#, fuzzy, c-format
+#, c-format
msgid "refund order"
-msgstr "reembolzado"
+msgstr "reembolsado"
#: src/paths/instance/orders/details/DetailPage.tsx:481
#, fuzzy, c-format
@@ -1180,12 +1184,12 @@ msgstr "reembolzar"
#: src/paths/instance/orders/details/DetailPage.tsx:553
#, c-format
msgid "Refunded amount"
-msgstr "Monto reembolzado"
+msgstr "Monto reembolsado"
#: src/paths/instance/orders/details/DetailPage.tsx:560
-#, fuzzy, c-format
+#, c-format
msgid "Refund taken"
-msgstr "Reembolzado"
+msgstr "Reembolsado"
#: src/paths/instance/orders/details/DetailPage.tsx:570
#, fuzzy, c-format
@@ -1248,7 +1252,7 @@ msgstr "No se pudo create el reembolso"
#: src/paths/instance/orders/list/ListPage.tsx:78
#, c-format
msgid "select date to show nearby orders"
-msgstr ""
+msgstr "seleccione la fecha para mostrar pedidos cercanos"
#: src/paths/instance/orders/list/ListPage.tsx:94
#, c-format
@@ -1258,17 +1262,17 @@ msgstr "ID de la orden"
#: src/paths/instance/orders/list/ListPage.tsx:100
#, c-format
msgid "jump to order with the given order ID"
-msgstr ""
+msgstr "saltar al pedido con el ID de pedido proporcionado"
#: src/paths/instance/orders/list/ListPage.tsx:122
#, c-format
msgid "remove all filters"
-msgstr ""
+msgstr "eliminar todos los filtros"
#: src/paths/instance/orders/list/ListPage.tsx:132
#, c-format
msgid "only show paid orders"
-msgstr ""
+msgstr "mostrar sólo pedidos pagados"
#: src/paths/instance/orders/list/ListPage.tsx:135
#, c-format
@@ -1291,6 +1295,8 @@ msgid ""
"only show orders where customers paid, but wire payments from payment "
"provider are still pending"
msgstr ""
+"mostrar sólo los pedidos en los que los clientes han pagado, pero los pagos "
+"por transferencia del proveedor de pago siguen pendientes"
#: src/paths/instance/orders/list/ListPage.tsx:155
#, c-format
@@ -1300,7 +1306,7 @@ msgstr "No transferido"
#: src/paths/instance/orders/list/ListPage.tsx:170
#, c-format
msgid "clear date filter"
-msgstr ""
+msgstr "borrar filtro de fechas"
#: src/paths/instance/orders/list/ListPage.tsx:184
#, c-format
@@ -1333,6 +1339,8 @@ msgid ""
"click here to configure the stock of the product, leave it as is and the "
"backend will not control stock"
msgstr ""
+"pulse aquí para configurar el stock del producto, déjelo como está y el "
+"backend no controlará el stock"
#: src/components/form/InputStock.tsx:109
#, c-format
@@ -1342,7 +1350,7 @@ msgstr "Administrar stock"
#: src/components/form/InputStock.tsx:115
#, c-format
msgid "this product has been configured without stock control"
-msgstr ""
+msgstr "este producto se ha configurado sin control de existencias"
#: src/components/form/InputStock.tsx:119
#, c-format
@@ -1372,7 +1380,7 @@ msgstr "Actual"
#: src/components/form/InputStock.tsx:196
#, c-format
msgid "remove stock control for this product"
-msgstr ""
+msgstr "eliminar el control de existencias de este producto"
#: src/components/form/InputStock.tsx:202
#, c-format
@@ -1393,26 +1401,27 @@ msgstr "Dirección de entrega"
#, c-format
msgid "product identification to use in URLs (for internal use only)"
msgstr ""
+"Identificación del producto para usar en las URL (solo para uso interno)"
#: src/components/product/ProductForm.tsx:139
#, c-format
msgid "illustration of the product for customers"
-msgstr ""
+msgstr "ilustración del producto para los clientes"
#: src/components/product/ProductForm.tsx:145
#, c-format
msgid "product description for customers"
-msgstr ""
+msgstr "descripción del producto para los clientes"
#: src/components/product/ProductForm.tsx:149
#, c-format
msgid "Age restricted"
-msgstr ""
+msgstr "Restricción de edad"
#: src/components/product/ProductForm.tsx:150
#, c-format
msgid "is this product restricted for customer below certain age?"
-msgstr ""
+msgstr "¿este producto está restringido para clientes menores de cierta edad?"
#: src/components/product/ProductForm.tsx:155
#, c-format
@@ -1420,12 +1429,16 @@ msgid ""
"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 "
"items, 5 meters) for customers"
msgstr ""
+"unidad que describe la cantidad de producto vendido (por ejemplo, 2 "
+"kilogramos, 5 litros, 3 artículos, 5 metros) para los clientes"
#: src/components/product/ProductForm.tsx:160
#, c-format
msgid ""
"sale price for customers, including taxes, for above units of the product"
msgstr ""
+"precio de venta para los clientes, impuestos incluidos, por encima de las "
+"unidades del producto"
#: src/components/product/ProductForm.tsx:164
#, c-format
@@ -1437,16 +1450,18 @@ msgstr "Existencias"
msgid ""
"product inventory for products with finite supply (for internal use only)"
msgstr ""
+"inventario de productos para productos con suministro finito (sólo para uso "
+"interno)"
#: src/components/product/ProductForm.tsx:171
#, c-format
msgid "taxes included in the product price, exposed to customers"
-msgstr ""
+msgstr "impuestos incluidos en el precio del producto, expuestos a los clientes"
#: src/paths/instance/products/create/CreatePage.tsx:66
#, c-format
msgid "Need to complete marked fields"
-msgstr ""
+msgstr "Necesita completar los campos marcados"
#: src/paths/instance/products/create/index.tsx:51
#, c-format
@@ -1461,7 +1476,7 @@ msgstr "Productos"
#: src/paths/instance/products/list/Table.tsx:73
#, c-format
msgid "add product to inventory"
-msgstr ""
+msgstr "añadir producto al inventario"
#: src/paths/instance/products/list/Table.tsx:137
#, c-format
@@ -1496,27 +1511,27 @@ msgstr "Actualizar"
#: src/paths/instance/products/list/Table.tsx:260
#, c-format
msgid "remove this product from the database"
-msgstr ""
+msgstr "eliminar este producto de la base de datos"
#: src/paths/instance/products/list/Table.tsx:331
#, c-format
msgid "update the product with new price"
-msgstr ""
+msgstr "actualizar el producto con el nuevo precio"
#: src/paths/instance/products/list/Table.tsx:341
#, c-format
msgid "update product with new price"
-msgstr ""
+msgstr "actualizar producto con nuevo precio"
#: src/paths/instance/products/list/Table.tsx:399
#, c-format
msgid "add more elements to the inventory"
-msgstr ""
+msgstr "añadir más elementos al inventario"
#: src/paths/instance/products/list/Table.tsx:404
#, c-format
msgid "report elements lost in the inventory"
-msgstr ""
+msgstr "informar de elementos perdidos en el inventario"
#: src/paths/instance/products/list/Table.tsx:409
#, fuzzy, c-format
@@ -1744,7 +1759,7 @@ msgstr ""
#: src/paths/instance/reserves/list/Table.tsx:210
#, c-format
msgid "authorize new tip from selected reserve"
-msgstr ""
+msgstr "autorizar nueva punta de reserva seleccionada"
#: src/paths/instance/reserves/list/Table.tsx:237
#, fuzzy, c-format
@@ -1780,32 +1795,32 @@ msgstr "no puede ser vacío"
#: src/paths/instance/templates/create/CreatePage.tsx:100
#, c-format
msgid "to short"
-msgstr ""
+msgstr "demasiado corta"
#: src/paths/instance/templates/create/CreatePage.tsx:108
#, c-format
msgid "just letters and numbers from 2 to 7"
-msgstr ""
+msgstr "sólo letras y números del 2 al 7"
#: src/paths/instance/templates/create/CreatePage.tsx:110
#, c-format
msgid "size of the key should be 32"
-msgstr ""
+msgstr "el tamaño de la clave debe ser 32"
#: src/paths/instance/templates/create/CreatePage.tsx:137
#, c-format
msgid "Identifier"
-msgstr ""
+msgstr "Identificador"
#: src/paths/instance/templates/create/CreatePage.tsx:138
#, c-format
msgid "Name of the template in URLs."
-msgstr ""
+msgstr "Nombre de la plantilla en las URL."
#: src/paths/instance/templates/create/CreatePage.tsx:144
#, c-format
msgid "Describe what this template stands for"
-msgstr ""
+msgstr "Describa lo que representa esta plantilla"
#: src/paths/instance/templates/create/CreatePage.tsx:149
#, fuzzy, c-format
@@ -1815,7 +1830,7 @@ msgstr "Estado de orden"
#: src/paths/instance/templates/create/CreatePage.tsx:150
#, c-format
msgid "If specified, this template will create order with the same summary"
-msgstr ""
+msgstr "Si se especifica, esta plantilla creará pedidos con el mismo resumen"
#: src/paths/instance/templates/create/CreatePage.tsx:154
#, fuzzy, c-format
@@ -1825,7 +1840,7 @@ msgstr "precio unitario"
#: src/paths/instance/templates/create/CreatePage.tsx:155
#, c-format
msgid "If specified, this template will create order with the same price"
-msgstr ""
+msgstr "Si se especifica, esta plantilla creará pedidos con el mismo precio"
#: src/paths/instance/templates/create/CreatePage.tsx:159
#, c-format
@@ -1835,7 +1850,7 @@ msgstr "Edad mínima"
#: src/paths/instance/templates/create/CreatePage.tsx:161
#, c-format
msgid "Is this contract restricted to some age?"
-msgstr ""
+msgstr "¿Este contrato está restringido a alguna edad?"
#: src/paths/instance/templates/create/CreatePage.tsx:165
#, fuzzy, c-format
@@ -1848,56 +1863,58 @@ msgid ""
"How much time has the customer to complete the payment once the order was "
"created."
msgstr ""
+"Cuánto tiempo tiene el cliente para completar el pago una vez creado el "
+"pedido."
#: src/paths/instance/templates/create/CreatePage.tsx:171
#, c-format
msgid "Verification algorithm"
-msgstr ""
+msgstr "Algoritmo de verificación"
#: src/paths/instance/templates/create/CreatePage.tsx:172
#, c-format
msgid "Algorithm to use to verify transaction in offline mode"
-msgstr ""
+msgstr "Algoritmo a utilizar para verificar la transacción en modo offline"
#: src/paths/instance/templates/create/CreatePage.tsx:180
#, c-format
msgid "Point-of-sale key"
-msgstr ""
+msgstr "Clave punto de venta"
#: src/paths/instance/templates/create/CreatePage.tsx:182
#, c-format
msgid "Useful to validate the purchase"
-msgstr ""
+msgstr "Útil para validar la compra"
#: src/paths/instance/templates/create/CreatePage.tsx:196
#, c-format
msgid "generate random secret key"
-msgstr ""
+msgstr "generar clave secreta aleatoria"
#: src/paths/instance/templates/create/CreatePage.tsx:203
#, c-format
msgid "random"
-msgstr ""
+msgstr "aleatorio"
#: src/paths/instance/templates/create/CreatePage.tsx:208
#, c-format
msgid "show secret key"
-msgstr ""
+msgstr "mostrar clave secreta"
#: src/paths/instance/templates/create/CreatePage.tsx:209
#, c-format
msgid "hide secret key"
-msgstr ""
+msgstr "ocultar clave secreta"
#: src/paths/instance/templates/create/CreatePage.tsx:216
#, c-format
msgid "hide"
-msgstr ""
+msgstr "ocultar"
#: src/paths/instance/templates/create/CreatePage.tsx:218
#, c-format
msgid "show"
-msgstr ""
+msgstr "mostrar"
#: src/paths/instance/templates/create/index.tsx:52
#, fuzzy, c-format
@@ -1912,7 +1929,7 @@ msgstr "Login necesario"
#: src/paths/instance/templates/use/UsePage.tsx:58
#, c-format
msgid "Order summary is required"
-msgstr ""
+msgstr "Se requiere resumen del pedido"
#: src/paths/instance/templates/use/UsePage.tsx:86
#, fuzzy, c-format
@@ -1922,7 +1939,7 @@ msgstr "cargar viejas transferencias"
#: src/paths/instance/templates/use/UsePage.tsx:108
#, c-format
msgid "Amount of the order"
-msgstr ""
+msgstr "Importe del pedido"
#: src/paths/instance/templates/use/UsePage.tsx:113
#, fuzzy, c-format
@@ -1940,16 +1957,19 @@ msgid ""
"Here you can specify a default value for fields that are not fixed. Default "
"values can be edited by the customer before the payment."
msgstr ""
+"Aquí puede especificar un valor por defecto para los campos que no son "
+"fijos. Los valores por defecto pueden ser editados por el cliente antes del "
+"pago."
#: src/paths/instance/templates/qr/QrPage.tsx:148
-#, fuzzy, c-format
+#, c-format
msgid "Fixed amount"
-msgstr "Monto reembolzado"
+msgstr "Importe fijo"
#: src/paths/instance/templates/qr/QrPage.tsx:149
-#, fuzzy, c-format
+#, c-format
msgid "Default amount"
-msgstr "Monto reembolzado"
+msgstr "Importe por defecto"
#: src/paths/instance/templates/qr/QrPage.tsx:161
#, fuzzy, c-format
@@ -1959,27 +1979,27 @@ msgstr "Estado de orden"
#: src/paths/instance/templates/qr/QrPage.tsx:177
#, c-format
msgid "Print"
-msgstr ""
+msgstr "Imprimir"
#: src/paths/instance/templates/qr/QrPage.tsx:184
#, c-format
msgid "Setup TOTP"
-msgstr ""
+msgstr "Configurar TOTP"
#: src/paths/instance/templates/list/Table.tsx:65
#, c-format
msgid "Templates"
-msgstr ""
+msgstr "Plantillas"
#: src/paths/instance/templates/list/Table.tsx:70
#, c-format
msgid "add new templates"
-msgstr ""
+msgstr "añadir nuevas plantillas"
#: src/paths/instance/templates/list/Table.tsx:142
#, c-format
msgid "load more templates before the first one"
-msgstr ""
+msgstr "cargar más plantillas antes de la primera"
#: src/paths/instance/templates/list/Table.tsx:146
#, fuzzy, c-format
@@ -1989,12 +2009,12 @@ msgstr "cargar nuevas transferencias"
#: src/paths/instance/templates/list/Table.tsx:181
#, c-format
msgid "delete selected templates from the database"
-msgstr ""
+msgstr "eliminar las plantillas seleccionadas de la base de datos"
#: src/paths/instance/templates/list/Table.tsx:188
#, c-format
msgid "use template to create new order"
-msgstr ""
+msgstr "utilizar la plantilla para crear un nuevo pedido"
#: src/paths/instance/templates/list/Table.tsx:195
#, fuzzy, c-format
@@ -2004,7 +2024,7 @@ msgstr "No se pudo create el reembolso"
#: src/paths/instance/templates/list/Table.tsx:210
#, c-format
msgid "load more templates after the last one"
-msgstr ""
+msgstr "cargar más plantillas después de la última"
#: src/paths/instance/templates/list/Table.tsx:214
#, fuzzy, c-format
@@ -2039,27 +2059,27 @@ msgstr "deberían ser iguales"
#: src/paths/instance/webhooks/create/CreatePage.tsx:85
#, c-format
msgid "Webhook ID to use"
-msgstr ""
+msgstr "ID de webhook a utilizar"
#: src/paths/instance/webhooks/create/CreatePage.tsx:89
#, c-format
msgid "Event"
-msgstr ""
+msgstr "Evento"
#: src/paths/instance/webhooks/create/CreatePage.tsx:90
#, c-format
msgid "The event of the webhook: why the webhook is used"
-msgstr ""
+msgstr "El evento del webhook: por qué se utiliza el webhook"
#: src/paths/instance/webhooks/create/CreatePage.tsx:94
#, c-format
msgid "Method"
-msgstr ""
+msgstr "Método"
#: src/paths/instance/webhooks/create/CreatePage.tsx:95
#, c-format
msgid "Method used by the webhook"
-msgstr ""
+msgstr "Método utilizado por el webhook"
#: src/paths/instance/webhooks/create/CreatePage.tsx:99
#, c-format
@@ -2069,12 +2089,12 @@ msgstr "URL"
#: src/paths/instance/webhooks/create/CreatePage.tsx:100
#, c-format
msgid "URL of the webhook where the customer will be redirected"
-msgstr ""
+msgstr "URL del webhook al que se redirigirá al cliente"
#: src/paths/instance/webhooks/create/CreatePage.tsx:104
#, c-format
msgid "Header"
-msgstr ""
+msgstr "Cabecera"
#: src/paths/instance/webhooks/create/CreatePage.tsx:106
#, c-format
@@ -2084,7 +2104,7 @@ msgstr ""
#: src/paths/instance/webhooks/create/CreatePage.tsx:111
#, c-format
msgid "Body"
-msgstr ""
+msgstr "Cuerpo"
#: src/paths/instance/webhooks/create/CreatePage.tsx:112
#, c-format
@@ -2114,17 +2134,17 @@ msgstr "cargar nuevas ordenes"
#: src/paths/instance/webhooks/list/Table.tsx:151
#, c-format
msgid "Event type"
-msgstr ""
+msgstr "Tipo de evento"
#: src/paths/instance/webhooks/list/Table.tsx:176
#, c-format
msgid "delete selected webhook from the database"
-msgstr ""
+msgstr "eliminar el webhook seleccionado de la base de datos"
#: src/paths/instance/webhooks/list/Table.tsx:198
#, c-format
msgid "load more webhooks after the last one"
-msgstr ""
+msgstr "cargar más webhooks después del último"
#: src/paths/instance/webhooks/list/Table.tsx:202
#, fuzzy, c-format
@@ -2164,17 +2184,17 @@ msgstr "La URL no tiene el formato correcto"
#: src/paths/instance/transfers/create/CreatePage.tsx:98
#, c-format
msgid "Credited bank account"
-msgstr ""
+msgstr "Abono en cuenta bancaria"
#: src/paths/instance/transfers/create/CreatePage.tsx:100
#, c-format
msgid "Select one account"
-msgstr ""
+msgstr "Selecciona una cuenta"
#: src/paths/instance/transfers/create/CreatePage.tsx:101
#, c-format
msgid "Bank account of the merchant where the payment was received"
-msgstr ""
+msgstr "Cuenta bancaria del comerciante donde se recibió el pago"
#: src/paths/instance/transfers/create/CreatePage.tsx:105
#, fuzzy, c-format
@@ -2187,6 +2207,8 @@ msgid ""
"unique identifier of the wire transfer used by the exchange, must be 52 "
"characters long"
msgstr ""
+"identificador único de la transferencia utilizado por la bolsa, debe tener "
+"52 caracteres"
#: src/paths/instance/transfers/create/CreatePage.tsx:112
#, c-format
@@ -2194,16 +2216,18 @@ msgid ""
"Base URL of the exchange that made the transfer, should have been in the "
"wire transfer subject"
msgstr ""
+"URL base de la bolsa que realizó la transferencia, debería haber estado en "
+"el asunto de la transferencia bancaria"
#: src/paths/instance/transfers/create/CreatePage.tsx:117
#, c-format
msgid "Amount credited"
-msgstr ""
+msgstr "Monto abonado"
#: src/paths/instance/transfers/create/CreatePage.tsx:118
#, c-format
msgid "Actual amount that was wired to the merchant's bank account"
-msgstr ""
+msgstr "Monto real que se transfirió a la cuenta bancaria del comerciante"
#: src/paths/instance/transfers/create/index.tsx:58
#, c-format
@@ -2223,7 +2247,7 @@ msgstr "cargar nuevas transferencias"
#: src/paths/instance/transfers/list/Table.tsx:129
#, c-format
msgid "load more transfers before the first one"
-msgstr ""
+msgstr "cargar más transferencias antes de la primera"
#: src/paths/instance/transfers/list/Table.tsx:133
#, c-format
@@ -2293,7 +2317,7 @@ msgstr "Dirección de cuenta"
#: src/paths/instance/transfers/list/ListPage.tsx:100
#, c-format
msgid "only show wire transfers confirmed by the merchant"
-msgstr ""
+msgstr "mostrar sólo las transferencias confirmadas por el comerciante"
#: src/paths/instance/transfers/list/ListPage.tsx:110
#, c-format
@@ -2308,7 +2332,7 @@ msgstr "Verificado"
#: src/paths/admin/create/CreatePage.tsx:69
#, c-format
msgid "is not valid"
-msgstr ""
+msgstr "no es válido"
#: src/paths/admin/create/CreatePage.tsx:94
#, fuzzy, c-format
@@ -2398,12 +2422,12 @@ msgstr "Dirección de cuenta"
#: src/components/form/InputPaytoForm.tsx:273
#, c-format
msgid "Business Identifier Code."
-msgstr ""
+msgstr "Código de identificación de la empresa."
#: src/components/form/InputPaytoForm.tsx:282
#, c-format
msgid "Bank Account Number."
-msgstr ""
+msgstr "Número de cuenta bancaria."
#: src/components/form/InputPaytoForm.tsx:292
#, c-format
@@ -2413,17 +2437,17 @@ msgstr "Interfaz de pago unificado."
#: src/components/form/InputPaytoForm.tsx:301
#, c-format
msgid "Bitcoin protocol."
-msgstr ""
+msgstr "Protocolo Bitcoin."
#: src/components/form/InputPaytoForm.tsx:310
#, c-format
msgid "Ethereum protocol."
-msgstr ""
+msgstr "Protocolo Ethereum."
#: src/components/form/InputPaytoForm.tsx:319
#, c-format
msgid "Interledger protocol."
-msgstr ""
+msgstr "Protocolo Interledger."
#: src/components/form/InputPaytoForm.tsx:328
#, c-format
@@ -2438,17 +2462,17 @@ msgstr ""
#: src/components/form/InputPaytoForm.tsx:334
#, c-format
msgid "Bank account."
-msgstr ""
+msgstr "Cuenta bancaria."
#: src/components/form/InputPaytoForm.tsx:343
#, c-format
msgid "Bank account owner's name."
-msgstr ""
+msgstr "Nombre del titular de la cuenta bancaria."
#: src/components/form/InputPaytoForm.tsx:370
#, c-format
msgid "No accounts yet."
-msgstr ""
+msgstr "Aún no hay cuentas."
#: src/components/instance/DefaultInstanceFormFields.tsx:52
#, c-format
@@ -2456,6 +2480,8 @@ msgid ""
"Name of the instance in URLs. The 'default' instance is special in that it "
"is used to administer other instances."
msgstr ""
+"Nombre de la instancia en URL. La instancia \"por defecto\" es especial, ya "
+"que se utiliza para administrar otras instancias."
#: src/components/instance/DefaultInstanceFormFields.tsx:58
#, fuzzy, c-format
@@ -2465,7 +2491,7 @@ msgstr "Nombre de edificio"
#: src/components/instance/DefaultInstanceFormFields.tsx:59
#, c-format
msgid "Legal name of the business represented by this instance."
-msgstr ""
+msgstr "Nombre legal de la empresa representada por esta instancia."
#: src/components/instance/DefaultInstanceFormFields.tsx:64
#, c-format
@@ -2485,17 +2511,17 @@ msgstr "URL de sitio web"
#: src/components/instance/DefaultInstanceFormFields.tsx:71
#, c-format
msgid "URL."
-msgstr ""
+msgstr "URL."
#: src/components/instance/DefaultInstanceFormFields.tsx:76
#, c-format
msgid "Logo"
-msgstr ""
+msgstr "Logotipo"
#: src/components/instance/DefaultInstanceFormFields.tsx:77
#, c-format
msgid "Logo image."
-msgstr ""
+msgstr "Imagen del logotipo."
#: src/components/instance/DefaultInstanceFormFields.tsx:82
#, c-format
@@ -2505,7 +2531,7 @@ msgstr "Cuenta bancaria"
#: src/components/instance/DefaultInstanceFormFields.tsx:83
#, c-format
msgid "URI specifying bank account for crediting revenue."
-msgstr ""
+msgstr "URI que especifica la cuenta bancaria para acreditar los ingresos."
#: src/components/instance/DefaultInstanceFormFields.tsx:88
#, c-format
@@ -2517,6 +2543,8 @@ msgstr "Impuesto máximo de deposito por omisión"
msgid ""
"Maximum deposit fees this merchant is willing to pay per order by default."
msgstr ""
+"Comisiones de depósito máximas que este comerciante está dispuesto a pagar "
+"por pedido por defecto."
#: src/components/instance/DefaultInstanceFormFields.tsx:94
#, c-format
@@ -2529,6 +2557,8 @@ msgid ""
"Maximum wire fees this merchant is willing to pay per wire transfer by "
"default."
msgstr ""
+"Comisiones de transferencia máximas que este comerciante está dispuesto a "
+"pagar por transferencia por defecto."
#: src/components/instance/DefaultInstanceFormFields.tsx:100
#, c-format
@@ -2541,11 +2571,13 @@ msgid ""
"Number of orders excess wire transfer fees will be divided by to compute per "
"order surcharge."
msgstr ""
+"El número de pedidos que excedan las tarifas de transferencia bancaria se "
+"dividirá para calcular el recargo por pedido."
#: src/components/instance/DefaultInstanceFormFields.tsx:107
#, c-format
msgid "Physical location of the merchant."
-msgstr ""
+msgstr "Ubicación física del comerciante."
#: src/components/instance/DefaultInstanceFormFields.tsx:114
#, c-format
@@ -2567,6 +2599,8 @@ msgstr "Retrazo de pago por omisión"
msgid ""
"Time customers have to pay an order before the offer expires by default."
msgstr ""
+"Tiempo que los clientes tienen para pagar un pedido antes de que caduque la "
+"oferta de forma predeterminada."
#: src/components/instance/DefaultInstanceFormFields.tsx:129
#, c-format
@@ -2580,6 +2614,10 @@ msgid ""
"enabling it to aggregate smaller payments into larger wire transfers and "
"reducing wire fees."
msgstr ""
+"Tiempo máximo que se le permite a un intercambio retrasar la transferencia "
+"de fondos al comerciante, lo que le permite agregar pagos más pequeños en "
+"transferencias electrónicas más grandes y reducir las tarifas de "
+"transferencia."
#: src/paths/instance/update/UpdatePage.tsx:164
#, c-format
@@ -2610,7 +2648,7 @@ msgstr "Login necesario"
#: src/components/exception/login.tsx:80
#, c-format
msgid "Please enter your access token."
-msgstr ""
+msgstr "Por favor, introduzca su clave de acceso."
#: src/components/exception/login.tsx:108
#, fuzzy, c-format
@@ -2620,7 +2658,7 @@ msgstr "Acceso denegado"
#: src/InstanceRoutes.tsx:171
#, c-format
msgid "The request to the backend take too long and was cancelled"
-msgstr ""
+msgstr "La petición al backend tardó demasiado y fue cancelada"
#: src/InstanceRoutes.tsx:172
#, c-format
diff --git a/packages/pogen/package.json b/packages/pogen/package.json
index ac57a41f0..f57dbad40 100644
--- a/packages/pogen/package.json
+++ b/packages/pogen/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/pogen",
- "version": "0.12.1",
+ "version": "0.12.2",
"bin": {
"pogen": "bin/pogen"
},
diff --git a/packages/taler-harness/debian/changelog b/packages/taler-harness/debian/changelog
index e088d2992..64f750458 100644
--- a/packages/taler-harness/debian/changelog
+++ b/packages/taler-harness/debian/changelog
@@ -1,3 +1,9 @@
+taler-harness (0.12.2) unstable; urgency=low
+
+ * Release 0.12.2
+
+ -- Florian Dold <dold@taler.net> Thu, 27 Jun 2024 20:19:19 +0200
+
taler-harness (0.12.1) unstable; urgency=low
* Release 0.12.1
diff --git a/packages/taler-harness/package.json b/packages/taler-harness/package.json
index 3b59339a6..02bebcfce 100644
--- a/packages/taler-harness/package.json
+++ b/packages/taler-harness/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-harness",
- "version": "0.12.1",
+ "version": "0.12.2",
"description": "",
"engines": {
"node": ">=0.12.0"
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-external.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-external.ts
new file mode 100644
index 000000000..3cd02882b
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-external.ts
@@ -0,0 +1,101 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionType,
+ WithdrawalType,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import { createSimpleTestkudosEnvironmentV3 } from "../harness/helpers.js";
+
+/**
+ * Test for a withdrawal that is externally confirmed.
+ */
+export async function runWithdrawalExternalTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bankClient, exchange } =
+ await createSimpleTestkudosEnvironmentV3(t);
+
+ // Create a withdrawal operation
+
+ const bankUser = await bankClient.createRandomBankUser();
+ bankClient.setAuth(bankUser);
+ const wop = await bankClient.createWithdrawalOperation(
+ bankUser.username,
+ "TESTKUDOS:10",
+ );
+
+ const talerWithdrawUri = wop.taler_withdraw_uri + "?external-confirmation=1";
+
+ // Hand it to the wallet
+
+ const detResp = await walletClient.call(
+ WalletApiOperation.GetWithdrawalDetailsForUri,
+ {
+ talerWithdrawUri: talerWithdrawUri,
+ },
+ );
+
+ const acceptResp = await walletClient.call(
+ WalletApiOperation.AcceptBankIntegratedWithdrawal,
+ {
+ exchangeBaseUrl: detResp.defaultExchangeBaseUrl!!,
+ talerWithdrawUri,
+ },
+ );
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: acceptResp.transactionId,
+ txState: {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.BankConfirmTransfer,
+ },
+ });
+
+ const txDetails = await walletClient.call(
+ WalletApiOperation.GetTransactionById,
+ {
+ transactionId: acceptResp.transactionId,
+ },
+ );
+
+ // Now we check that the external-confirmation=1 flag actually did something!
+
+ t.assertDeepEqual(txDetails.type, TransactionType.Withdrawal);
+ t.assertDeepEqual(
+ txDetails.withdrawalDetails.type,
+ WithdrawalType.TalerBankIntegrationApi,
+ );
+ t.assertDeepEqual(txDetails.withdrawalDetails.externalConfirmation, true);
+ t.assertDeepEqual(txDetails.withdrawalDetails.bankConfirmationUrl, undefined);
+
+ t.logStep("confirming withdrawal operation");
+
+ await bankClient.confirmWithdrawalOperation(bankUser.username, {
+ withdrawalOperationId: wop.withdrawal_id,
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+}
+
+runWithdrawalExternalTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts
index 238bf3b98..eb71396e7 100644
--- a/packages/taler-harness/src/integrationtests/testrunner.ts
+++ b/packages/taler-harness/src/integrationtests/testrunner.ts
@@ -108,6 +108,7 @@ import { runWalletDevExperimentsTest } from "./test-wallet-dev-experiments.js";
import { runWalletExchangeUpdateTest } from "./test-wallet-exchange-update.js";
import { runWalletGenDbTest } from "./test-wallet-gendb.js";
import { runWalletInsufficientBalanceTest } from "./test-wallet-insufficient-balance.js";
+import { runWalletNetworkAvailabilityTest } from "./test-wallet-network-availability.js";
import { runWalletNotificationsTest } from "./test-wallet-notifications.js";
import { runWalletObservabilityTest } from "./test-wallet-observability.js";
import { runWalletRefreshErrorsTest } from "./test-wallet-refresh-errors.js";
@@ -118,13 +119,13 @@ import { runWithdrawalAbortBankTest } from "./test-withdrawal-abort-bank.js";
import { runWithdrawalAmountTest } from "./test-withdrawal-amount.js";
import { runWithdrawalBankIntegratedTest } from "./test-withdrawal-bank-integrated.js";
import { runWithdrawalConversionTest } from "./test-withdrawal-conversion.js";
+import { runWithdrawalExternalTest } from "./test-withdrawal-external.js";
import { runWithdrawalFakebankTest } from "./test-withdrawal-fakebank.js";
import { runWithdrawalFeesTest } from "./test-withdrawal-fees.js";
import { runWithdrawalFlexTest } from "./test-withdrawal-flex.js";
import { runWithdrawalHandoverTest } from "./test-withdrawal-handover.js";
import { runWithdrawalHugeTest } from "./test-withdrawal-huge.js";
import { runWithdrawalManualTest } from "./test-withdrawal-manual.js";
-import { runWalletNetworkAvailabilityTest } from "./test-wallet-network-availability.js";
/**
* Test runner.
@@ -240,6 +241,7 @@ const allTests: TestMainFunction[] = [
runWithdrawalFlexTest,
runExchangeMasterPubChangeTest,
runMerchantCategoriesTest,
+ runWithdrawalExternalTest,
];
export interface TestRunSpec {
diff --git a/packages/taler-util/package.json b/packages/taler-util/package.json
index 11213cdfa..c165489b3 100644
--- a/packages/taler-util/package.json
+++ b/packages/taler-util/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-util",
- "version": "0.12.1",
+ "version": "0.12.2",
"description": "Generic helper functionality for GNU Taler",
"type": "module",
"types": "./lib/index.node.d.ts",
diff --git a/packages/taler-util/src/http-client/challenger.ts b/packages/taler-util/src/http-client/challenger.ts
index e7b128b02..6a920749c 100644
--- a/packages/taler-util/src/http-client/challenger.ts
+++ b/packages/taler-util/src/http-client/challenger.ts
@@ -164,8 +164,6 @@ export class ChallengerHttpClient {
}
case HttpStatusCode.BadRequest:
return opKnownHttpFailure(resp.status, resp);
- case HttpStatusCode.Forbidden:
- return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotAcceptable:
@@ -205,7 +203,7 @@ export class ChallengerHttpClient {
case HttpStatusCode.BadRequest:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Forbidden:
- return opKnownHttpFailure(resp.status, resp);
+ return opKnownAlternativeFailure(resp, HttpStatusCode.Forbidden, codecForChallengeInvalidPinResponse());
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotAcceptable:
diff --git a/packages/taler-util/src/http-client/types.ts b/packages/taler-util/src/http-client/types.ts
index 6e758773c..3816b1598 100644
--- a/packages/taler-util/src/http-client/types.ts
+++ b/packages/taler-util/src/http-client/types.ts
@@ -1567,6 +1567,14 @@ export const codecForChallengerTermsOfServiceResponse =
.property("name", codecForConstString("challenger"))
.property("version", codecForString())
.property("implementation", codecOptional(codecForString()))
+ .property("restrictions", codecOptional(codecForMap(codecForAny())))
+ .property(
+ "address_type",
+ codecForEither(
+ codecForConstString("phone"),
+ codecForConstString("email"),
+ ),
+ )
.build("ChallengerApi.ChallengerTermsOfServiceResponse");
export const codecForChallengeSetupResponse =
@@ -1578,10 +1586,13 @@ export const codecForChallengeSetupResponse =
export const codecForChallengeStatus =
(): Codec<ChallengerApi.ChallengeStatus> =>
buildCodecForObject<ChallengerApi.ChallengeStatus>()
- .property("restrictions", codecOptional(codecForMap(codecForAny())))
.property("fix_address", codecForBoolean())
+ .property("solved", codecForBoolean())
.property("last_address", codecOptional(codecForMap(codecForAny())))
.property("changes_left", codecForNumber())
+ .property("retransmission_time", codecForTimestamp)
+ .property("pin_transmissions_left", codecForNumber())
+ .property("auth_attempts_left", codecForNumber())
.build("ChallengerApi.ChallengeStatus");
export const codecForChallengeResponse =
@@ -1596,10 +1607,10 @@ export const codecForChallengeCreateResponse =
(): Codec<ChallengerApi.ChallengeCreateResponse> =>
buildCodecForObject<ChallengerApi.ChallengeCreateResponse>()
.property("attempts_left", codecForNumber())
- .property("address", codecForAny())
.property("type", codecForConstString("created"))
+ .property("address", codecForAny())
.property("transmitted", codecForBoolean())
- .property("next_tx_time", codecForString())
+ .property("retransmission_time", codecForTimestamp)
.build("ChallengerApi.ChallengeCreateResponse");
export const codecForChallengeRedirect =
@@ -5385,6 +5396,19 @@ export namespace ChallengerApi {
// URN of the implementation (needed to interpret 'revision' in version).
// @since v0, may become mandatory in the future.
implementation?: string;
+
+ // Object; map of keys (names of the fields of the address
+ // to be entered by the user) to objects with a "regex" (string)
+ // containing an extended Posix regular expression for allowed
+ // address field values, and a "hint"/"hint_i18n" giving a
+ // human-readable explanation to display if the value entered
+ // by the user does not match the regex. Keys that are not mapped
+ // to such an object have no restriction on the value provided by
+ // the user. See "ADDRESS_RESTRICTIONS" in the challenger configuration.
+ restrictions: Record<string, Restriction> | undefined;
+
+ // @since v2.
+ address_type: "email" | "phone";
}
export interface ChallengeSetupResponse {
@@ -5399,16 +5423,6 @@ export namespace ChallengerApi {
}
export interface ChallengeStatus {
- // Object; map of keys (names of the fields of the address
- // to be entered by the user) to objects with a "regex" (string)
- // containing an extended Posix regular expression for allowed
- // address field values, and a "hint"/"hint_i18n" giving a
- // human-readable explanation to display if the value entered
- // by the user does not match the regex. Keys that are not mapped
- // to such an object have no restriction on the value provided by
- // the user. See "ADDRESS_RESTRICTIONS" in the challenger configuration.
- restrictions: Record<string, Restriction> | undefined;
-
// indicates if the given address cannot be changed anymore, the
// form should be read-only if set to true.
fix_address: boolean;
@@ -5420,6 +5434,25 @@ export namespace ChallengerApi {
// number of times the address can still be changed, may or may not be
// shown to the user
changes_left: Integer;
+
+ // is the challenge already solved?
+ solved: boolean;
+
+ // when we would re-transmit the challenge the next
+ // time (at the earliest) if requested by the user
+ // only present if challenge already created
+ // @since v2
+ retransmission_time: Timestamp;
+
+ // how many times might the PIN still be retransmitted
+ // only present if challenge already created
+ // @since v2
+ pin_transmissions_left: Integer;
+
+ // how many times might the user still try entering the PIN code
+ // only present if challenge already created
+ // @since v2
+ auth_attempts_left: Integer;
}
export type ChallengeResponse = ChallengeRedirect | ChallengeCreateResponse;
@@ -5447,7 +5480,7 @@ export namespace ChallengerApi {
// timestamp explaining when we would re-transmit the challenge the next
// time (at the earliest) if requested by the user
- next_tx_time: string;
+ retransmission_time: TalerProtocolTimestamp;
}
export type ChallengeSolveResponse = ChallengeRedirect | InvalidPinResponse;
diff --git a/packages/taler-util/src/operation.ts b/packages/taler-util/src/operation.ts
index e2ab9d4e4..2d17238dc 100644
--- a/packages/taler-util/src/operation.ts
+++ b/packages/taler-util/src/operation.ts
@@ -146,7 +146,10 @@ export function opKnownTalerFailure<T extends TalerErrorCode>(
return { type: "fail", case: s, detail };
}
-export function opUnknownFailure(resp: HttpResponse, error: TalerErrorDetail): never {
+export function opUnknownFailure(
+ resp: HttpResponse,
+ error: TalerErrorDetail,
+): never {
throw TalerError.fromDetail(
TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
{
@@ -179,15 +182,51 @@ export function narrowOpSuccessOrThrow<Body, ErrorEnum>(
}
}
+export async function succeedOrThrow<R, E>(
+ promise: Promise<OperationResult<R, E>>,
+): Promise<R> {
+ const resp = await promise;
+ if (isOperationOk(resp)) {
+ return resp.body;
+ }
+
+ if (isOperationFail(resp)) {
+ throw TalerError.fromUncheckedDetail({ ...resp, case: resp.case } as any);
+ }
+ throw TalerError.fromException(resp);
+}
+
+export async function failOrThrow<E>(
+ s: E,
+ promise: Promise<OperationResult<unknown, E>>,
+): Promise<TalerErrorDetail | undefined> {
+ const resp = await promise;
+ 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,
> = TT[p] extends (...args: any[]) => infer Ret
? Ret extends Promise<infer Result>
- ? Result extends OperationResult<any, any>
- ? Result
- : never
- : never //api always use Promises
+ ? Result extends OperationResult<any, any>
+ ? Result
+ : never
+ : never //api always use Promises
: never; //error cases just for functions
export type FailCasesByMethod<TT extends object, p extends keyof TT> = Exclude<
@@ -195,4 +234,4 @@ export type FailCasesByMethod<TT extends object, p extends keyof TT> = Exclude<
OperationOk<any>
>;
-export type RedirectResult = { redirectURL: URL }
+export type RedirectResult = { redirectURL: URL };
diff --git a/packages/taler-util/src/qr.ts b/packages/taler-util/src/qr.ts
index 372291250..4d90ccf14 100644
--- a/packages/taler-util/src/qr.ts
+++ b/packages/taler-util/src/qr.ts
@@ -34,6 +34,9 @@ function encodePaytoAsSwissQrBill(paytoUri: string): EncodeResult {
return { type: "skip" };
}
const amountStr = parsedPayto.params["amount"];
+ if (amountStr === undefined) {
+ return { type: "skip" };
+ }
const iban = parsedPayto.targetPath;
const countryCode = iban.slice(0, 2);
const lines = [
@@ -105,7 +108,9 @@ function encodePaytoAsEpcQr(paytoUri: string): EncodeResult {
"", // optional BIC
parsedPayto.params["receiver-name"], // Beneficiary name
parsedPayto.targetPath, // Beneficiary IBAN
- `${Amounts.currencyOf(amountStr)}${Amounts.stringifyValue(amountStr, 2)}`, // Amount
+ amountStr !== undefined
+ ? `${Amounts.currencyOf(amountStr)}${Amounts.stringifyValue(amountStr, 2)}`
+ : "", // Amount (optional)
"", // AT-44 Purpose
parsedPayto.params["message"], // AT-05 Unstructured remittance information
];
diff --git a/packages/taler-util/src/taler-types.ts b/packages/taler-util/src/taler-types.ts
index 66f98ea9a..ac42ca278 100644
--- a/packages/taler-util/src/taler-types.ts
+++ b/packages/taler-util/src/taler-types.ts
@@ -723,6 +723,8 @@ export class ExchangeKeysJson {
currency: string;
+ currency_specification?: CurrencySpecification;
+
/**
* The exchange's master public key.
*/
@@ -1504,6 +1506,7 @@ export const codecForExchangeKeysJson = (): Codec<ExchangeKeysJson> =>
buildCodecForObject<ExchangeKeysJson>()
.property("base_url", codecForString())
.property("currency", codecForString())
+ .property("currency_specification", codecOptional(codecForCurrencySpecificiation()))
.property("master_public_key", codecForString())
.property("auditors", codecForList(codecForAuditor()))
.property("list_issue_date", codecForTimestamp)
diff --git a/packages/taler-util/src/taleruri.test.ts b/packages/taler-util/src/taleruri.test.ts
index b92366fb3..d80470dab 100644
--- a/packages/taler-util/src/taleruri.test.ts
+++ b/packages/taler-util/src/taleruri.test.ts
@@ -54,6 +54,18 @@ test("taler withdraw uri parsing", (t) => {
t.is(r1.bankIntegrationApiBaseUrl, "https://bank.example.com/");
});
+test("taler withdraw uri parsing with external confirmation", (t) => {
+ const url1 = "taler://withdraw/bank.example.com/12345?external-confirmation=1";
+ const r1 = parseWithdrawUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.externalConfirmation, true);
+ t.is(r1.withdrawalOperationId, "12345");
+ t.is(r1.bankIntegrationApiBaseUrl, "https://bank.example.com/");
+});
+
test("taler withdraw uri parsing (http)", (t) => {
const url1 = "taler+http://withdraw/bank.example.com/12345";
const r1 = parseWithdrawUri(url1);
diff --git a/packages/taler-util/src/taleruri.ts b/packages/taler-util/src/taleruri.ts
index 54b7525e3..d3186d2f5 100644
--- a/packages/taler-util/src/taleruri.ts
+++ b/packages/taler-util/src/taleruri.ts
@@ -29,6 +29,7 @@ import { opFixedSuccess, opKnownTalerFailure } from "./operation.js";
import { TalerErrorCode } from "./taler-error-codes.js";
import { AmountString } from "./taler-types.js";
import { URL, URLSearchParams } from "./url.js";
+
/**
* A parsed taler URI.
*/
@@ -89,6 +90,7 @@ export interface WithdrawUriResult {
type: TalerUriAction.Withdraw;
bankIntegrationApiBaseUrl: string;
withdrawalOperationId: string;
+ externalConfirmation?: boolean;
}
export interface RefundUriResult {
@@ -140,7 +142,12 @@ export function parseWithdrawUriWithError(s: string) {
if (pi.type === "fail") {
return pi;
}
- const parts = pi.body.rest.split("/");
+
+ const c = pi.body.rest.split("?", 2);
+ const path = c[0];
+ const q = new URLSearchParams(c[1] ?? "");
+
+ const parts = path.split("/");
if (parts.length < 2) {
return opKnownTalerFailure(TalerErrorCode.WALLET_TALER_URI_MALFORMED, {
@@ -166,6 +173,7 @@ export function parseWithdrawUriWithError(s: string) {
`${pi.body.innerProto}://${p}/`,
),
withdrawalOperationId: withdrawId,
+ externalConfirmation: q.get("external-confirmation") == "1",
};
return opFixedSuccess(result);
}
diff --git a/packages/taler-util/src/transactions-types.ts b/packages/taler-util/src/transactions-types.ts
index a6ac5aec6..b4e2738ee 100644
--- a/packages/taler-util/src/transactions-types.ts
+++ b/packages/taler-util/src/transactions-types.ts
@@ -299,6 +299,11 @@ interface WithdrawalDetailsForTalerBankIntegrationApi {
*/
reserveIsReady: boolean;
+ /**
+ * Is the bank transfer for the withdrawal externally confirmed?
+ */
+ externalConfirmation?: boolean;
+
exchangeCreditAccountDetails?: WithdrawalExchangeAccountDetails[];
}
diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts
index 2c92d9295..ec401f3f6 100644
--- a/packages/taler-util/src/wallet-types.ts
+++ b/packages/taler-util/src/wallet-types.ts
@@ -558,11 +558,13 @@ export enum ScopeType {
}
export type ScopeInfoGlobal = { type: ScopeType.Global; currency: string };
+
export type ScopeInfoExchange = {
type: ScopeType.Exchange;
currency: string;
url: string;
};
+
export type ScopeInfoAuditor = {
type: ScopeType.Auditor;
currency: string;
@@ -571,6 +573,22 @@ export type ScopeInfoAuditor = {
export type ScopeInfo = ScopeInfoGlobal | ScopeInfoExchange | ScopeInfoAuditor;
+/**
+ * Encode scope info as a string.
+ *
+ * Format must be stable as it's used in the database.
+ */
+export function stringifyScopeInfo(si: ScopeInfo): string {
+ switch (si.type) {
+ case ScopeType.Global:
+ return `taler-si:global/${si.currency}}`;
+ case ScopeType.Auditor:
+ return `taler-si:auditor/${si.currency}/${encodeURIComponent(si.url)}`;
+ case ScopeType.Exchange:
+ return `taler-si:exchange/${si.currency}/${encodeURIComponent(si.url)}`;
+ }
+}
+
export interface BalancesResponse {
balances: WalletBalance[];
}
@@ -3439,3 +3457,9 @@ export const codecForGetQrCodesForPaytoRequest = () =>
export interface GetQrCodesForPaytoResponse {
codes: QrCodeSpec[];
}
+
+export type EmptyObject = Record<string, never>;
+
+export const codecForEmptyObject = (): Codec<EmptyObject> =>
+ buildCodecForObject<EmptyObject>()
+ .build("EmptyObject"); \ No newline at end of file
diff --git a/packages/taler-wallet-cli/debian/changelog b/packages/taler-wallet-cli/debian/changelog
index fde3084fa..7ef8445dc 100644
--- a/packages/taler-wallet-cli/debian/changelog
+++ b/packages/taler-wallet-cli/debian/changelog
@@ -1,14 +1,14 @@
-taler-wallet-cli (0.12.1) unstable; urgency=low
+taler-wallet-cli (0.12.2) unstable; urgency=low
- * Release 0.12.1
+ * Release 0.12.2
- -- Florian Dold <dold@taler.net> Wed, 26 Jun 2024 09:30:32 -0600
+ -- Florian Dold <dold@taler.net> Thu, 27 Jun 2024 20:19:19 +0200
-taler-wallet-cli (v0.12.1) unstable; urgency=low
+taler-wallet-cli (0.12.1) unstable; urgency=low
- * Release v0.12.1
+ * Release 0.12.1
- -- Florian Dold <dold@taler.net> Wed, 26 Jun 2024 09:30:20 -0600
+ -- Florian Dold <dold@taler.net> Wed, 26 Jun 2024 09:30:32 -0600
taler-wallet-cli (0.12.0) unstable; urgency=low
diff --git a/packages/taler-wallet-cli/package.json b/packages/taler-wallet-cli/package.json
index f04fa43fa..3430d525d 100644
--- a/packages/taler-wallet-cli/package.json
+++ b/packages/taler-wallet-cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-wallet-cli",
- "version": "0.12.1",
+ "version": "0.12.2",
"description": "",
"engines": {
"node": ">=0.18.0"
diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts
index c165b548c..be74e464b 100644
--- a/packages/taler-wallet-cli/src/index.ts
+++ b/packages/taler-wallet-cli/src/index.ts
@@ -1380,13 +1380,8 @@ advancedCli
advancedCli
.subcommand("pending", "pending", { help: "Show pending operations." })
.action(async (args) => {
- await withWallet(args, { lazyTaskLoop: true }, async (wallet) => {
- const pending = await wallet.client.call(
- WalletApiOperation.GetPendingOperations,
- {},
- );
- console.log(JSON.stringify(pending, undefined, 2));
- });
+ console.error("Subcommand removed due to deprecation.");
+ process.exit(1);
});
advancedCli
diff --git a/packages/taler-wallet-core/package.json b/packages/taler-wallet-core/package.json
index ff068806a..273ad75f6 100644
--- a/packages/taler-wallet-core/package.json
+++ b/packages/taler-wallet-core/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-wallet-core",
- "version": "0.12.1",
+ "version": "0.12.2",
"description": "",
"engines": {
"node": ">=0.18.0"
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index 3438cbdc7..336ffab67 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -40,6 +40,7 @@ import {
CoinPublicKeyString,
CoinRefreshRequest,
CoinStatus,
+ CurrencySpecification,
DenomLossEventType,
DenomSelectionState,
DenominationInfo,
@@ -51,6 +52,7 @@ import {
HashCodeString,
Logger,
RefreshReason,
+ ScopeInfo,
TalerErrorDetail,
TalerPreciseTimestamp,
TalerProtocolDuration,
@@ -61,6 +63,7 @@ import {
WireInfo,
WithdrawalExchangeAccountDetails,
codecForAny,
+ stringifyScopeInfo,
} from "@gnu-taler/taler-util";
import { DbRetryInfo, TaskIdentifiers } from "./common.js";
import {
@@ -400,6 +403,8 @@ export interface ReserveBankInfo {
wireTypes: string[] | undefined;
currency: string | undefined;
+
+ externalConfirmation?: boolean;
}
/**
@@ -907,8 +912,8 @@ export interface CoinRecord {
/**
* History item for a coin.
- *
- * DB-specific format,
+ *
+ * DB-specific format,
*/
export type DbWalletCoinHistoryItem =
| {
@@ -2368,6 +2373,23 @@ export interface DenomLossEventRecord {
exchangeBaseUrl: string;
}
+export interface CurrencyInfoRecord {
+ /**
+ * Stringified scope info.
+ */
+ scopeInfoStr: string;
+
+ /**
+ * Currency specification.
+ */
+ currencySpec: CurrencySpecification;
+
+ /**
+ * How did the currency info get set?
+ */
+ source: "exchange" | "user" | "preset";
+}
+
/**
* Schema definition for the IndexedDB
* wallet database.
@@ -2402,6 +2424,11 @@ export const WalletStoresV1 = {
}),
},
}),
+ currencyInfo: describeStoreV2({
+ recordCodec: passthroughCodec<CurrencyInfoRecord>(),
+ storeName: "currencyInfo",
+ keyPath: "scopeInfoStr",
+ }),
globalCurrencyAuditors: describeStoreV2({
recordCodec: passthroughCodec<GlobalCurrencyAuditorRecord>(),
storeName: "globalCurrencyAuditors",
@@ -3360,3 +3387,75 @@ export async function deleteTalerDatabase(
req.onsuccess = () => resolve();
});
}
+
+/**
+ * High-level helpers to access the database.
+ * Eventually all access to the database should
+ * go through helpers in this namespace.
+ */
+export namespace WalletDbHelpers {
+ export interface GetCurrencyInfoDbResult {
+ /**
+ * Currency specification.
+ */
+ currencySpec: CurrencySpecification;
+
+ /**
+ * How did the currency info get set?
+ */
+ source: "exchange" | "user" | "preset";
+ }
+
+ export interface StoreCurrencyInfoDbRequest {
+ scopeInfo: ScopeInfo;
+ currencySpec: CurrencySpecification;
+ source: "exchange" | "user" | "preset";
+ }
+
+ export async function getCurrencyInfo(
+ tx: WalletDbReadOnlyTransaction<["currencyInfo"]>,
+ scopeInfo: ScopeInfo,
+ ): Promise<GetCurrencyInfoDbResult | undefined> {
+ const s = stringifyScopeInfo(scopeInfo);
+ const res = await tx.currencyInfo.get(s);
+ if (!res) {
+ return undefined;
+ }
+ return {
+ currencySpec: res.currencySpec,
+ source: res.source,
+ };
+ }
+
+ /**
+ * Store currency info for a scope.
+ *
+ * Overrides existing currency infos.
+ */
+ export async function upsertCurrencyInfo(
+ tx: WalletDbReadWriteTransaction<["currencyInfo"]>,
+ req: StoreCurrencyInfoDbRequest,
+ ): Promise<void> {
+ await tx.currencyInfo.put({
+ scopeInfoStr: stringifyScopeInfo(req.scopeInfo),
+ currencySpec: req.currencySpec,
+ source: req.source,
+ });
+ }
+
+ export async function insertCurrencyInfoUnlessExists(
+ tx: WalletDbReadWriteTransaction<["currencyInfo"]>,
+ req: StoreCurrencyInfoDbRequest,
+ ): Promise<void> {
+ const scopeInfoStr = stringifyScopeInfo(req.scopeInfo);
+ const oldRec = await tx.currencyInfo.get(scopeInfoStr);
+ if (oldRec) {
+ return;
+ }
+ await tx.currencyInfo.put({
+ scopeInfoStr: stringifyScopeInfo(req.scopeInfo),
+ currencySpec: req.currencySpec,
+ source: req.source,
+ });
+ }
+}
diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts
index 8fa439715..ab3f95214 100644
--- a/packages/taler-wallet-core/src/exchanges.ts
+++ b/packages/taler-wallet-core/src/exchanges.ts
@@ -31,6 +31,7 @@ import {
CancellationToken,
CoinRefreshRequest,
CoinStatus,
+ CurrencySpecification,
DeleteExchangeRequest,
DenomKeyType,
DenomLossEventType,
@@ -124,6 +125,7 @@ import {
ExchangeEntryDbRecordStatus,
ExchangeEntryDbUpdateStatus,
ExchangeEntryRecord,
+ WalletDbHelpers,
WalletDbReadOnlyTransaction,
WalletDbReadWriteTransaction,
WalletStoresV1,
@@ -710,6 +712,7 @@ export interface ExchangeKeysDownloadResult {
globalFees: GlobalFees[];
accounts: ExchangeWireAccount[];
wireFees: { [methodName: string]: WireFeesJson[] };
+ currencySpecification?: CurrencySpecification;
}
/**
@@ -872,6 +875,7 @@ async function downloadExchangeKeysInfo(
globalFees: exchangeKeysJsonUnchecked.global_fees,
accounts: exchangeKeysJsonUnchecked.accounts,
wireFees: exchangeKeysJsonUnchecked.wire_fees,
+ currencySpecification: exchangeKeysJsonUnchecked.currency_specification,
};
}
@@ -1470,6 +1474,7 @@ export async function updateExchangeFromUrlHandler(
"recoupGroups",
"coinAvailability",
"denomLossEvents",
+ "currencyInfo",
],
},
async (tx) => {
@@ -1575,6 +1580,19 @@ export async function updateExchangeFromUrlHandler(
r.updateStatus = ExchangeEntryDbUpdateStatus.Ready;
r.cachebreakNextUpdate = false;
await tx.exchanges.put(r);
+
+ if (keysInfo.currencySpecification) {
+ await WalletDbHelpers.insertCurrencyInfoUnlessExists(tx, {
+ currencySpec: keysInfo.currencySpecification,
+ scopeInfo: {
+ type: ScopeType.Exchange,
+ currency: newDetails.currency,
+ url: exchangeBaseUrl,
+ },
+ source: "exchange",
+ });
+ }
+
const drRowId = await tx.exchangeDetails.put(newDetails);
checkDbInvariant(
typeof drRowId.key === "number",
diff --git a/packages/taler-wallet-core/src/transactions.ts b/packages/taler-wallet-core/src/transactions.ts
index 8268828be..0649f9ce2 100644
--- a/packages/taler-wallet-core/src/transactions.ts
+++ b/packages/taler-wallet-core/src/transactions.ts
@@ -768,7 +768,10 @@ function buildTransactionForBankIntegratedWithdraw(
confirmed: wg.wgInfo.bankInfo.timestampBankConfirmed ? true : false,
exchangeCreditAccountDetails: wg.wgInfo.exchangeCreditAccounts,
reservePub: wg.reservePub,
- bankConfirmationUrl: wg.wgInfo.bankInfo.confirmUrl,
+ bankConfirmationUrl: wg.wgInfo.bankInfo.externalConfirmation
+ ? undefined
+ : wg.wgInfo.bankInfo.confirmUrl,
+ externalConfirmation: wg.wgInfo.bankInfo.externalConfirmation,
reserveIsReady:
wg.status === WithdrawalGroupStatus.Done ||
wg.status === WithdrawalGroupStatus.PendingReady,
diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts
index ce8be2927..cd17bc8cd 100644
--- a/packages/taler-wallet-core/src/wallet-api-types.ts
+++ b/packages/taler-wallet-core/src/wallet-api-types.ts
@@ -59,6 +59,7 @@ import {
DeleteExchangeRequest,
DeleteStoredBackupRequest,
DeleteTransactionRequest,
+ EmptyObject,
ExchangeDetailedResponse,
ExchangesListResponse,
ExchangesShortListResponse,
@@ -202,7 +203,6 @@ export enum WalletApiOperation {
GetUserAttentionRequests = "getUserAttentionRequests",
GetUserAttentionUnreadCount = "getUserAttentionUnreadCount",
MarkAttentionRequestAsRead = "markAttentionRequestAsRead",
- GetPendingOperations = "getPendingOperations",
GetActiveTasks = "getActiveTasks",
SetExchangeTosAccepted = "setExchangeTosAccepted",
SetExchangeTosForgotten = "setExchangeTosForgotten",
@@ -287,8 +287,6 @@ export enum WalletApiOperation {
// group: Initialization
-type EmptyObject = Record<string, never>;
-
/**
* Initialize wallet-core.
*
@@ -1142,17 +1140,6 @@ export type GetUserAttentionsUnreadCount = {
response: UserAttentionsCountResponse;
};
-/**
- * Get wallet-internal pending tasks.
- *
- * @deprecated
- */
-export type GetPendingTasksOp = {
- op: WalletApiOperation.GetPendingOperations;
- request: EmptyObject;
- response: any;
-};
-
export type GetActiveTasksOp = {
op: WalletApiOperation.GetActiveTasks;
request: EmptyObject;
@@ -1301,7 +1288,6 @@ export type WalletOperations = {
[WalletApiOperation.GetTransactionById]: GetTransactionByIdOp;
[WalletApiOperation.GetWithdrawalTransactionByUri]: GetWithdrawalTransactionByUriOp;
[WalletApiOperation.RetryPendingNow]: RetryPendingNowOp;
- [WalletApiOperation.GetPendingOperations]: GetPendingTasksOp;
[WalletApiOperation.GetActiveTasks]: GetActiveTasksOp;
[WalletApiOperation.GetUserAttentionRequests]: GetUserAttentionRequests;
[WalletApiOperation.GetUserAttentionUnreadCount]: GetUserAttentionsUnreadCount;
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index 4e5fdab71..536f559d4 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -24,24 +24,48 @@
*/
import { IDBDatabase, IDBFactory } from "@gnu-taler/idb-bridge";
import {
+ AbortTransactionRequest,
AbsoluteTime,
+ AcceptManualWithdrawalRequest,
+ AcceptManualWithdrawalResult,
ActiveTask,
+ AddExchangeRequest,
+ AddKnownBankAccountsRequest,
AmountJson,
AmountString,
Amounts,
CancellationToken,
CoinDumpJson,
CoinStatus,
+ ConfirmPayRequest,
+ ConfirmPayResult,
CoreApiResponse,
CreateStoredBackupResponse,
DeleteStoredBackupRequest,
DenominationInfo,
Duration,
+ EmptyObject,
ExchangesShortListResponse,
+ FailTransactionRequest,
+ ForgetKnownBankAccountsRequest,
+ GetActiveTasksResponse,
+ GetContractTermsDetailsRequest,
+ GetCurrencySpecificationRequest,
GetCurrencySpecificationResponse,
+ GetDepositWireTypesForCurrencyRequest,
+ GetDepositWireTypesForCurrencyResponse,
+ GetExchangeTosRequest,
+ GetExchangeTosResult,
+ GetQrCodesForPaytoRequest,
+ GetQrCodesForPaytoResponse,
+ HintNetworkAvailabilityRequest,
+ InitRequest,
InitResponse,
+ IntegrationTestArgs,
+ IntegrationTestV2Args,
KnownBankAccounts,
KnownBankAccountsInfo,
+ ListExchangesForScopedCurrencyRequest,
ListGlobalCurrencyAuditorsResponse,
ListGlobalCurrencyExchangesResponse,
Logger,
@@ -54,22 +78,34 @@ import {
PrepareWithdrawExchangeRequest,
PrepareWithdrawExchangeResponse,
RecoverStoredBackupRequest,
+ SharePaymentRequest,
+ SharePaymentResult,
+ StartRefundQueryRequest,
StoredBackupList,
+ SuspendTransactionRequest,
TalerBankIntegrationHttpClient,
TalerError,
TalerErrorCode,
TalerProtocolTimestamp,
TalerUriAction,
+ TestingGetDenomStatsRequest,
TestingGetDenomStatsResponse,
+ TestingGetReserveHistoryRequest,
+ TestingListTasksForTransactionRequest,
TestingListTasksForTransactionsResponse,
TestingWaitTransactionRequest,
TimerAPI,
TimerGroup,
TransactionType,
+ TransactionsResponse,
+ UpdateExchangeEntryRequest,
+ ValidateIbanRequest,
ValidateIbanResponse,
+ WalletContractData,
WalletCoreVersion,
WalletNotification,
WalletRunConfig,
+ WithdrawTestBalanceRequest,
canonicalizeBaseUrl,
checkDbInvariant,
codecForAbortTransaction,
@@ -95,6 +131,7 @@ import {
codecForDeleteExchangeRequest,
codecForDeleteStoredBackupRequest,
codecForDeleteTransactionRequest,
+ codecForEmptyObject,
codecForFailTransactionRequest,
codecForForceRefreshRequest,
codecForForgetKnownBankAccounts,
@@ -156,7 +193,6 @@ import {
parseTalerUri,
performanceNow,
safeStringifyException,
- sampleWalletCoreTransactions,
setDangerousTimetravel,
validateIban,
} from "@gnu-taler/taler-util";
@@ -170,6 +206,7 @@ import {
markAttentionRequestAsRead,
} from "./attention.js";
import {
+ RunBackupCycleRequest,
addBackupProvider,
codecForAddBackupProviderRequest,
codecForRemoveBackupProvider,
@@ -191,6 +228,7 @@ import {
CoinSourceType,
ConfigRecordKey,
DenominationRecord,
+ WalletDbHelpers,
WalletDbReadOnlyTransaction,
WalletStoresV1,
clearDatabase,
@@ -680,16 +718,493 @@ async function handlePrepareWithdrawExchange(
};
}
-/**
- * Response returned from the pending operations API.
- *
- * @deprecated this is a placeholder for the response type of a deprecated wallet-core request.
- */
-export interface PendingOperationsResponse {
- /**
- * List of pending operations.
- */
- pendingOperations: any[];
+async function handleRetryPendingNow(
+ wex: WalletExecutionContext,
+): Promise<EmptyObject> {
+ logger.error("retryPendingNow currently not implemented");
+ return {};
+}
+
+async function handleSharePayment(
+ wex: WalletExecutionContext,
+ req: SharePaymentRequest,
+): Promise<SharePaymentResult> {
+ return await sharePayment(wex, req.merchantBaseUrl, req.orderId);
+}
+
+async function handleDeleteStoredBackup(
+ wex: WalletExecutionContext,
+ req: DeleteStoredBackupRequest,
+): Promise<EmptyObject> {
+ await deleteStoredBackup(wex, req);
+ return {};
+}
+
+async function handleRecoverStoredBackup(
+ wex: WalletExecutionContext,
+ req: RecoverStoredBackupRequest,
+): Promise<EmptyObject> {
+ await recoverStoredBackup(wex, req);
+ return {};
+}
+
+async function handleSetWalletRunConfig(
+ wex: WalletExecutionContext,
+ req: InitRequest,
+) {
+ if (logger.shouldLogTrace()) {
+ const initType = wex.ws.initCalled
+ ? "repeat initialization"
+ : "first initialization";
+ logger.trace(`init request (${initType}): ${j2s(req)}`);
+ }
+
+ // Write to the DB to make sure that we're failing early in
+ // case the DB is not writeable.
+ try {
+ await wex.db.runReadWriteTx({ storeNames: ["config"] }, async (tx) => {
+ tx.config.put({
+ key: ConfigRecordKey.LastInitInfo,
+ value: timestampProtocolToDb(TalerProtocolTimestamp.now()),
+ });
+ });
+ } catch (e) {
+ logger.error("error writing to database during initialization");
+ throw TalerError.fromDetail(TalerErrorCode.WALLET_DB_UNAVAILABLE, {
+ innerError: getErrorDetailFromException(e),
+ });
+ }
+ wex.ws.initWithConfig(applyRunConfigDefaults(req.config));
+
+ if (wex.ws.config.testing.skipDefaults) {
+ logger.trace("skipping defaults");
+ } else {
+ logger.trace("filling defaults");
+ await fillDefaults(wex);
+ }
+ const resp: InitResponse = {
+ versionInfo: handleGetVersion(wex),
+ };
+
+ if (req.config?.lazyTaskLoop) {
+ logger.trace("lazily starting task loop");
+ } else {
+ await wex.taskScheduler.ensureRunning();
+ }
+
+ wex.ws.initCalled = true;
+ return resp;
+}
+
+async function handleWithdrawTestkudos(wex: WalletExecutionContext) {
+ await withdrawTestBalance(wex, {
+ amount: "TESTKUDOS:10" as AmountString,
+ corebankApiBaseUrl: "https://bank.test.taler.net/",
+ exchangeBaseUrl: "https://exchange.test.taler.net/",
+ });
+ // FIXME: Is this correct?
+ return {
+ versionInfo: handleGetVersion(wex),
+ };
+}
+
+async function handleWithdrawTestBalance(
+ wex: WalletExecutionContext,
+ req: WithdrawTestBalanceRequest,
+): Promise<EmptyObject> {
+ await withdrawTestBalance(wex, req);
+ return {};
+}
+
+async function handleTestingListTasksForTransaction(
+ wex: WalletExecutionContext,
+ req: TestingListTasksForTransactionRequest,
+): Promise<TestingListTasksForTransactionsResponse> {
+ return {
+ taskIdList: listTaskForTransactionId(req.transactionId),
+ };
+}
+
+async function handleRunIntegrationTest(
+ wex: WalletExecutionContext,
+ req: IntegrationTestArgs,
+): Promise<EmptyObject> {
+ await runIntegrationTest(wex, req);
+ return {};
+}
+
+async function handleRunIntegrationTestV2(
+ wex: WalletExecutionContext,
+ req: IntegrationTestV2Args,
+): Promise<EmptyObject> {
+ await runIntegrationTest2(wex, req);
+ return {};
+}
+
+async function handleValidateIban(
+ wex: WalletExecutionContext,
+ req: ValidateIbanRequest,
+): Promise<ValidateIbanResponse> {
+ const valRes = validateIban(req.iban);
+ const resp: ValidateIbanResponse = {
+ valid: valRes.type === "valid",
+ };
+ return resp;
+}
+
+async function handleAddExchange(
+ wex: WalletExecutionContext,
+ req: AddExchangeRequest,
+): Promise<EmptyObject> {
+ await fetchFreshExchange(wex, req.exchangeBaseUrl, {});
+ return {};
+}
+
+async function handleUpdateExchangeEntry(
+ wex: WalletExecutionContext,
+ req: UpdateExchangeEntryRequest,
+): Promise<EmptyObject> {
+ await fetchFreshExchange(wex, req.exchangeBaseUrl, {
+ forceUpdate: !!req.force,
+ });
+ return {};
+}
+
+async function handleTestingGetDenomStats(
+ wex: WalletExecutionContext,
+ req: TestingGetDenomStatsRequest,
+): Promise<TestingGetDenomStatsResponse> {
+ const denomStats: TestingGetDenomStatsResponse = {
+ numKnown: 0,
+ numLost: 0,
+ numOffered: 0,
+ };
+ await wex.db.runReadOnlyTx({ storeNames: ["denominations"] }, async (tx) => {
+ const denoms = await tx.denominations.indexes.byExchangeBaseUrl.getAll(
+ req.exchangeBaseUrl,
+ );
+ for (const d of denoms) {
+ denomStats.numKnown++;
+ if (d.isOffered) {
+ denomStats.numOffered++;
+ }
+ if (d.isLost) {
+ denomStats.numLost++;
+ }
+ }
+ });
+ return denomStats;
+}
+
+async function handleListExchangesForScopedCurrency(
+ wex: WalletExecutionContext,
+ req: ListExchangesForScopedCurrencyRequest,
+): Promise<ExchangesShortListResponse> {
+ const exchangesResp = await listExchanges(wex);
+ const result: ExchangesShortListResponse = {
+ exchanges: [],
+ };
+ // Right now we only filter on the currency, as wallet-core doesn't
+ // fully support scoped currencies yet.
+ for (const exch of exchangesResp.exchanges) {
+ if (exch.currency === req.scope.currency) {
+ result.exchanges.push({
+ exchangeBaseUrl: exch.exchangeBaseUrl,
+ });
+ }
+ }
+ return result;
+}
+
+async function handleAddKnownBankAccount(
+ wex: WalletExecutionContext,
+ req: AddKnownBankAccountsRequest,
+): Promise<EmptyObject> {
+ await addKnownBankAccounts(wex, req.payto, req.alias, req.currency);
+ return {};
+}
+
+async function handleForgetKnownBankAccounts(
+ wex: WalletExecutionContext,
+ req: ForgetKnownBankAccountsRequest,
+): Promise<EmptyObject> {
+ await forgetKnownBankAccounts(wex, req.payto);
+ return {};
+}
+
+// FIXME: Doesn't have proper type!
+async function handleTestingGetReserveHistory(
+ wex: WalletExecutionContext,
+ req: TestingGetReserveHistoryRequest,
+): Promise<any> {
+ const reserve = await wex.db.runReadOnlyTx(
+ { storeNames: ["reserves"] },
+ async (tx) => {
+ return tx.reserves.indexes.byReservePub.get(req.reservePub);
+ },
+ );
+ if (!reserve) {
+ throw Error("no reserve pub found");
+ }
+ const sigResp = await wex.cryptoApi.signReserveHistoryReq({
+ reservePriv: reserve.reservePriv,
+ startOffset: 0,
+ });
+ const exchangeBaseUrl = req.exchangeBaseUrl;
+ const url = new URL(`reserves/${req.reservePub}/history`, exchangeBaseUrl);
+ const resp = await wex.http.fetch(url.href, {
+ headers: { ["Taler-Reserve-History-Signature"]: sigResp.sig },
+ });
+ const historyJson = await readSuccessResponseJsonOrThrow(resp, codecForAny());
+ return historyJson;
+}
+
+async function handleAcceptManualWithdrawal(
+ wex: WalletExecutionContext,
+ req: AcceptManualWithdrawalRequest,
+): Promise<AcceptManualWithdrawalResult> {
+ const res = await createManualWithdrawal(wex, {
+ amount: Amounts.parseOrThrow(req.amount),
+ exchangeBaseUrl: req.exchangeBaseUrl,
+ restrictAge: req.restrictAge,
+ forceReservePriv: req.forceReservePriv,
+ });
+ return res;
+}
+
+async function handleGetExchangeTos(
+ wex: WalletExecutionContext,
+ req: GetExchangeTosRequest,
+): Promise<GetExchangeTosResult> {
+ return getExchangeTos(
+ wex,
+ req.exchangeBaseUrl,
+ req.acceptedFormat,
+ req.acceptLanguage,
+ );
+}
+
+async function handleGetContractTermsDetails(
+ wex: WalletExecutionContext,
+ req: GetContractTermsDetailsRequest,
+): Promise<WalletContractData> {
+ if (req.proposalId) {
+ // FIXME: deprecated path
+ return getContractTermsDetails(wex, req.proposalId);
+ }
+ if (req.transactionId) {
+ const parsedTx = parseTransactionIdentifier(req.transactionId);
+ if (parsedTx?.tag === TransactionType.Payment) {
+ return getContractTermsDetails(wex, parsedTx.proposalId);
+ }
+ throw Error("transactionId is not a payment transaction");
+ }
+ throw Error("transactionId missing");
+}
+
+async function handleGetQrCodesForPayto(
+ wex: WalletExecutionContext,
+ req: GetQrCodesForPaytoRequest,
+): Promise<GetQrCodesForPaytoResponse> {
+ return {
+ codes: getQrCodesForPayto(req.paytoUri),
+ };
+}
+
+async function handleConfirmPay(
+ wex: WalletExecutionContext,
+ req: ConfirmPayRequest,
+): Promise<ConfirmPayResult> {
+ let transactionId;
+ if (req.proposalId) {
+ // legacy client support
+ transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId: req.proposalId,
+ });
+ } else if (req.transactionId) {
+ transactionId = req.transactionId;
+ } else {
+ throw Error("transactionId or (deprecated) proposalId required");
+ }
+ return await confirmPay(wex, transactionId, req.sessionId);
+}
+
+async function handleAbortTransaction(
+ wex: WalletExecutionContext,
+ req: AbortTransactionRequest,
+): Promise<EmptyObject> {
+ await abortTransaction(wex, req.transactionId);
+ return {};
+}
+
+async function handleSuspendTransaction(
+ wex: WalletExecutionContext,
+ req: SuspendTransactionRequest,
+): Promise<EmptyObject> {
+ await suspendTransaction(wex, req.transactionId);
+ return {};
+}
+
+async function handleGetActiveTasks(
+ wex: WalletExecutionContext,
+ req: EmptyObject,
+): Promise<GetActiveTasksResponse> {
+ const allTasksId = (await getActiveTaskIds(wex.ws)).taskIds;
+
+ const tasksInfo = await Promise.all(
+ allTasksId.map(async (id) => {
+ return await wex.db.runReadOnlyTx(
+ { storeNames: ["operationRetries"] },
+ async (tx) => {
+ return tx.operationRetries.get(id);
+ },
+ );
+ }),
+ );
+
+ const tasks = allTasksId.map((taskId, i): ActiveTask => {
+ const transaction = convertTaskToTransactionId(taskId);
+ const d = tasksInfo[i];
+
+ const firstTry = !d
+ ? undefined
+ : timestampAbsoluteFromDb(d.retryInfo.firstTry);
+ const nextTry = !d
+ ? undefined
+ : timestampAbsoluteFromDb(d.retryInfo.nextRetry);
+ const counter = d?.retryInfo.retryCounter;
+ const lastError = d?.lastError;
+
+ return {
+ taskId: taskId,
+ retryCounter: counter,
+ firstTry,
+ nextTry,
+ lastError,
+ transaction,
+ };
+ });
+ return { tasks };
+}
+
+async function handleFailTransaction(
+ wex: WalletExecutionContext,
+ req: FailTransactionRequest,
+): Promise<EmptyObject> {
+ await failTransaction(wex, req.transactionId);
+ return {};
+}
+
+async function handleTestingGetSampleTransactions(
+ wex: WalletExecutionContext,
+ req: EmptyObject,
+): Promise<TransactionsResponse> {
+ // FIXME!
+ return { transactions: [] };
+ // These are out of date!
+ //return { transactions: sampleWalletCoreTransactions };
+}
+
+async function handleStartRefundQuery(
+ wex: WalletExecutionContext,
+ req: StartRefundQueryRequest,
+): Promise<EmptyObject> {
+ const txIdParsed = parseTransactionIdentifier(req.transactionId);
+ if (!txIdParsed) {
+ throw Error("invalid transaction ID");
+ }
+ if (txIdParsed.tag !== TransactionType.Payment) {
+ throw Error("expected payment transaction ID");
+ }
+ await startQueryRefund(wex, txIdParsed.proposalId);
+ return {};
+}
+
+async function handleAddBackupProvider(
+ wex: WalletExecutionContext,
+ req: RunBackupCycleRequest,
+): Promise<EmptyObject> {
+ await runBackupCycle(wex, req);
+ return {};
+}
+
+async function handleHintNetworkAvailability(
+ wex: WalletExecutionContext,
+ req: HintNetworkAvailabilityRequest,
+): Promise<EmptyObject> {
+ wex.ws.networkAvailable = req.isNetworkAvailable;
+ // When network becomes available, restart tasks as they're blocked
+ // waiting for the network.
+ // When network goes down, restart tasks so they notice the network
+ // is down and wait.
+ await restartAllRunningTasks(wex);
+ return {};
+}
+
+async function handleGetDepositWireTypesForCurrency(
+ wex: WalletExecutionContext,
+ req: GetDepositWireTypesForCurrencyRequest,
+): Promise<GetDepositWireTypesForCurrencyResponse> {
+ const wtSet: Set<string> = new Set();
+ await wex.db.runReadOnlyTx(
+ { storeNames: ["exchanges", "exchangeDetails"] },
+ async (tx) => {
+ const exchanges = await tx.exchanges.getAll();
+ for (const exchange of exchanges) {
+ const det = await getExchangeWireDetailsInTx(tx, exchange.baseUrl);
+ if (!det) {
+ continue;
+ }
+ if (det.currency !== req.currency) {
+ continue;
+ }
+ for (const acc of det.wireInfo.accounts) {
+ let usable = true;
+ for (const dr of acc.debit_restrictions) {
+ if (dr.type === "deny") {
+ usable = false;
+ break;
+ }
+ }
+ if (!usable) {
+ break;
+ }
+ const parsedPayto = parsePaytoUri(acc.payto_uri);
+ if (!parsedPayto) {
+ continue;
+ }
+ wtSet.add(parsedPayto.targetType);
+ }
+ }
+ },
+ );
+ return {
+ wireTypes: [...wtSet],
+ };
+}
+
+async function handleListGlobalCurrencyExchanges(
+ wex: WalletExecutionContext,
+ req: EmptyObject,
+): Promise<ListGlobalCurrencyExchangesResponse> {
+ const resp: ListGlobalCurrencyExchangesResponse = {
+ exchanges: [],
+ };
+ await wex.db.runReadOnlyTx(
+ { storeNames: ["globalCurrencyExchanges"] },
+ async (tx) => {
+ const gceList = await tx.globalCurrencyExchanges.iter().toArray();
+ for (const gce of gceList) {
+ resp.exchanges.push({
+ currency: gce.currency,
+ exchangeBaseUrl: gce.exchangeBaseUrl,
+ exchangeMasterPub: gce.exchangeMasterPub,
+ });
+ }
+ },
+ );
+ return resp;
}
/**
@@ -710,105 +1225,45 @@ async function dispatchRequestInternal(
// definitions we already have?
switch (operation) {
case WalletApiOperation.CreateStoredBackup:
- return createStoredBackup(wex);
+ return await createStoredBackup(wex);
case WalletApiOperation.DeleteStoredBackup: {
const req = codecForDeleteStoredBackupRequest().decode(payload);
- await deleteStoredBackup(wex, req);
- return {};
+ return await handleDeleteStoredBackup(wex, req);
}
case WalletApiOperation.ListStoredBackups:
return listStoredBackups(wex);
case WalletApiOperation.RecoverStoredBackup: {
const req = codecForRecoverStoredBackupRequest().decode(payload);
- await recoverStoredBackup(wex, req);
- return {};
+ return await handleRecoverStoredBackup(wex, req);
}
case WalletApiOperation.SetWalletRunConfig:
case WalletApiOperation.InitWallet: {
const req = codecForInitRequest().decode(payload);
-
- if (logger.shouldLogTrace()) {
- const initType = wex.ws.initCalled
- ? "repeat initialization"
- : "first initialization";
- logger.trace(`init request (${initType}): ${j2s(req)}`);
- }
-
- // Write to the DB to make sure that we're failing early in
- // case the DB is not writeable.
- try {
- await wex.db.runReadWriteTx({ storeNames: ["config"] }, async (tx) => {
- tx.config.put({
- key: ConfigRecordKey.LastInitInfo,
- value: timestampProtocolToDb(TalerProtocolTimestamp.now()),
- });
- });
- } catch (e) {
- logger.error("error writing to database during initialization");
- throw TalerError.fromDetail(TalerErrorCode.WALLET_DB_UNAVAILABLE, {
- innerError: getErrorDetailFromException(e),
- });
- }
- wex.ws.initWithConfig(applyRunConfigDefaults(req.config));
-
- if (wex.ws.config.testing.skipDefaults) {
- logger.trace("skipping defaults");
- } else {
- logger.trace("filling defaults");
- await fillDefaults(wex);
- }
- const resp: InitResponse = {
- versionInfo: getVersion(wex),
- };
-
- if (req.config?.lazyTaskLoop) {
- logger.trace("lazily starting task loop");
- } else {
- await wex.taskScheduler.ensureRunning();
- }
-
- wex.ws.initCalled = true;
- return resp;
+ return await handleSetWalletRunConfig(wex, req);
}
case WalletApiOperation.WithdrawTestkudos: {
- await withdrawTestBalance(wex, {
- amount: "TESTKUDOS:10" as AmountString,
- corebankApiBaseUrl: "https://bank.test.taler.net/",
- exchangeBaseUrl: "https://exchange.test.taler.net/",
- });
- return {
- versionInfo: getVersion(wex),
- };
+ return await handleWithdrawTestkudos(wex);
}
case WalletApiOperation.WithdrawTestBalance: {
const req = codecForWithdrawTestBalance().decode(payload);
- await withdrawTestBalance(wex, req);
- return {};
+ return await handleWithdrawTestBalance(wex, req);
}
case WalletApiOperation.TestingListTaskForTransaction: {
const req =
codecForTestingListTasksForTransactionRequest().decode(payload);
- return {
- taskIdList: listTaskForTransactionId(req.transactionId),
- } satisfies TestingListTasksForTransactionsResponse;
+ return await handleTestingListTasksForTransaction(wex, req);
}
case WalletApiOperation.RunIntegrationTest: {
const req = codecForIntegrationTestArgs().decode(payload);
- await runIntegrationTest(wex, req);
- return {};
+ return await handleRunIntegrationTest(wex, req);
}
case WalletApiOperation.RunIntegrationTestV2: {
const req = codecForIntegrationTestV2Args().decode(payload);
- await runIntegrationTest2(wex, req);
- return {};
+ return await handleRunIntegrationTestV2(wex, req);
}
case WalletApiOperation.ValidateIban: {
const req = codecForValidateIbanRequest().decode(payload);
- const valRes = validateIban(req.iban);
- const resp: ValidateIbanResponse = {
- valid: valRes.type === "valid",
- };
- return resp;
+ return handleValidateIban(wex, req);
}
case WalletApiOperation.TestPay: {
const req = codecForTestPayArgs().decode(payload);
@@ -828,45 +1283,18 @@ async function dispatchRequestInternal(
}
case WalletApiOperation.AddExchange: {
const req = codecForAddExchangeRequest().decode(payload);
- await fetchFreshExchange(wex, req.exchangeBaseUrl, {});
- return {};
+ return await handleAddExchange(wex, req);
}
case WalletApiOperation.TestingPing: {
return {};
}
case WalletApiOperation.UpdateExchangeEntry: {
const req = codecForUpdateExchangeEntryRequest().decode(payload);
- await fetchFreshExchange(wex, req.exchangeBaseUrl, {
- forceUpdate: !!req.force,
- });
- return {};
+ return await handleUpdateExchangeEntry(wex, req);
}
case WalletApiOperation.TestingGetDenomStats: {
const req = codecForTestingGetDenomStatsRequest().decode(payload);
- const denomStats: TestingGetDenomStatsResponse = {
- numKnown: 0,
- numLost: 0,
- numOffered: 0,
- };
- await wex.db.runReadOnlyTx(
- { storeNames: ["denominations"] },
- async (tx) => {
- const denoms =
- await tx.denominations.indexes.byExchangeBaseUrl.getAll(
- req.exchangeBaseUrl,
- );
- for (const d of denoms) {
- denomStats.numKnown++;
- if (d.isOffered) {
- denomStats.numOffered++;
- }
- if (d.isLost) {
- denomStats.numLost++;
- }
- }
- },
- );
- return denomStats;
+ return handleTestingGetDenomStats(wex, req);
}
case WalletApiOperation.ListExchanges: {
return await listExchanges(wex);
@@ -878,20 +1306,7 @@ async function dispatchRequestInternal(
case WalletApiOperation.ListExchangesForScopedCurrency: {
const req =
codecForListExchangesForScopedCurrencyRequest().decode(payload);
- const exchangesResp = await listExchanges(wex);
- const result: ExchangesShortListResponse = {
- exchanges: [],
- };
- // Right now we only filter on the currency, as wallet-core doesn't
- // fully support scoped currencies yet.
- for (const exch of exchangesResp.exchanges) {
- if (exch.currency === req.scope.currency) {
- result.exchanges.push({
- exchangeBaseUrl: exch.exchangeBaseUrl,
- });
- }
- }
- return result;
+ return await handleListExchangesForScopedCurrency(wex, req);
}
case WalletApiOperation.GetExchangeDetailedInfo: {
const req = codecForAddExchangeRequest().decode(payload);
@@ -903,13 +1318,11 @@ async function dispatchRequestInternal(
}
case WalletApiOperation.AddKnownBankAccounts: {
const req = codecForAddKnownBankAccounts().decode(payload);
- await addKnownBankAccounts(wex, req.payto, req.alias, req.currency);
- return {};
+ return await handleAddKnownBankAccount(wex, req);
}
case WalletApiOperation.ForgetKnownBankAccounts: {
const req = codecForForgetKnownBankAccounts().decode(payload);
- await forgetKnownBankAccounts(wex, req.payto);
- return {};
+ return await handleForgetKnownBankAccounts(wex, req);
}
case WalletApiOperation.GetWithdrawalDetailsForUri: {
const req = codecForGetWithdrawalDetailsForUri().decode(payload);
@@ -917,48 +1330,16 @@ async function dispatchRequestInternal(
}
case WalletApiOperation.TestingGetReserveHistory: {
const req = codecForTestingGetReserveHistoryRequest().decode(payload);
- const reserve = await wex.db.runReadOnlyTx(
- { storeNames: ["reserves"] },
- async (tx) => {
- return tx.reserves.indexes.byReservePub.get(req.reservePub);
- },
- );
- if (!reserve) {
- throw Error("no reserve pub found");
- }
- const sigResp = await wex.cryptoApi.signReserveHistoryReq({
- reservePriv: reserve.reservePriv,
- startOffset: 0,
- });
- const exchangeBaseUrl = req.exchangeBaseUrl;
- const url = new URL(
- `reserves/${req.reservePub}/history`,
- exchangeBaseUrl,
- );
- const resp = await wex.http.fetch(url.href, {
- headers: { ["Taler-Reserve-History-Signature"]: sigResp.sig },
- });
- const historyJson = await readSuccessResponseJsonOrThrow(
- resp,
- codecForAny(),
- );
- return historyJson;
+ return await handleTestingGetReserveHistory(wex, req);
}
case WalletApiOperation.AcceptManualWithdrawal: {
const req = codecForAcceptManualWithdrawalRequest().decode(payload);
- const res = await createManualWithdrawal(wex, {
- amount: Amounts.parseOrThrow(req.amount),
- exchangeBaseUrl: req.exchangeBaseUrl,
- restrictAge: req.restrictAge,
- forceReservePriv: req.forceReservePriv,
- });
- return res;
+ return await handleAcceptManualWithdrawal(wex, req);
}
case WalletApiOperation.GetWithdrawalDetailsForAmount: {
const req =
codecForGetWithdrawalDetailsForAmountRequest().decode(payload);
- const resp = await getWithdrawalDetailsForAmount(wex, cts, req);
- return resp;
+ return await getWithdrawalDetailsForAmount(wex, cts, req);
}
case WalletApiOperation.GetBalances: {
return await getBalances(wex);
@@ -979,12 +1360,6 @@ async function dispatchRequestInternal(
const req = codecForUserAttentionsRequest().decode(payload);
return await getUserAttentionsUnreadCount(wex, req);
}
- case WalletApiOperation.GetPendingOperations: {
- // FIXME: Eventually remove the handler after deprecation period.
- return {
- pendingOperations: [],
- } satisfies PendingOperationsResponse;
- }
case WalletApiOperation.SetExchangeTosAccepted: {
const req = codecForAcceptExchangeTosRequest().decode(payload);
await acceptExchangeTermsOfService(wex, req.exchangeBaseUrl);
@@ -1017,39 +1392,22 @@ async function dispatchRequestInternal(
}
case WalletApiOperation.GetExchangeTos: {
const req = codecForGetExchangeTosRequest().decode(payload);
- return getExchangeTos(
- wex,
- req.exchangeBaseUrl,
- req.acceptedFormat,
- req.acceptLanguage,
- );
+ return await handleGetExchangeTos(wex, req);
}
case WalletApiOperation.GetContractTermsDetails: {
const req = codecForGetContractTermsDetails().decode(payload);
- if (req.proposalId) {
- // FIXME: deprecated path
- return getContractTermsDetails(wex, req.proposalId);
- }
- if (req.transactionId) {
- const parsedTx = parseTransactionIdentifier(req.transactionId);
- if (parsedTx?.tag === TransactionType.Payment) {
- return getContractTermsDetails(wex, parsedTx.proposalId);
- }
- throw Error("transactionId is not a payment transaction");
- }
- throw Error("transactionId missing");
+ return handleGetContractTermsDetails(wex, req);
}
case WalletApiOperation.RetryPendingNow: {
- logger.error("retryPendingNow currently not implemented");
- return {};
+ return handleRetryPendingNow(wex);
}
case WalletApiOperation.SharePayment: {
const req = codecForSharePaymentRequest().decode(payload);
- return await sharePayment(wex, req.merchantBaseUrl, req.orderId);
+ return await handleSharePayment(wex, req);
}
case WalletApiOperation.PrepareWithdrawExchange: {
const req = codecForPrepareWithdrawExchangeRequest().decode(payload);
- return handlePrepareWithdrawExchange(wex, req);
+ return await handlePrepareWithdrawExchange(wex, req);
}
case WalletApiOperation.CheckPayForTemplate: {
const req = codecForCheckPayTemplateRequest().decode(payload);
@@ -1065,78 +1423,26 @@ async function dispatchRequestInternal(
}
case WalletApiOperation.GetQrCodesForPayto: {
const req = codecForGetQrCodesForPaytoRequest().decode(payload);
- return {
- codes: getQrCodesForPayto(req.paytoUri),
- };
+ return handleGetQrCodesForPayto(wex, req);
}
case WalletApiOperation.ConfirmPay: {
const req = codecForConfirmPayRequest().decode(payload);
- let transactionId;
- if (req.proposalId) {
- // legacy client support
- transactionId = constructTransactionIdentifier({
- tag: TransactionType.Payment,
- proposalId: req.proposalId,
- });
- } else if (req.transactionId) {
- transactionId = req.transactionId;
- } else {
- throw Error("transactionId or (deprecated) proposalId required");
- }
- return await confirmPay(wex, transactionId, req.sessionId);
+ return handleConfirmPay(wex, req);
}
case WalletApiOperation.AbortTransaction: {
const req = codecForAbortTransaction().decode(payload);
- await abortTransaction(wex, req.transactionId);
- return {};
+ return handleAbortTransaction(wex, req);
}
case WalletApiOperation.SuspendTransaction: {
const req = codecForSuspendTransaction().decode(payload);
- await suspendTransaction(wex, req.transactionId);
- return {};
+ return handleSuspendTransaction(wex, req);
}
case WalletApiOperation.GetActiveTasks: {
- const allTasksId = (await getActiveTaskIds(wex.ws)).taskIds;
-
- const tasksInfo = await Promise.all(
- allTasksId.map(async (id) => {
- return await wex.db.runReadOnlyTx(
- { storeNames: ["operationRetries"] },
- async (tx) => {
- return tx.operationRetries.get(id);
- },
- );
- }),
- );
-
- const tasks = allTasksId.map((taskId, i): ActiveTask => {
- const transaction = convertTaskToTransactionId(taskId);
- const d = tasksInfo[i];
-
- const firstTry = !d
- ? undefined
- : timestampAbsoluteFromDb(d.retryInfo.firstTry);
- const nextTry = !d
- ? undefined
- : timestampAbsoluteFromDb(d.retryInfo.nextRetry);
- const counter = d?.retryInfo.retryCounter;
- const lastError = d?.lastError;
-
- return {
- taskId: taskId,
- retryCounter: counter,
- firstTry,
- nextTry,
- lastError,
- transaction,
- };
- });
- return { tasks };
+ return await handleGetActiveTasks(wex, {});
}
case WalletApiOperation.FailTransaction: {
const req = codecForFailTransactionRequest().decode(payload);
- await failTransaction(wex, req.transactionId);
- return {};
+ return await handleFailTransaction(wex, req);
}
case WalletApiOperation.ResumeTransaction: {
const req = codecForResumeTransaction().decode(payload);
@@ -1152,7 +1458,8 @@ async function dispatchRequestInternal(
return {};
}
case WalletApiOperation.TestingGetSampleTransactions:
- return { transactions: sampleWalletCoreTransactions };
+ const req = codecForEmptyObject().decode(payload);
+ return handleTestingGetSampleTransactions(wex, req);
case WalletApiOperation.ForceRefresh: {
const req = codecForForceRefreshRequest().decode(payload);
return await forceRefresh(wex, req);
@@ -1163,15 +1470,7 @@ async function dispatchRequestInternal(
}
case WalletApiOperation.StartRefundQuery: {
const req = codecForStartRefundQueryRequest().decode(payload);
- const txIdParsed = parseTransactionIdentifier(req.transactionId);
- if (!txIdParsed) {
- throw Error("invalid transaction ID");
- }
- if (txIdParsed.tag !== TransactionType.Payment) {
- throw Error("expected payment transaction ID");
- }
- await startQueryRefund(wex, txIdParsed.proposalId);
- return {};
+ return handleStartRefundQuery(wex, req);
}
case WalletApiOperation.AddBackupProvider: {
const req = codecForAddBackupProviderRequest().decode(payload);
@@ -1179,8 +1478,7 @@ async function dispatchRequestInternal(
}
case WalletApiOperation.RunBackupCycle: {
const req = codecForRunBackupCycle().decode(payload);
- await runBackupCycle(wex, req);
- return {};
+ return handleAddBackupProvider(wex, req);
}
case WalletApiOperation.RemoveBackupProvider: {
const req = codecForRemoveBackupProvider().decode(payload);
@@ -1197,48 +1495,8 @@ async function dispatchRequestInternal(
return {};
}
case WalletApiOperation.GetCurrencySpecification: {
- // Ignore result, just validate in this mock implementation
const req = codecForGetCurrencyInfoRequest().decode(payload);
- // Hard-coded mock for KUDOS and TESTKUDOS
- if (req.scope.currency === "KUDOS") {
- const kudosResp: GetCurrencySpecificationResponse = {
- currencySpecification: {
- name: "Kudos (Taler Demonstrator)",
- num_fractional_input_digits: 2,
- num_fractional_normal_digits: 2,
- num_fractional_trailing_zero_digits: 2,
- alt_unit_names: {
- "0": "ク",
- },
- },
- };
- return kudosResp;
- } else if (req.scope.currency === "TESTKUDOS") {
- const testkudosResp: GetCurrencySpecificationResponse = {
- currencySpecification: {
- name: "Test (Taler Unstable Demonstrator)",
- num_fractional_input_digits: 0,
- num_fractional_normal_digits: 0,
- num_fractional_trailing_zero_digits: 0,
- alt_unit_names: {
- "0": "テ",
- },
- },
- };
- return testkudosResp;
- }
- const defaultResp: GetCurrencySpecificationResponse = {
- currencySpecification: {
- name: req.scope.currency,
- num_fractional_input_digits: 2,
- num_fractional_normal_digits: 2,
- num_fractional_trailing_zero_digits: 2,
- alt_unit_names: {
- "0": req.scope.currency,
- },
- },
- };
- return defaultResp;
+ return handleGetCurrencySpecification(wex, req);
}
case WalletApiOperation.ImportBackupRecovery: {
const req = codecForAny().decode(payload);
@@ -1247,13 +1505,7 @@ async function dispatchRequestInternal(
}
case WalletApiOperation.HintNetworkAvailability: {
const req = codecForHintNetworkAvailabilityRequest().decode(payload);
- wex.ws.networkAvailable = req.isNetworkAvailable;
- // When network becomes available, restart tasks as they're blocked
- // waiting for the network.
- // When network goes down, restart tasks so they notice the network
- // is down and wait.
- await restartAllRunningTasks(wex);
- return {};
+ return await handleHintNetworkAvailability(wex, req);
}
case WalletApiOperation.ConvertDepositAmount: {
const req = codecForConvertAmountRequest.decode(payload);
@@ -1325,61 +1577,11 @@ async function dispatchRequestInternal(
case WalletApiOperation.GetDepositWireTypesForCurrency: {
const req =
codecForGetDepositWireTypesForCurrencyRequest().decode(payload);
- const wtSet: Set<string> = new Set();
- await wex.db.runReadOnlyTx(
- { storeNames: ["exchanges", "exchangeDetails"] },
- async (tx) => {
- const exchanges = await tx.exchanges.getAll();
- for (const exchange of exchanges) {
- const det = await getExchangeWireDetailsInTx(tx, exchange.baseUrl);
- if (!det) {
- continue;
- }
- if (det.currency !== req.currency) {
- continue;
- }
- for (const acc of det.wireInfo.accounts) {
- let usable = true;
- for (const dr of acc.debit_restrictions) {
- if (dr.type === "deny") {
- usable = false;
- break;
- }
- }
- if (!usable) {
- break;
- }
- const parsedPayto = parsePaytoUri(acc.payto_uri);
- if (!parsedPayto) {
- continue;
- }
- wtSet.add(parsedPayto.targetType);
- }
- }
- },
- );
- return {
- wireTypes: [...wtSet],
- };
+ return handleGetDepositWireTypesForCurrency(wex, req);
}
case WalletApiOperation.ListGlobalCurrencyExchanges: {
- const resp: ListGlobalCurrencyExchangesResponse = {
- exchanges: [],
- };
- await wex.db.runReadOnlyTx(
- { storeNames: ["globalCurrencyExchanges"] },
- async (tx) => {
- const gceList = await tx.globalCurrencyExchanges.iter().toArray();
- for (const gce of gceList) {
- resp.exchanges.push({
- currency: gce.currency,
- exchangeBaseUrl: gce.exchangeBaseUrl,
- exchangeMasterPub: gce.exchangeMasterPub,
- });
- }
- },
- );
- return resp;
+ const req = codecForEmptyObject().decode(payload);
+ return await handleListGlobalCurrencyExchanges(wex, req);
}
case WalletApiOperation.ListGlobalCurrencyAuditors: {
const resp: ListGlobalCurrencyAuditorsResponse = {
@@ -1554,7 +1756,7 @@ async function dispatchRequestInternal(
return {};
}
case WalletApiOperation.GetVersion: {
- return getVersion(wex);
+ return handleGetVersion(wex);
}
case WalletApiOperation.TestingWaitTransactionsFinal:
return await waitUntilAllTransactionsFinal(wex);
@@ -1626,7 +1828,66 @@ async function dispatchRequestInternal(
);
}
-export function getVersion(wex: WalletExecutionContext): WalletCoreVersion {
+export async function handleGetCurrencySpecification(
+ wex: WalletExecutionContext,
+ req: GetCurrencySpecificationRequest,
+): Promise<GetCurrencySpecificationResponse> {
+ const spec = await wex.db.runReadOnlyTx(
+ {
+ storeNames: ["currencyInfo"],
+ },
+ async (tx) => {
+ return WalletDbHelpers.getCurrencyInfo(tx, req.scope);
+ },
+ );
+ if (spec) {
+ return {
+ currencySpecification: spec.currencySpec,
+ };
+ }
+ // Hard-coded mock for KUDOS and TESTKUDOS
+ if (req.scope.currency === "KUDOS") {
+ const kudosResp: GetCurrencySpecificationResponse = {
+ currencySpecification: {
+ name: "Kudos (Taler Demonstrator)",
+ num_fractional_input_digits: 2,
+ num_fractional_normal_digits: 2,
+ num_fractional_trailing_zero_digits: 2,
+ alt_unit_names: {
+ "0": "ク",
+ },
+ },
+ };
+ return kudosResp;
+ } else if (req.scope.currency === "TESTKUDOS") {
+ const testkudosResp: GetCurrencySpecificationResponse = {
+ currencySpecification: {
+ name: "Test (Taler Unstable Demonstrator)",
+ num_fractional_input_digits: 0,
+ num_fractional_normal_digits: 0,
+ num_fractional_trailing_zero_digits: 0,
+ alt_unit_names: {
+ "0": "テ",
+ },
+ },
+ };
+ return testkudosResp;
+ }
+ const defaultResp: GetCurrencySpecificationResponse = {
+ currencySpecification: {
+ name: req.scope.currency,
+ num_fractional_input_digits: 2,
+ num_fractional_normal_digits: 2,
+ num_fractional_trailing_zero_digits: 2,
+ alt_unit_names: {
+ "0": req.scope.currency,
+ },
+ },
+ };
+ return defaultResp;
+}
+
+function handleGetVersion(wex: WalletExecutionContext): WalletCoreVersion {
const result: WalletCoreVersion = {
implementationSemver: walletCoreBuildInfo.implementationSemver,
implementationGitHash: walletCoreBuildInfo.implementationGitHash,
diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts
index 087db7938..083fa2a2d 100644
--- a/packages/taler-wallet-core/src/withdraw.ts
+++ b/packages/taler-wallet-core/src/withdraw.ts
@@ -64,6 +64,7 @@ import {
TalerErrorCode,
TalerErrorDetail,
TalerPreciseTimestamp,
+ TalerUriAction,
Transaction,
TransactionAction,
TransactionIdStr,
@@ -95,6 +96,7 @@ import {
getRandomBytes,
j2s,
makeErrorDetail,
+ parseTalerUri,
parseWithdrawUri,
} from "@gnu-taler/taler-util";
import {
@@ -3064,6 +3066,17 @@ export async function prepareBankIntegratedWithdrawal(
},
);
+ const parsedUri = parseTalerUri(req.talerWithdrawUri);
+ if (parsedUri?.type !== TalerUriAction.Withdraw) {
+ throw TalerError.fromDetail(TalerErrorCode.WALLET_TALER_URI_MALFORMED, {});
+ }
+
+ const externalConfirmation = parsedUri.externalConfirmation;
+
+ logger.info(
+ `creating withdrawal with externalConfirmation=${externalConfirmation}`,
+ );
+
const withdrawInfo = await getBankWithdrawalInfo(
wex.http,
req.talerWithdrawUri,
@@ -3099,6 +3112,7 @@ export async function prepareBankIntegratedWithdrawal(
timestampReserveInfoPosted: undefined,
wireTypes: withdrawInfo.wireTypes,
currency: withdrawInfo.currency,
+ externalConfirmation,
},
},
reserveStatus: WithdrawalGroupStatus.DialogProposed,
@@ -3220,22 +3234,14 @@ export async function confirmWithdrawal(
rec.denomsSel = initalDenoms;
rec.rawWithdrawalAmount = initalDenoms.totalWithdrawCost;
rec.effectiveWithdrawalAmount = initalDenoms.totalCoinValue;
-
- rec.wgInfo = {
- withdrawalType: WithdrawalRecordType.BankIntegrated,
- exchangeCreditAccounts: withdrawalAccountList,
- bankInfo: {
- exchangePaytoUri,
- talerWithdrawUri,
- confirmUrl: confirmUrl,
- timestampBankConfirmed: undefined,
- timestampReserveInfoPosted: undefined,
- wireTypes: bankWireTypes,
- currency: bankCurrency,
- },
- };
- pending = true;
+ checkDbInvariant(
+ rec.wgInfo.withdrawalType === WithdrawalRecordType.BankIntegrated,
+ "withdrawal type mismatch",
+ );
+ rec.wgInfo.exchangeCreditAccounts = withdrawalAccountList;
+ rec.wgInfo.bankInfo.exchangePaytoUri = exchangePaytoUri;
rec.status = WithdrawalGroupStatus.PendingRegisteringBank;
+ pending = true;
return TransitionResult.transition(rec);
}
default: {
diff --git a/packages/taler-wallet-embedded/package.json b/packages/taler-wallet-embedded/package.json
index df91d5829..dbdb23673 100644
--- a/packages/taler-wallet-embedded/package.json
+++ b/packages/taler-wallet-embedded/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-wallet-embedded",
- "version": "0.12.1",
+ "version": "0.12.2",
"description": "",
"engines": {
"node": ">=0.18.0"
diff --git a/packages/taler-wallet-webextension/manifest-common.json b/packages/taler-wallet-webextension/manifest-common.json
index f80786e7c..01248e964 100644
--- a/packages/taler-wallet-webextension/manifest-common.json
+++ b/packages/taler-wallet-webextension/manifest-common.json
@@ -2,7 +2,7 @@
"name": "GNU Taler Wallet (git)",
"description": "Privacy preserving and transparent payments",
"author": "GNU Taler Developers",
- "version": "0.12.1",
+ "version": "0.12.2",
"icons": {
"16": "static/img/taler-logo-16.png",
"19": "static/img/taler-logo-19.png",
@@ -14,5 +14,5 @@
"256": "static/img/taler-logo-256.png",
"512": "static/img/taler-logo-512.png"
},
- "version_name": "0.12.1"
+ "version_name": "0.12.2"
}
diff --git a/packages/taler-wallet-webextension/package.json b/packages/taler-wallet-webextension/package.json
index 9acc0d0d4..5c622da70 100644
--- a/packages/taler-wallet-webextension/package.json
+++ b/packages/taler-wallet-webextension/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-wallet-webextension",
- "version": "0.12.1",
+ "version": "0.12.2",
"description": "GNU Taler Wallet browser extension",
"main": "./build/index.js",
"types": "./build/index.d.ts",
diff --git a/packages/web-util/package.json b/packages/web-util/package.json
index ae7d98819..fe8a4a3f7 100644
--- a/packages/web-util/package.json
+++ b/packages/web-util/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/web-util",
- "version": "0.12.1",
+ "version": "0.12.2",
"description": "Generic helper functionality for GNU Taler Web Apps",
"type": "module",
"types": "./lib/index.node.d.ts",