diff options
260 files changed, 9203 insertions, 6099 deletions
diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 00c61e8a4..4c931ad04 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,18 +1,17 @@ { - // See https://go.microsoft.com/fwlink/?LinkId=733558 - // for the documentation about the tasks.json format - "version": "2.0.0", - "tasks": [ - { - "type": "typescript", - "tsconfig": "tsconfig.build.json", - "problemMatcher": [ - "$tsc" - ], - "group": { - "kind": "build", - "isDefault": true, - }, - } - ] -}
\ No newline at end of file + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "type": "typescript", + "tsconfig": "tsconfig.build.json", + "problemMatcher": ["$tsc"], + "group": { + "kind": "build", + "isDefault": true + }, + "label": "tsc: build - tsconfig.build.json" + } + ] +} diff --git a/build-system/taler-build-scripts b/build-system/taler-build-scripts -Subproject 23538677f6c6be2a62f38dc6137ecdd1c76b7b1 +Subproject 001f5dd081fc8729ff8def90c4a1c3f93eb8689 diff --git a/ci/Containerfile b/ci/Containerfile index 4e3369830..39bd740a9 100644 --- a/ci/Containerfile +++ b/ci/Containerfile @@ -6,6 +6,7 @@ RUN apt-get update -yq && \ apt-get install -yqq \ git \ python3 \ + codespell \ python3-distutils \ make \ zip \ diff --git a/ci/jobs/0-codespell/dictionary.txt b/ci/jobs/0-codespell/dictionary.txt index b4d643349..95f860504 100644 --- a/ci/jobs/0-codespell/dictionary.txt +++ b/ci/jobs/0-codespell/dictionary.txt @@ -42,3 +42,4 @@ ths updateing wan wih +vie diff --git a/ci/jobs/0-codespell/job.sh b/ci/jobs/0-codespell/job.sh index 58bd07b5a..cb41388c3 100755 --- a/ci/jobs/0-codespell/job.sh +++ b/ci/jobs/0-codespell/job.sh @@ -3,4 +3,4 @@ set -exuo pipefail job_dir=$(dirname "${BASH_SOURCE[0]}") -codespell -I "${job_dir}"/dictionary.txt -S "*.bib,*.bst,*.cls,*.json,*.png,*.svg,*.wav,*.gz,*/templating/test?/**,**/auditor/*.sql,**/templating/mustach**,*.fees,*key,*.tag,*.info,*.latexmkrc,*.ecc,*.jpg,*.zkey,*.sqlite,*/contrib/hellos/**,*/vpn/tests/**,*.priv,*.file,*.tgz,*.woff,*.gif,*.odt,*.fee,*.deflate,*.dat,*.jpeg,*.eps,*.odg,*/m4/ax_lib_postgresql.m4,*/m4/libgcrypt.m4,*.rpath,config.status,ABOUT-NLS,*/doc/texinfo.tex,*.PNG,*.??.json,*.docx,*.ods,*.doc,*.docx,*.xcf,*.xlsx,*.ecc,*.ttf,*.woff2,*.eot,*.ttf,*.eot,*.mp4,*.pptx,*.epgz,*.min.js,**/*.map,**/fonts/**,*.pack.js,*.po,*.bbl,*/afl-tests/*,*/.git/**,*.pdf,*.epub,**/signing-key.asc,**/pnpm-lock.yaml,**/*.svg,**/*.cls,**/rfc.bib,**/*.bst,*/cbdc-es.tex,*/cbdc-it.tex,**/ExchangeSelection/example.ts,*/testcurl/test_tricky.c,*/i18n/strings.ts,*/src/anastasis-data.ts,**/doc/flows/main.de.tex" +codespell -I "${job_dir}"/dictionary.txt -S "*.bib,*.bst,*.cls,*.json,*.png,*.svg,*.wav,*.gz,*/templating/test?/**,**/auditor/*.sql,**/templating/mustach**,*.fees,*key,*.tag,*.info,*.latexmkrc,*.ecc,*.jpg,*.zkey,*.sqlite,*/contrib/hellos/**,*/vpn/tests/**,*.priv,*.file,*.tgz,*.woff,*.gif,*.odt,*.fee,*.deflate,*.dat,*.jpeg,*.eps,*.odg,*/m4/ax_lib_postgresql.m4,*/m4/libgcrypt.m4,*.rpath,config.status,ABOUT-NLS,*/doc/texinfo.tex,*.PNG,*.??.json,*.docx,*.ods,*.doc,*.docx,*.xcf,*.xlsx,*.ecc,*.ttf,*.woff2,*.eot,*.ttf,*.eot,*.mp4,*.pptx,*.epgz,*.min.js,**/*.map,**/fonts/**,*.pack.js,*.po,*.bbl,*/afl-tests/*,*/.git/**,*.pdf,*.epub,**/signing-key.asc,**/pnpm-lock.yaml,**/*.svg,**/*.cls,**/rfc.bib,**/*.bst,*/cbdc-es.tex,*/cbdc-it.tex,**/ExchangeSelection/example.ts,*/testcurl/test_tricky.c,*/i18n/strings.ts,*/src/anastasis-data.ts,**/doc/flows/main.de.tex,*/vendor/**" diff --git a/package.json b/package.json index 9ce6d628e..e7b4b52d7 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "eslint": "^8.29.0", "eslint-config-prettier": "^8.5.0", "nx": "15.0.1", - "prettier": "^2.8.8" + "prettier": "^2.8.8", + "typescript": "^5.2.2" } } diff --git a/packages/aml-backoffice-ui/package.json b/packages/aml-backoffice-ui/package.json index 3fde72b74..a761374df 100644 --- a/packages/aml-backoffice-ui/package.json +++ b/packages/aml-backoffice-ui/package.json @@ -59,7 +59,7 @@ "postcss": "^8.4.23", "postcss-cli": "^10.1.0", "tailwindcss": "^3.3.2", - "typescript": "5.1.3" + "typescript": "5.2.2" }, "pogen": { "domain": "aml-backoffice" diff --git a/packages/aml-backoffice-ui/tsconfig.json b/packages/aml-backoffice-ui/tsconfig.json index cc5bdf396..9826fac07 100644 --- a/packages/aml-backoffice-ui/tsconfig.json +++ b/packages/aml-backoffice-ui/tsconfig.json @@ -1,12 +1,9 @@ { "compilerOptions": { /* Basic Options */ - "target": "ES5", - "module": "ES6", - "lib": [ - "DOM", - "ES2017" - ], + "target": "ES2020", + "module": "Node16", + "lib": ["DOM", "ES2020"], "allowJs": true /* Allow javascript files to be compiled. */, // "checkJs": true, /* Report errors in .js files. */ "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, @@ -45,7 +42,5 @@ /* Advanced Options */ "skipLibCheck": true /* Skip type checking of declaration files. */ }, - "include": [ - "src/**/*" - ] -}
\ No newline at end of file + "include": ["src/**/*"] +} diff --git a/packages/anastasis-cli/package.json b/packages/anastasis-cli/package.json index 2bbf32c3b..55af7cc06 100644 --- a/packages/anastasis-cli/package.json +++ b/packages/anastasis-cli/package.json @@ -33,12 +33,12 @@ "@types/node": "^18.11.17", "prettier": "^2.8.8", "rimraf": "^3.0.2", - "typedoc": "^0.24.8", - "typescript": "^5.1.3" + "typedoc": "^0.25.1", + "typescript": "^5.2.2" }, "dependencies": { - "@gnu-taler/taler-util": "workspace:*", "@gnu-taler/anastasis-core": "workspace:*", + "@gnu-taler/taler-util": "workspace:*", "tslib": "^2.5.3" } } diff --git a/packages/anastasis-cli/tsconfig.json b/packages/anastasis-cli/tsconfig.json index 83da34277..7675edfbc 100644 --- a/packages/anastasis-cli/tsconfig.json +++ b/packages/anastasis-cli/tsconfig.json @@ -2,11 +2,11 @@ "compileOnSave": true, "compilerOptions": { "composite": true, - "target": "ES2018", - "module": "ESNext", + "target": "ES2020", + "module": "Node16", "moduleResolution": "Node16", "sourceMap": true, - "lib": ["es6"], + "lib": ["ES2020"], "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "strict": true, diff --git a/packages/anastasis-core/package.json b/packages/anastasis-core/package.json index c3a7106fb..aa182e047 100644 --- a/packages/anastasis-core/package.json +++ b/packages/anastasis-core/package.json @@ -18,7 +18,7 @@ "devDependencies": { "ava": "^4.3.3", "rimraf": "^3.0.2", - "typescript": "^5.1.3" + "typescript": "^5.2.2" }, "dependencies": { "@gnu-taler/taler-util": "workspace:*", diff --git a/packages/anastasis-core/src/index.ts b/packages/anastasis-core/src/index.ts index 88f3bdbe2..89cf186dd 100644 --- a/packages/anastasis-core/src/index.ts +++ b/packages/anastasis-core/src/index.ts @@ -138,7 +138,6 @@ export * as validators from "./validators.js"; export * from "./challenge-feedback-types.js"; const httpLib = createPlatformHttpLib({ - allowHttp: true, enableThrottling: false, }); diff --git a/packages/anastasis-core/tsconfig.json b/packages/anastasis-core/tsconfig.json index 44a159836..a12f2e641 100644 --- a/packages/anastasis-core/tsconfig.json +++ b/packages/anastasis-core/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "composite": true, "target": "ES2020", - "module": "ESNext", + "module": "Node16", "moduleResolution": "Node16", "sourceMap": true, "lib": ["ES2020"], diff --git a/packages/anastasis-webui/package.json b/packages/anastasis-webui/package.json index eff5ba04d..efee9633e 100644 --- a/packages/anastasis-webui/package.json +++ b/packages/anastasis-webui/package.json @@ -44,6 +44,6 @@ "chai": "^4.3.6", "mocha": "^9.2.0", "sass": "1.56.1", - "typescript": "^5.1.3" + "typescript": "^5.2.2" } } diff --git a/packages/anastasis-webui/tsconfig.json b/packages/anastasis-webui/tsconfig.json index 642c34f5a..9e52f2b7e 100644 --- a/packages/anastasis-webui/tsconfig.json +++ b/packages/anastasis-webui/tsconfig.json @@ -1,11 +1,11 @@ { "compilerOptions": { /* Basic Options */ - "target": "ES5", - "module": "ES6", + "target": "ES2020", + "module": "Node16", "lib": [ "DOM", - "ES2017" + "ES2020" ], "allowJs": true /* Allow javascript files to be compiled. */, // "checkJs": true, /* Report errors in .js files. */ diff --git a/packages/demobank-ui/package.json b/packages/demobank-ui/package.json index a584bbf35..d33fae709 100644 --- a/packages/demobank-ui/package.json +++ b/packages/demobank-ui/package.json @@ -60,7 +60,7 @@ "po2json": "^0.4.5", "preact-render-to-string": "^5.2.6", "sass": "1.56.1", - "typescript": "5.1.3" + "typescript": "5.2.2" }, "pogen": { "domain": "bank" diff --git a/packages/demobank-ui/tsconfig.json b/packages/demobank-ui/tsconfig.json index abb9a9f36..9826fac07 100644 --- a/packages/demobank-ui/tsconfig.json +++ b/packages/demobank-ui/tsconfig.json @@ -1,12 +1,9 @@ { "compilerOptions": { /* Basic Options */ - "target": "ES5", - "module": "ES6", - "lib": [ - "DOM", - "ES2016" - ], + "target": "ES2020", + "module": "Node16", + "lib": ["DOM", "ES2020"], "allowJs": true /* Allow javascript files to be compiled. */, // "checkJs": true, /* Report errors in .js files. */ "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, @@ -45,7 +42,5 @@ /* Advanced Options */ "skipLibCheck": true /* Skip type checking of declaration files. */ }, - "include": [ - "src/**/*" - ] -}
\ No newline at end of file + "include": ["src/**/*"] +} diff --git a/packages/idb-bridge/package.json b/packages/idb-bridge/package.json index 1c9c46b02..ce64314ff 100644 --- a/packages/idb-bridge/package.json +++ b/packages/idb-bridge/package.json @@ -29,7 +29,7 @@ "ava": "^5.3.1", "prettier": "^2.8.8", "rimraf": "^5.0.1", - "typescript": "^5.1.6" + "typescript": "^5.2.2" }, "dependencies": { "tslib": "^2.6.0" diff --git a/packages/idb-bridge/src/SqliteBackend.ts b/packages/idb-bridge/src/SqliteBackend.ts index c40281861..a25ec0045 100644 --- a/packages/idb-bridge/src/SqliteBackend.ts +++ b/packages/idb-bridge/src/SqliteBackend.ts @@ -1882,7 +1882,7 @@ export class SqliteBackend implements Backend { } } - clearObjectStore( + async clearObjectStore( btx: DatabaseTransaction, objectStoreName: string, ): Promise<void> { @@ -1906,7 +1906,21 @@ export class SqliteBackend implements Backend { ); } - throw new Error("Method not implemented."); + this._prep(sqlClearObjectStore).run({ + object_store_id: scopeInfo.objectStoreId, + }); + + for (const index of scopeInfo.indexMap.values()) { + let stmt: Sqlite3Statement; + if (index.unique) { + stmt = this._prep(sqlClearUniqueIndexData); + } else { + stmt = this._prep(sqlClearIndexData); + } + stmt.run({ + index_id: index.indexId, + }); + } } } @@ -1963,6 +1977,15 @@ CREATE TABLE IF NOT EXISTS unique_index_data ); `; +const sqlClearObjectStore = ` +DELETE FROM object_data WHERE object_store_id=$object_store_id`; + +const sqlClearIndexData = ` +DELETE FROM index_data WHERE index_id=$index_id`; + +const sqlClearUniqueIndexData = ` +DELETE FROM unique_index_data WHERE index_id=$index_id`; + const sqlListDatabases = ` SELECT name, version FROM databases; `; diff --git a/packages/idb-bridge/src/backend-interface.ts b/packages/idb-bridge/src/backend-interface.ts index 3255261e2..690f92f54 100644 --- a/packages/idb-bridge/src/backend-interface.ts +++ b/packages/idb-bridge/src/backend-interface.ts @@ -144,7 +144,7 @@ export interface IndexMeta { unique: boolean; } -// FIXME: Instead of refering to an object store by name, +// FIXME: Instead of referring to an object store by name, // maybe refer to it via some internal, numeric ID? // This would simplify renaming. export interface Backend { diff --git a/packages/idb-bridge/src/bridge-idb.ts b/packages/idb-bridge/src/bridge-idb.ts index 8cecba534..f3749c77c 100644 --- a/packages/idb-bridge/src/bridge-idb.ts +++ b/packages/idb-bridge/src/bridge-idb.ts @@ -735,7 +735,9 @@ export class BridgeIDBDatabase extends FakeEventTarget implements IDBDatabase { } if (this._closePending) { - throw new InvalidStateError(); + throw new InvalidStateError( + `tried to start transaction on ${this._name}, but a close is pending`, + ); } if (!Array.isArray(storeNames)) { @@ -930,6 +932,9 @@ export class BridgeIDBFactory { // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-running-a-versionchange-transaction for (const otherConn of this.connections) { + if (otherConn._name != db._name) { + continue; + } if (otherConn._closePending) { continue; } diff --git a/packages/idb-bridge/tsconfig.json b/packages/idb-bridge/tsconfig.json index 19e9c2a74..44a27284b 100644 --- a/packages/idb-bridge/tsconfig.json +++ b/packages/idb-bridge/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { "composite": true, - "lib": ["es6"], - "module": "ES2020", + "lib": ["ES2020"], + "module": "Node16", "moduleResolution": "Node16", "target": "ES2020", "allowJs": true, diff --git a/packages/merchant-backend-ui/package.json b/packages/merchant-backend-ui/package.json index 23e5007e9..03e64308c 100644 --- a/packages/merchant-backend-ui/package.json +++ b/packages/merchant-backend-ui/package.json @@ -65,6 +65,6 @@ "sirv-cli": "^1.0.11", "ts-node": "^10.9.1", "tslib": "2.5.3", - "typescript": "5.1.3" + "typescript": "5.2.2" } } diff --git a/packages/merchant-backoffice-ui/package.json b/packages/merchant-backoffice-ui/package.json index 3a0c22adb..12aeb4b0e 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.0.5", + "version": "0.1.0", "license": "AGPL-3.0-or-later", "type": "module", "scripts": { @@ -41,7 +41,7 @@ "preact": "10.11.3", "preact-router": "3.2.1", "qrcode-generator": "1.4.4", - "swr": "1.3.0", + "swr": "2.2.2", "yup": "^0.32.9" }, "devDependencies": { @@ -75,10 +75,10 @@ "rimraf": "^3.0.2", "sass": "1.56.1", "source-map-support": "^0.5.21", - "typedoc": "^0.24.8", - "typescript": "5.1.3" + "typedoc": "^0.25.1", + "typescript": "5.2.2" }, "pogen": { "domain": "taler-merchant-backoffice" } -} +}
\ No newline at end of file diff --git a/packages/merchant-backoffice-ui/src/Application.tsx b/packages/merchant-backoffice-ui/src/Application.tsx index f6a81ff8d..5e82821ae 100644 --- a/packages/merchant-backoffice-ui/src/Application.tsx +++ b/packages/merchant-backoffice-ui/src/Application.tsx @@ -19,19 +19,20 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { HttpStatusCode, LibtoolVersion } from "@gnu-taler/taler-util"; import { ErrorType, TranslationProvider, useTranslationContext, } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; import { route } from "preact-router"; -import { useMemo, useState } from "preact/hooks"; +import { useMemo } from "preact/hooks"; import { ApplicationReadyRoutes } from "./ApplicationReadyRoutes.js"; import { Loading } from "./components/exception/loading.js"; import { - NotificationCard, - NotYetReadyAppMenu, + NotConnectedAppMenu, + NotificationCard } from "./components/menu/index.js"; import { BackendContextProvider, @@ -41,23 +42,24 @@ import { ConfigContextProvider } from "./context/config.js"; import { useBackendConfig } from "./hooks/backend.js"; import { strings } from "./i18n/strings.js"; import LoginPage from "./paths/login/index.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; -import { Settings } from "./paths/settings/index.js"; export function Application(): VNode { return ( - // <FetchContextProvider> <BackendContextProvider> <TranslationProvider source={strings}> <ApplicationStatusRoutes /> </TranslationProvider> </BackendContextProvider> - // </FetchContextProvider> ); } +/** + * Check connection testing against /config + * + * @returns + */ function ApplicationStatusRoutes(): VNode { - const { updateLoginStatus, triedToLog } = useBackendContext(); + const { url, updateLoginStatus, triedToLog } = useBackendContext(); const result = useBackendConfig(); const { i18n } = useTranslationContext(); @@ -71,19 +73,10 @@ function ApplicationStatusRoutes(): VNode { : { currency: "unknown", version: "unknown" }; const ctx = useMemo(() => ({ currency, version }), [currency, version]); - const [showSettings, setShowSettings] = useState(false) - - if (showSettings) { - return <Fragment> - <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="UI Settings" /> - <Settings /> - </Fragment> - } - if (!triedToLog) { return ( <Fragment> - <NotYetReadyAppMenu title="Welcome!" onShowSettings={() => setShowSettings(true)} /> + <NotConnectedAppMenu title="Welcome!" /> <LoginPage onConfirm={updateLoginInfoAndGoToRoot} /> </Fragment> ); @@ -97,7 +90,7 @@ function ApplicationStatusRoutes(): VNode { ) { return ( <Fragment> - <NotYetReadyAppMenu title="Login" onShowSettings={() => setShowSettings(true)} /> + <NotConnectedAppMenu title="Login" /> <LoginPage onConfirm={updateLoginInfoAndGoToRoot} /> </Fragment> ); @@ -108,7 +101,7 @@ function ApplicationStatusRoutes(): VNode { ) { return ( <Fragment> - <NotYetReadyAppMenu title="Error" onShowSettings={() => setShowSettings(true)} /> + <NotConnectedAppMenu title="Error" /> <NotificationCard notification={{ message: i18n.str`Server not found`, @@ -122,7 +115,7 @@ function ApplicationStatusRoutes(): VNode { } if (result.type === ErrorType.SERVER) { <Fragment> - <NotYetReadyAppMenu title="Error" onShowSettings={() => setShowSettings(true)} /> + <NotConnectedAppMenu title="Error" /> <NotificationCard notification={{ message: i18n.str`Server response with an error code`, @@ -135,7 +128,7 @@ function ApplicationStatusRoutes(): VNode { } if (result.type === ErrorType.UNREADABLE) { <Fragment> - <NotYetReadyAppMenu title="Error" onShowSettings={() => setShowSettings(true)} /> + <NotConnectedAppMenu title="Error" /> <NotificationCard notification={{ message: i18n.str`Response from server is unreadable, http status: ${result.status}`, @@ -148,7 +141,7 @@ function ApplicationStatusRoutes(): VNode { } return ( <Fragment> - <NotYetReadyAppMenu title="Error" onShowSettings={() => setShowSettings(true)} /> + <NotConnectedAppMenu title="Error" /> <NotificationCard notification={{ message: i18n.str`Unexpected Error`, @@ -161,6 +154,25 @@ function ApplicationStatusRoutes(): VNode { ); } + const SUPPORTED_VERSION = "5:0:1" + if (!LibtoolVersion.compare( + SUPPORTED_VERSION, + result.data.version, + )?.compatible) { + return <Fragment> + <NotConnectedAppMenu title="Error" /> + <NotificationCard + notification={{ + message: i18n.str`Incompatible version`, + type: "ERROR", + description: i18n.str`Merchant backend server version ${result.data.version} is not compatible with the supported version ${SUPPORTED_VERSION}`, + }} + /> + <LoginPage onConfirm={updateLoginInfoAndGoToRoot} /> + </Fragment> + + } + return ( <div class="has-navbar-fixed-top"> <ConfigContextProvider value={ctx}> diff --git a/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx b/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx index 277c2b176..46dea98e3 100644 --- a/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx +++ b/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx @@ -22,7 +22,7 @@ import { ErrorType, useTranslationContext } from "@gnu-taler/web-util/browser"; import { createHashHistory } from "history"; import { Fragment, h, VNode } from "preact"; import { Router, Route, route } from "preact-router"; -import { useState } from "preact/hooks"; +import { useEffect, useState } from "preact/hooks"; import { NotificationCard, NotYetReadyAppMenu, @@ -35,52 +35,55 @@ import { INSTANCE_ID_LOOKUP } from "./utils/constants.js"; import { HttpStatusCode } from "@gnu-taler/taler-util"; import { Settings } from "./paths/settings/index.js"; +/** + * Check if admin against /management/instances + * @returns + */ export function ApplicationReadyRoutes(): VNode { const { i18n } = useTranslationContext(); + const [unauthorized, setUnauthorized] = useState(false) const { url: backendURL, - updateLoginStatus, - clearAllTokens, + updateLoginStatus: updateLoginStatus2, } = useBackendContext(); + function updateLoginStatus(url: string, token: string | undefined) { + console.log("updateing", url, token) + updateLoginStatus2(url, token) + setUnauthorized(false) + } + const result = useBackendInstancesTestForAdmin(); const clearTokenAndGoToRoot = () => { - clearAllTokens(); route("/"); }; const [showSettings, setShowSettings] = useState(false) + // useEffect(() => { + // setUnauthorized(FF) + // }, [FF]) + const unauthorizedAdmin = !result.loading && !result.ok && result.type === ErrorType.CLIENT && result.status === HttpStatusCode.Unauthorized if (showSettings) { return <Fragment> - <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="UI Settings" onLogout={clearTokenAndGoToRoot} /> - <Settings/> + <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="UI Settings" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} /> + <Settings /> </Fragment> } - if (result.loading) return <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Loading..." />; - let admin = true; - let instanceNameByBackendURL; + if (result.loading) { + return <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Loading..." isPasswordOk={false} />; + } - if (!result.ok) { - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) { - return ( - <Fragment> - <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Login" onLogout={clearTokenAndGoToRoot} /> - <NotificationCard - notification={{ - message: i18n.str`Access denied`, - description: i18n.str`Check your token is valid`, - type: "ERROR", - }} - /> - <LoginPage onConfirm={updateLoginStatus} /> - </Fragment> - ); - } + let admin = result.ok || unauthorizedAdmin; + let instanceNameByBackendURL: string | undefined; + + if (!admin) { + // * the testing against admin endpoint failed and it's not + // an authorization problem + // * merchant backend will return this SPA under the main + // endpoint or /instance/<id> endpoint + // => trying to infer the instance id const path = new URL(backendURL).pathname; const match = INSTANCE_ID_LOOKUP.exec(path); if (!match || !match[1]) { @@ -89,7 +92,7 @@ export function ApplicationReadyRoutes(): VNode { // does not match our pattern return ( <Fragment> - <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Error" onLogout={clearTokenAndGoToRoot} /> + <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Error" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} /> <NotificationCard notification={{ message: i18n.str`Couldn't access the server.`, @@ -102,10 +105,24 @@ export function ApplicationReadyRoutes(): VNode { ); } - admin = false; instanceNameByBackendURL = match[1]; } + console.log(unauthorized, unauthorizedAdmin) + if (unauthorized || unauthorizedAdmin) { + return <Fragment> + <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Login" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} /> + <NotificationCard + notification={{ + message: i18n.str`Access denied`, + description: i18n.str`Check your token is valid`, + type: "ERROR", + }} + /> + <LoginPage onConfirm={updateLoginStatus} /> + </Fragment> + } + const history = createHashHistory(); return ( <Router history={history}> @@ -113,6 +130,11 @@ export function ApplicationReadyRoutes(): VNode { default component={DefaultMainRoute} admin={admin} + onUnauthorized={() => setUnauthorized(true)} + onLoginPass={() => { + console.log("ahora si") + setUnauthorized(false) + }} instanceNameByBackendURL={instanceNameByBackendURL} /> </Router> @@ -122,6 +144,8 @@ export function ApplicationReadyRoutes(): VNode { function DefaultMainRoute({ instance, admin, + onUnauthorized, + onLoginPass, instanceNameByBackendURL, url, //from preact-router }: any): VNode { @@ -133,6 +157,8 @@ function DefaultMainRoute({ <InstanceRoutes admin={admin} path={url} + onUnauthorized={onUnauthorized} + onLoginPass={onLoginPass} id={instanceName} setInstanceName={setInstanceName} /> diff --git a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx index 1547442ea..ee8db9a9f 100644 --- a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx +++ b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx @@ -40,6 +40,7 @@ import { import { useInstanceKYCDetails } from "./hooks/instance.js"; import InstanceCreatePage from "./paths/admin/create/index.js"; import InstanceListPage from "./paths/admin/list/index.js"; +import TokenPage from "./paths/instance/token/index.js"; import ListKYCPage from "./paths/instance/kyc/list/index.js"; import OrderCreatePage from "./paths/instance/orders/create/index.js"; import OrderDetailsPage from "./paths/instance/orders/details/index.js"; @@ -47,6 +48,9 @@ import OrderListPage from "./paths/instance/orders/list/index.js"; import ProductCreatePage from "./paths/instance/products/create/index.js"; import ProductListPage from "./paths/instance/products/list/index.js"; import ProductUpdatePage from "./paths/instance/products/update/index.js"; +import BankAccountCreatePage from "./paths/instance/accounts/create/index.js"; +import BankAccountListPage from "./paths/instance/accounts/list/index.js"; +import BankAccountUpdatePage from "./paths/instance/accounts/update/index.js"; import ReservesCreatePage from "./paths/instance/reserves/create/index.js"; import ReservesDetailsPage from "./paths/instance/reserves/details/index.js"; import ReservesListPage from "./paths/instance/reserves/list/index.js"; @@ -58,6 +62,9 @@ import TemplateUpdatePage from "./paths/instance/templates/update/index.js"; import WebhookCreatePage from "./paths/instance/webhooks/create/index.js"; import WebhookListPage from "./paths/instance/webhooks/list/index.js"; import WebhookUpdatePage from "./paths/instance/webhooks/update/index.js"; +import ValidatorCreatePage from "./paths/instance/validators/create/index.js"; +import ValidatorListPage from "./paths/instance/validators/list/index.js"; +import ValidatorUpdatePage from "./paths/instance/validators/update/index.js"; import TransferCreatePage from "./paths/instance/transfers/create/index.js"; import TransferListPage from "./paths/instance/transfers/list/index.js"; import InstanceUpdatePage, { @@ -69,11 +76,16 @@ import NotFoundPage from "./paths/notfound/index.js"; import { Notification } from "./utils/types.js"; import { MerchantBackend } from "./declaration.js"; import { Settings } from "./paths/settings/index.js"; +import { dateFormatForSettings, useSettings } from "./hooks/useSettings.js"; export enum InstancePaths { - // details = '/', error = "/error", - update = "/update", + server = "/server", + token = "/token", + + bank_list = "/bank", + bank_update = "/bank/:bid/update", + bank_new = "/bank/new", product_list = "/products", product_update = "/product/:pid/update", @@ -102,11 +114,15 @@ export enum InstancePaths { webhooks_update = "/webhooks/:tid/update", webhooks_new = "/webhooks/new", - settings = "/settings", + validators_list = "/validators", + validators_update = "/validators/:vid/update", + validators_new = "/validators/new", + + settings = "/interface", } // eslint-disable-next-line @typescript-eslint/no-empty-function -const noop = () => {}; +const noop = () => { }; export enum AdminPaths { list_instances = "/instances", @@ -118,6 +134,8 @@ export interface Props { id: string; admin?: boolean; path: string; + onUnauthorized: () => void; + onLoginPass: () => void; setInstanceName: (s: string) => void; } @@ -125,40 +143,29 @@ export function InstanceRoutes({ id, admin, path, + onUnauthorized, + onLoginPass, setInstanceName, }: Props): VNode { - const [_, updateDefaultToken] = useBackendDefaultToken(); + const [defaultToken, updateDefaultToken] = useBackendDefaultToken(); const [token, updateToken] = useBackendInstanceToken(id); - const { - updateLoginStatus: changeBackend, - addTokenCleaner, - clearAllTokens, - } = useBackendContext(); - const cleaner = useCallback(() => { - updateToken(undefined); - }, [id]); const { i18n } = useTranslationContext(); type GlobalNotifState = (Notification & { to: string }) | undefined; const [globalNotification, setGlobalNotification] = useState<GlobalNotifState>(undefined); - useEffect(() => { - addTokenCleaner(cleaner); - }, [addTokenCleaner, cleaner]); - const changeToken = (token?: string) => { if (admin) { updateToken(token); } else { updateDefaultToken(token); } + onLoginPass() }; - const updateLoginStatus = (url: string, token?: string) => { - changeBackend(url); - if (!token) return; - changeToken(token); - }; + // const updateLoginStatus = (url: string, token?: string) => { + // changeToken(token); + // }; const value = useMemo( () => ({ id, token, admin, changeToken }), @@ -192,18 +199,17 @@ export function InstanceRoutes({ }; } - const LoginPageAccessDenied = () => ( - <Fragment> - <NotificationCard - notification={{ - message: i18n.str`Access denied`, - description: i18n.str`The access token provided is invalid.`, - type: "ERROR", - }} - /> - <LoginPage onConfirm={updateLoginStatus} /> - </Fragment> - ); + // const LoginPageAccessDeniend = onUnauthorized + const LoginPageAccessDenied = () => { + onUnauthorized() + return <NotificationCard + notification={{ + message: i18n.str`Access denied`, + description: i18n.str`Redirecting to login page.`, + type: "ERROR", + }} + /> + } function IfAdminCreateDefaultOr<T>(Next: FunctionComponent<any>) { return function IfAdminCreateDefaultOrImpl(props?: T) { @@ -234,8 +240,10 @@ export function InstanceRoutes({ } const clearTokenAndGoToRoot = () => { - clearAllTokens(); route("/"); + // clear all tokens + updateToken(undefined) + updateDefaultToken(undefined) }; return ( @@ -244,11 +252,12 @@ export function InstanceRoutes({ instance={id} admin={admin} onShowSettings={() => { - route("/settings") + route(InstancePaths.settings) }} path={path} onLogout={clearTokenAndGoToRoot} setInstanceName={setInstanceName} + isPasswordOk={defaultToken !== undefined} /> <KycBanner /> <NotificationCard notification={globalNotification} /> @@ -308,7 +317,7 @@ export function InstanceRoutes({ * Update instance page */} <Route - path={InstancePaths.update} + path={InstancePaths.server} component={InstanceUpdatePage} onBack={() => { route(`/`); @@ -322,13 +331,26 @@ export function InstanceRoutes({ onLoadError={ServerErrorRedirectTo(InstancePaths.error)} /> {/** + * Update instance page + */} + <Route + path={InstancePaths.token} + component={TokenPage} + onChange={() => { + route(`/`); + }} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.error)} + /> + {/** * Product pages */} <Route path={InstancePaths.product_list} component={ProductListPage} onUnauthorized={LoginPageAccessDenied} - onLoadError={ServerErrorRedirectTo(InstancePaths.update)} + onLoadError={ServerErrorRedirectTo(InstancePaths.server)} onCreate={() => { route(InstancePaths.product_new); }} @@ -361,6 +383,45 @@ export function InstanceRoutes({ }} /> {/** + * Bank pages + */} + <Route + path={InstancePaths.bank_list} + component={BankAccountListPage} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.server)} + onCreate={() => { + route(InstancePaths.bank_new); + }} + onSelect={(id: string) => { + route(InstancePaths.bank_update.replace(":bid", id)); + }} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + /> + <Route + path={InstancePaths.bank_update} + component={BankAccountUpdatePage} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.product_list)} + onConfirm={() => { + route(InstancePaths.bank_list); + }} + onBack={() => { + route(InstancePaths.bank_list); + }} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + /> + <Route + path={InstancePaths.bank_new} + component={BankAccountCreatePage} + onConfirm={() => { + route(InstancePaths.bank_list); + }} + onBack={() => { + route(InstancePaths.bank_list); + }} + /> + {/** * Order pages */} <Route @@ -373,7 +434,7 @@ export function InstanceRoutes({ route(InstancePaths.order_details.replace(":oid", id)); }} onUnauthorized={LoginPageAccessDenied} - onLoadError={ServerErrorRedirectTo(InstancePaths.update)} + onLoadError={ServerErrorRedirectTo(InstancePaths.server)} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} /> <Route @@ -389,8 +450,8 @@ export function InstanceRoutes({ <Route path={InstancePaths.order_new} component={OrderCreatePage} - onConfirm={() => { - route(InstancePaths.order_list); + onConfirm={(orderId: string) => { + route(InstancePaths.order_details.replace(":oid", orderId)); }} onBack={() => { route(InstancePaths.order_list); @@ -404,7 +465,7 @@ export function InstanceRoutes({ component={TransferListPage} onUnauthorized={LoginPageAccessDenied} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} - onLoadError={ServerErrorRedirectTo(InstancePaths.update)} + onLoadError={ServerErrorRedirectTo(InstancePaths.server)} onCreate={() => { route(InstancePaths.transfers_new); }} @@ -427,7 +488,7 @@ export function InstanceRoutes({ component={WebhookListPage} onUnauthorized={LoginPageAccessDenied} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} - onLoadError={ServerErrorRedirectTo(InstancePaths.update)} + onLoadError={ServerErrorRedirectTo(InstancePaths.server)} onCreate={() => { route(InstancePaths.webhooks_new); }} @@ -459,6 +520,45 @@ export function InstanceRoutes({ }} /> {/** + * Validator pages + */} + <Route + path={InstancePaths.validators_list} + component={ValidatorListPage} + onUnauthorized={LoginPageAccessDenied} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onLoadError={ServerErrorRedirectTo(InstancePaths.server)} + onCreate={() => { + route(InstancePaths.validators_new); + }} + onSelect={(id: string) => { + route(InstancePaths.validators_update.replace(":vid", id)); + }} + /> + <Route + path={InstancePaths.validators_update} + component={ValidatorUpdatePage} + onConfirm={() => { + route(InstancePaths.validators_list); + }} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.validators_list)} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onBack={() => { + route(InstancePaths.validators_list); + }} + /> + <Route + path={InstancePaths.validators_new} + component={ValidatorCreatePage} + onConfirm={() => { + route(InstancePaths.validators_list); + }} + onBack={() => { + route(InstancePaths.validators_list); + }} + /> + {/** * Templates pages */} <Route @@ -466,7 +566,7 @@ export function InstanceRoutes({ component={TemplateListPage} onUnauthorized={LoginPageAccessDenied} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} - onLoadError={ServerErrorRedirectTo(InstancePaths.update)} + onLoadError={ServerErrorRedirectTo(InstancePaths.server)} onCreate={() => { route(InstancePaths.templates_new); }} @@ -535,7 +635,7 @@ export function InstanceRoutes({ component={ReservesListPage} onUnauthorized={LoginPageAccessDenied} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} - onLoadError={ServerErrorRedirectTo(InstancePaths.update)} + onLoadError={ServerErrorRedirectTo(InstancePaths.server)} onSelect={(id: string) => { route(InstancePaths.reserves_details.replace(":rid", id)); }} @@ -590,7 +690,7 @@ function AdminInstanceUpdatePage({ const { updateLoginStatus: changeBackend } = useBackendContext(); const updateLoginStatus = (url: string, token?: string): void => { changeBackend(url); - if (token) changeToken(token); + changeToken(token); }; const value = useMemo( () => ({ id, token, admin: true, changeToken }), @@ -607,20 +707,20 @@ function AdminInstanceUpdatePage({ const notif = error.type === ErrorType.TIMEOUT ? { - message: i18n.str`The request to the backend take too long and was cancelled`, - description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`, - type: "ERROR" as const, - } + message: i18n.str`The request to the backend take too long and was cancelled`, + description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`, + type: "ERROR" as const, + } : { - message: i18n.str`The backend reported a problem: HTTP status #${error.status}`, - description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`, - details: - error.type === ErrorType.CLIENT || + message: i18n.str`The backend reported a problem: HTTP status #${error.status}`, + description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`, + details: + error.type === ErrorType.CLIENT || error.type === ErrorType.SERVER - ? error.payload.detail - : undefined, - type: "ERROR" as const, - }; + ? error.payload.detail + : undefined, + type: "ERROR" as const, + }; return ( <Fragment> <NotificationCard notification={notif} /> @@ -650,7 +750,8 @@ function AdminInstanceUpdatePage({ function KycBanner(): VNode { const kycStatus = useInstanceKYCDetails(); const { i18n } = useTranslationContext(); - const today = format(new Date(), "yyyy-MM-dd"); + const [settings] = useSettings(); + const today = format(new Date(), dateFormatForSettings(settings)); const [lastHide, setLastHide] = useLocalStorage("kyc-last-hide"); const hasBeenHidden = today === lastHide; const needsToBeShown = kycStatus.ok && kycStatus.data.type === "redirect"; diff --git a/packages/merchant-backoffice-ui/src/components/exception/login.tsx b/packages/merchant-backoffice-ui/src/components/exception/login.tsx index f2f94a7c5..4fa440fc7 100644 --- a/packages/merchant-backoffice-ui/src/components/exception/login.tsx +++ b/packages/merchant-backoffice-ui/src/components/exception/login.tsx @@ -93,7 +93,7 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode { <input class="input" type="password" - placeholder={"set new access token"} + placeholder={"current access token"} name="token" onKeyPress={(e) => e.keyCode === 13 @@ -186,7 +186,7 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode { <input class="input" type="password" - placeholder={"set new access token"} + placeholder={"current access token"} name="token" onKeyPress={(e) => e.keyCode === 13 diff --git a/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx b/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx index 1f41c3564..a398629dc 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx @@ -20,16 +20,18 @@ */ import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; -import { h, VNode } from "preact"; +import { ComponentChildren, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { DatePicker } from "../picker/DatePicker.js"; import { InputProps, useField } from "./useField.js"; +import { dateFormatForSettings, useSettings } from "../../hooks/useSettings.js"; export interface Props<T> extends InputProps<T> { readonly?: boolean; expand?: boolean; //FIXME: create separated components InputDate and InputTimestamp withTimestampSupport?: boolean; + side?: ComponentChildren; } export function InputDate<T>({ @@ -41,9 +43,11 @@ export function InputDate<T>({ tooltip, expand, withTimestampSupport, + side, }: Props<keyof T>): VNode { const [opened, setOpened] = useState(false); const { i18n } = useTranslationContext(); + const [settings] = useSettings() const { error, required, value, onChange } = useField<T>(name); @@ -51,14 +55,14 @@ export function InputDate<T>({ if (!value) { strValue = withTimestampSupport ? "unknown" : ""; } else if (value instanceof Date) { - strValue = format(value, "yyyy/MM/dd"); + strValue = format(value, dateFormatForSettings(settings)); } else if (value.t_s) { strValue = value.t_s === "never" ? withTimestampSupport ? "never" : "" - : format(new Date(value.t_s * 1000), "yyyy/MM/dd"); + : format(new Date(value.t_s * 1000), dateFormatForSettings(settings)); } return ( @@ -142,6 +146,7 @@ export function InputDate<T>({ </button> </span> )} + {side} </div> <DatePicker opened={opened} diff --git a/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx index 8d324660e..5cd69a0b3 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx @@ -18,9 +18,9 @@ * * @author Sebastian Javier Marchano (sebasjm) */ +import { parsePaytoUri, PaytoUriGeneric, stringifyPaytoUri } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; -import { useCallback, useState } from "preact/hooks"; import { COUNTRY_TABLE } from "../../utils/constants.js"; import { undefinedIfEmpty } from "../../utils/table.js"; import { FormErrors, FormProvider } from "./FormProvider.js"; @@ -28,23 +28,23 @@ import { Input } from "./Input.js"; import { InputGroup } from "./InputGroup.js"; import { InputSelector } from "./InputSelector.js"; import { InputProps, useField } from "./useField.js"; -import { InputWithAddon } from "./InputWithAddon.js"; -import { MerchantBackend } from "../../declaration.js"; +import { useEffect, useState } from "preact/hooks"; export interface Props<T> extends InputProps<T> { isValid?: (e: any) => boolean; } +// type Entity = PaytoUriGeneric // https://datatracker.ietf.org/doc/html/rfc8905 type Entity = { // iban, bitcoin, x-taler-bank. it defined the format target: string; // path1 if the first field to be used - path1: string; + path1?: string; // path2 if the second field to be used, optional path2?: string; - // options of the payto uri - options: { + // params of the payto uri + params: { "receiver-name"?: string; sender?: string; message?: string; @@ -52,13 +52,6 @@ type Entity = { instruction?: string; [name: string]: string | undefined; }; - auth: { - type: "unset" | "basic" | "none"; - url?: string; - username?: string; - password?: string; - repeat?: string; - }; }; function isEthereumAddress(address: string) { @@ -171,14 +164,10 @@ const targets = [ "bitcoin", "ethereum", ]; -const accountAuthType = ["none", "basic"]; const noTargetValue = targets[0]; -const defaultTarget: Partial<Entity> = { +const defaultTarget: Entity = { target: noTargetValue, - options: {}, - auth: { - type: "unset" as const, - }, + params: {}, }; export function InputPaytoForm<T>({ @@ -187,110 +176,91 @@ export function InputPaytoForm<T>({ label, tooltip, }: Props<keyof T>): VNode { - const { value: paytos, onChange, required } = useField<T>(name); - - const [value, valueHandler] = useState<Partial<Entity>>(defaultTarget); + const { value: initialValueStr, onChange } = useField<T>(name); - let payToPath; - if (value.target === "iban" && value.path1) { - payToPath = `/${value.path1.toUpperCase()}`; - } else if (value.path1) { - if (value.path2) { - payToPath = `/${value.path1}/${value.path2}`; - } else { - payToPath = `/${value.path1}`; - } + const initialPayto = parsePaytoUri(initialValueStr ?? "") + const paths = !initialPayto ? [] : initialPayto.targetPath.split("/") + const initialPath1 = paths.length >= 1 ? paths[0] : undefined; + const initialPath2 = paths.length >= 2 ? paths[1] : undefined; + const initial: Entity = initialPayto === undefined ? defaultTarget : { + target: initialPayto.targetType, + params: initialPayto.params, + path1: initialPath1, + path2: initialPath2, } - const { i18n } = useTranslationContext(); + const [value, setValue] = useState<Partial<Entity>>(initial) - const ops = value.options ?? {}; - const url = tryUrl(`payto://${value.target}${payToPath}`); - if (url) { - Object.keys(ops).forEach((opt_key) => { - const opt_value = ops[opt_key]; - if (opt_value) url.searchParams.set(opt_key, opt_value); - }); - } - const paytoURL = !url ? "" : url.href; + const { i18n } = useTranslationContext(); const errors: FormErrors<Entity> = { target: - value.target === noTargetValue && !paytos.length + value.target === noTargetValue ? i18n.str`required` : undefined, path1: !value.path1 ? i18n.str`required` : value.target === "iban" - ? validateIBAN(value.path1, i18n) - : value.target === "bitcoin" - ? validateBitcoin(value.path1, i18n) - : value.target === "ethereum" - ? validateEthereum(value.path1, i18n) - : undefined, + ? validateIBAN(value.path1, i18n) + : value.target === "bitcoin" + ? validateBitcoin(value.path1, i18n) + : value.target === "ethereum" + ? validateEthereum(value.path1, i18n) + : undefined, path2: value.target === "x-taler-bank" ? !value.path2 ? i18n.str`required` : undefined : undefined, - options: undefinedIfEmpty({ - "receiver-name": !value.options?.["receiver-name"] + params: undefinedIfEmpty({ + "receiver-name": !value.params?.["receiver-name"] ? i18n.str`required` : undefined, }), - auth: !value.auth - ? undefined - : undefinedIfEmpty({ - username: - value.auth.type === "basic" && !value.auth.username - ? i18n.str`required` - : undefined, - password: - value.auth.type === "basic" && !value.auth.password - ? i18n.str`required` - : undefined, - repeat: - value.auth.type === "basic" && !value.auth.repeat - ? i18n.str`required` - : value.auth.repeat !== value.auth.password - ? i18n.str`is not the same` - : undefined, - }), }; const hasErrors = Object.keys(errors).some( (k) => (errors as any)[k] !== undefined, ); + const str = hasErrors || !value.target ? undefined : stringifyPaytoUri({ + targetType: value.target, + targetPath: value.path2 ? `${value.path1}/${value.path2}` : (value.path1 ?? ""), + params: value.params ?? {} as any, + isKnown: false, + }) + useEffect(() => { + onChange(str as any) + }, [str]) - const submit = useCallback((): void => { - const accounts: MerchantBackend.Instances.MerchantBankAccount[] = paytos; - const alreadyExists = - accounts.findIndex((x) => x.payto_uri === paytoURL) !== -1; - if (!alreadyExists) { - const newValue: MerchantBackend.Instances.MerchantBankAccount = { - payto_uri: paytoURL, - }; - if (value.auth) { - if (value.auth.url) { - newValue.credit_facade_url = value.auth.url; - } - if (value.auth.type === "none") { - newValue.credit_facade_credentials = { - type: "none", - }; - } - if (value.auth.type === "basic") { - newValue.credit_facade_credentials = { - type: "basic", - username: value.auth.username ?? "", - password: value.auth.password ?? "", - }; - } - } - onChange([newValue, ...accounts] as any); - } - valueHandler(defaultTarget); - }, [value]); + // const submit = useCallback((): void => { + // // const accounts: MerchantBackend.BankAccounts.AccountAddDetails[] = paytos; + // // const alreadyExists = + // // accounts.findIndex((x) => x.payto_uri === paytoURL) !== -1; + // // if (!alreadyExists) { + // const newValue: MerchantBackend.BankAccounts.AccountAddDetails = { + // payto_uri: paytoURL, + // }; + // if (value.auth) { + // if (value.auth.url) { + // newValue.credit_facade_url = value.auth.url; + // } + // if (value.auth.type === "none") { + // newValue.credit_facade_credentials = { + // type: "none", + // }; + // } + // if (value.auth.type === "basic") { + // newValue.credit_facade_credentials = { + // type: "basic", + // username: value.auth.username ?? "", + // password: value.auth.password ?? "", + // }; + // } + // } + // onChange(newValue as any); + // // } + // // valueHandler(defaultTarget); + // }, [value]); //FIXME: translating plural singular return ( @@ -299,11 +269,11 @@ export function InputPaytoForm<T>({ name="tax" errors={errors} object={value} - valueHandler={valueHandler} + valueHandler={setValue} > <InputSelector<Entity> name="target" - label={i18n.str`Target type`} + label={i18n.str`Account type`} tooltip={i18n.str`Method to use for wire transfer`} values={targets} toStr={(v) => (v === noTargetValue ? i18n.str`Choose one...` : v)} @@ -400,150 +370,15 @@ export function InputPaytoForm<T>({ {value.target !== noTargetValue && ( <Fragment> <Input - name="options.receiver-name" + name="params.receiver-name" label={i18n.str`Name`} tooltip={i18n.str`Bank account owner's name.`} /> - <InputWithAddon - name="auth.url" - label={i18n.str`Account info URL`} - help="https://bank.com" - expand - tooltip={i18n.str`From where the merchant can download information about incoming wire transfers to this account`} - /> - <InputSelector - name="auth.type" - label={i18n.str`Auth type`} - tooltip={i18n.str`Choose the authentication type for the account info URL`} - values={accountAuthType} - toStr={(str) => { - // if (str === "unset") { - // return "Without change"; - // } - if (str === "none") return "Without authentication"; - return "Username and password"; - }} - /> - {value.auth?.type === "basic" ? ( - <Fragment> - <Input - name="auth.username" - label={i18n.str`Username`} - tooltip={i18n.str`Username to access the account information.`} - /> - <Input - name="auth.password" - inputType="password" - label={i18n.str`Password`} - tooltip={i18n.str`Password to access the account information.`} - /> - <Input - name="auth.repeat" - inputType="password" - label={i18n.str`Repeat password`} - /> - </Fragment> - ) : undefined} - - {/* <InputWithAddon - name="options.credit_credentials" - label={i18n.str`Account info`} - inputType={showKey ? "text" : "password"} - help="From where the merchant can download information about incoming wire transfers to this account" - expand - tooltip={i18n.str`Useful to validate the purchase`} - fromStr={(v) => v.toUpperCase()} - addonAfter={ - <span class="icon"> - {showKey ? ( - <i class="mdi mdi-eye" /> - ) : ( - <i class="mdi mdi-eye-off" /> - )} - </span> - } - side={ - <span style={{ display: "flex" }}> - <button - data-tooltip={ - showKey - ? i18n.str`show secret key` - : i18n.str`hide secret key` - } - class="button is-info mr-3" - onClick={(e) => { - setShowKey(!showKey); - }} - > - {showKey ? ( - <i18n.Translate>hide</i18n.Translate> - ) : ( - <i18n.Translate>show</i18n.Translate> - )} - </button> - </span> - } - /> */} </Fragment> )} - {/** - * Show the values in the list - */} - <div class="field is-horizontal"> - <div class="field-label is-normal" /> - <div class="field-body" style={{ display: "block" }}> - {paytos.map( - (v: MerchantBackend.Instances.MerchantBankAccount, i: number) => ( - <div - key={i} - class="tags has-addons mt-3 mb-0 mr-3" - style={{ flexWrap: "nowrap" }} - > - <span - class="tag is-medium is-info mb-0" - style={{ maxWidth: "90%" }} - > - {v.payto_uri} - </span> - <a - class="tag is-medium is-danger is-delete mb-0" - onClick={() => { - onChange(paytos.filter((f: any) => f !== v) as any); - }} - /> - </div> - ), - )} - {!paytos.length && i18n.str`No accounts yet.`} - {required && ( - <span class="icon has-text-danger is-right"> - <i class="mdi mdi-alert" /> - </span> - )} - </div> - </div> - {value.target !== noTargetValue && ( - <div class="buttons is-right mt-5"> - <button - class="button is-info" - data-tooltip={i18n.str`add tax to the tax list`} - disabled={hasErrors} - onClick={submit} - > - <i18n.Translate>Add</i18n.Translate> - </button> - </div> - )} </FormProvider> </InputGroup> ); } -function tryUrl(s: string): URL | undefined { - try { - return new URL(s); - } catch (e) { - return undefined; - } -} diff --git a/packages/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx index 1c1fcb907..be5800d14 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx @@ -22,32 +22,41 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { useState } from "preact/hooks"; import emptyImage from "../../assets/empty.png"; -import { MerchantBackend, WithId } from "../../declaration.js"; import { FormErrors, FormProvider } from "./FormProvider.js"; import { InputWithAddon } from "./InputWithAddon.js"; +import { TranslatedString } from "@gnu-taler/taler-util"; -type Entity = MerchantBackend.Products.ProductDetail & WithId; +type Entity = { + id: string, + description: string; + image?: string; + extra?: string; +}; -export interface Props { - selected?: Entity; - onChange: (p?: Entity) => void; - products: (MerchantBackend.Products.ProductDetail & WithId)[]; +export interface Props<T extends Entity> { + selected?: T; + onChange: (p?: T) => void; + label: TranslatedString; + list: T[]; + withImage?: boolean; } -interface ProductSearch { +interface Search { name: string; } -export function InputSearchProduct({ +export function InputSearchOnList<T extends Entity>({ selected, onChange, - products, -}: Props): VNode { - const [prodForm, setProdName] = useState<Partial<ProductSearch>>({ + label, + list, + withImage, +}: Props<T>): VNode { + const [nameForm, setNameForm] = useState<Partial<Search>>({ name: "", }); - const errors: FormErrors<ProductSearch> = { + const errors: FormErrors<Search> = { name: undefined, }; const { i18n } = useTranslationContext(); @@ -55,15 +64,17 @@ export function InputSearchProduct({ if (selected) { return ( <article class="media"> - <figure class="media-left"> - <p class="image is-128x128"> - <img src={selected.image ? selected.image : emptyImage} /> - </p> - </figure> + {withImage && + <figure class="media-left"> + <p class="image is-128x128"> + <img src={selected.image ? selected.image : emptyImage} /> + </p> + </figure> + } <div class="media-content"> <div class="content"> <p class="media-meta"> - <i18n.Translate>Product id</i18n.Translate>: <b>{selected.id}</b> + <i18n.Translate>ID</i18n.Translate>: <b>{selected.id}</b> </p> <p> <i18n.Translate>Description</i18n.Translate>:{" "} @@ -84,15 +95,15 @@ export function InputSearchProduct({ } return ( - <FormProvider<ProductSearch> + <FormProvider<Search> errors={errors} - object={prodForm} - valueHandler={setProdName} + object={nameForm} + valueHandler={setNameForm} > - <InputWithAddon<ProductSearch> + <InputWithAddon<Search> name="name" - label={i18n.str`Product`} - tooltip={i18n.str`search products by it's description or id`} + label={label} + tooltip={i18n.str`enter description or id`} addonAfter={ <span class="icon"> <i class="mdi mdi-magnify" /> @@ -100,13 +111,14 @@ export function InputSearchProduct({ } > <div> - <ProductList - name={prodForm.name} - list={products} + <DropdownList + name={nameForm.name} + list={list} onSelect={(p) => { - setProdName({ name: "" }); + setNameForm({ name: "" }); onChange(p); }} + withImage={!!withImage} /> </div> </InputWithAddon> @@ -114,13 +126,14 @@ export function InputSearchProduct({ ); } -interface ProductListProps { +interface DropdownListProps<T extends Entity> { name?: string; - onSelect: (p: MerchantBackend.Products.ProductDetail & WithId) => void; - list: (MerchantBackend.Products.ProductDetail & WithId)[]; + onSelect: (p: T) => void; + list: T[]; + withImage: boolean; } -function ProductList({ name, onSelect, list }: ProductListProps) { +function DropdownList<T extends Entity>({ name, onSelect, list, withImage }: DropdownListProps<T>) { const { i18n } = useTranslationContext(); if (!name) { /* FIXME @@ -149,7 +162,7 @@ function ProductList({ name, onSelect, list }: ProductListProps) { {!filtered.length ? ( <div class="dropdown-item"> <i18n.Translate> - no products found with that description + no match found with that description or id </i18n.Translate> </div> ) : ( @@ -161,18 +174,20 @@ function ProductList({ name, onSelect, list }: ProductListProps) { style={{ cursor: "pointer" }} > <article class="media"> - <div class="media-left"> - <div class="image" style={{ minWidth: 64 }}> - <img - src={p.image ? p.image : emptyImage} - style={{ width: 64, height: 64 }} - /> + {withImage && + <div class="media-left"> + <div class="image" style={{ minWidth: 64 }}> + <img + src={p.image ? p.image : emptyImage} + style={{ width: 64, height: 64 }} + /> + </div> </div> - </div> + } <div class="media-content"> <div class="content"> <p> - <strong>{p.id}</strong> <small>{p.price}</small> + <strong>{p.id}</strong> {p.extra !== undefined ? <small>{p.extra}</small> : undefined} <br /> {p.description} </p> diff --git a/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx b/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx index 61ddf3c84..f95dfcd05 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx @@ -56,7 +56,7 @@ export function InputToggle<T>({ return ( <div class="field is-horizontal"> <div class="field-label is-normal"> - <label class="label" style={{ width: 200 }}> + <label class="label" > {label} {tooltip && ( <span class="icon has-tooltip-right" data-tooltip={tooltip}> @@ -65,7 +65,7 @@ export function InputToggle<T>({ )} </label> </div> - <div class="field-body is-flex-grow-1"> + <div class="field-body is-flex-grow-3"> <div class="field"> <p class={expand ? "control is-expanded" : "control"}> <label class="toggle" style={{ marginLeft: 4, marginTop: 0 }}> diff --git a/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx b/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx index 24380ce98..b75dc83b3 100644 --- a/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx +++ b/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx @@ -24,14 +24,13 @@ import { Fragment, h, VNode } from "preact"; import { useBackendContext } from "../../context/backend.js"; import { Entity } from "../../paths/admin/create/CreatePage.js"; import { Input } from "../form/Input.js"; -import { InputCurrency } from "../form/InputCurrency.js"; import { InputDuration } from "../form/InputDuration.js"; import { InputGroup } from "../form/InputGroup.js"; import { InputImage } from "../form/InputImage.js"; import { InputLocation } from "../form/InputLocation.js"; -import { InputPaytoForm } from "../form/InputPaytoForm.js"; -import { InputWithAddon } from "../form/InputWithAddon.js"; import { InputSelector } from "../form/InputSelector.js"; +import { InputToggle } from "../form/InputToggle.js"; +import { InputWithAddon } from "../form/InputWithAddon.js"; export function DefaultInstanceFormFields({ readonlyId, @@ -85,28 +84,10 @@ export function DefaultInstanceFormFields({ tooltip={i18n.str`Logo image.`} /> - <InputPaytoForm<Entity> - name="accounts" - label={i18n.str`Bank account`} - tooltip={i18n.str`URI specifying bank account for crediting revenue.`} - /> - - <InputCurrency<Entity> - name="default_max_deposit_fee" - label={i18n.str`Default max deposit fee`} - tooltip={i18n.str`Maximum deposit fees this merchant is willing to pay per order by default.`} - /> - - <InputCurrency<Entity> - name="default_max_wire_fee" - label={i18n.str`Default max wire fee`} - tooltip={i18n.str`Maximum wire fees this merchant is willing to pay per wire transfer by default.`} - /> - - <Input<Entity> - name="default_wire_fee_amortization" - label={i18n.str`Default wire fee amortization`} - tooltip={i18n.str`Number of orders excess wire transfer fees will be divided by to compute per order surcharge.`} + <InputToggle<Entity> + name="use_stefan" + label={i18n.str`Pay transaction fee`} + tooltip={i18n.str`Assume the cost of the transaction of let the user pay for it.`} /> <InputGroup diff --git a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx index f3cf80b92..be2f8dde5 100644 --- a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx +++ b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx @@ -25,6 +25,7 @@ import { useBackendContext } from "../../context/backend.js"; import { useConfigContext } from "../../context/config.js"; import { useInstanceKYCDetails } from "../../hooks/instance.js"; import { LangSelector } from "./LangSelector.js"; +import { useCredentialsChecker } from "../../hooks/backend.js"; const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined; const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined; @@ -36,6 +37,7 @@ interface Props { instance: string; admin?: boolean; mimic?: boolean; + isPasswordOk: boolean; } export function Sidebar({ @@ -45,6 +47,7 @@ export function Sidebar({ onLogout, admin, mimic, + isPasswordOk }: Props): VNode { const config = useConfigContext(); const backend = useBackendContext(); @@ -53,7 +56,7 @@ export function Sidebar({ const needKYC = kycStatus.ok && kycStatus.data.type === "redirect"; return ( - <aside class="aside is-placed-left is-expanded"> + <aside class="aside is-placed-left is-expanded" style={{ overflowY: "scroll" }}> {mobile && ( <div class="footer" @@ -78,10 +81,10 @@ export function Sidebar({ </div> </div> <div class="menu is-menu-main"> - {instance ? ( + {isPasswordOk && instance ? ( <Fragment> <ul class="menu-list"> - <li> + <li> <a href={"/orders"} class="has-icon"> <span class="icon"> <i class="mdi mdi-cash-register" /> @@ -104,7 +107,7 @@ export function Sidebar({ <li> <a href={"/transfers"} class="has-icon"> <span class="icon"> - <i class="mdi mdi-bank" /> + <i class="mdi mdi-arrow-left-right" /> </span> <span class="menu-item-label"> <i18n.Translate>Transfers</i18n.Translate> @@ -137,12 +140,22 @@ export function Sidebar({ </p> <ul class="menu-list"> <li> - <a href={"/update"} class="has-icon"> + <a href={"/bank"} class="has-icon"> <span class="icon"> - <i class="mdi mdi-square-edit-outline" /> + <i class="mdi mdi-bank" /> + </span> + <span class="menu-item-label"> + <i18n.Translate>Bank account</i18n.Translate> + </span> + </a> + </li> + <li> + <a href={"/validators"} class="has-icon"> + <span class="icon"> + <i class="mdi mdi-lock" /> </span> <span class="menu-item-label"> - <i18n.Translate>Account</i18n.Translate> + <i18n.Translate>Validators</i18n.Translate> </span> </a> </li> @@ -164,6 +177,26 @@ export function Sidebar({ </span> </a> </li> + <li> + <a href={"/server"} class="has-icon"> + <span class="icon"> + <i class="mdi mdi-square-edit-outline" /> + </span> + <span class="menu-item-label"> + <i18n.Translate>Server</i18n.Translate> + </span> + </a> + </li> + <li> + <a href={"/token"} class="has-icon"> + <span class="icon"> + <i class="mdi mdi-security" /> + </span> + <span class="menu-item-label"> + <i18n.Translate>Access token</i18n.Translate> + </span> + </a> + </li> </ul> </Fragment> ) : undefined} @@ -174,12 +207,12 @@ export function Sidebar({ <li> <a class="has-icon is-state-info is-hoverable" onClick={(): void => onShowSettings()} - > + > <span class="icon"> <i class="mdi mdi-newspaper" /> </span> <span class="menu-item-label"> - <i18n.Translate>Settings</i18n.Translate> + <i18n.Translate>Interface</i18n.Translate> </span> </a> </li> @@ -211,7 +244,7 @@ export function Sidebar({ </span> </div> </li> - {admin && !mimic && ( + {isPasswordOk && admin && !mimic && ( <Fragment> <p class="menu-label"> <i18n.Translate>Instances</i18n.Translate> @@ -238,19 +271,21 @@ export function Sidebar({ </li> </Fragment> )} - <li> - <a - class="has-icon is-state-info is-hoverable" - onClick={(): void => onLogout()} - > - <span class="icon"> - <i class="mdi mdi-logout default" /> - </span> - <span class="menu-item-label"> - <i18n.Translate>Log out</i18n.Translate> - </span> - </a> - </li> + {isPasswordOk && + <li> + <a + class="has-icon is-state-info is-hoverable" + onClick={(): void => onLogout()} + > + <span class="icon"> + <i class="mdi mdi-logout default" /> + </span> + <span class="menu-item-label"> + <i18n.Translate>Log out</i18n.Translate> + </span> + </a> + </li> + } </ul> </div> </aside> diff --git a/packages/merchant-backoffice-ui/src/components/menu/index.tsx b/packages/merchant-backoffice-ui/src/components/menu/index.tsx index cdbae4ae0..cb318906f 100644 --- a/packages/merchant-backoffice-ui/src/components/menu/index.tsx +++ b/packages/merchant-backoffice-ui/src/components/menu/index.tsx @@ -24,7 +24,7 @@ import { Sidebar } from "./SideBar.js"; function getInstanceTitle(path: string, id: string): string { switch (path) { - case InstancePaths.update: + case InstancePaths.server: return `${id}: Settings`; case InstancePaths.order_list: return `${id}: Orders`; @@ -50,6 +50,12 @@ function getInstanceTitle(path: string, id: string): string { return `${id}: New webhook`; case InstancePaths.webhooks_update: return `${id}: Update webhook`; + case InstancePaths.validators_list: + return `${id}: Validators`; + case InstancePaths.validators_new: + return `${id}: New validator`; + case InstancePaths.validators_update: + return `${id}: Update validators`; case InstancePaths.templates_new: return `${id}: New template`; case InstancePaths.templates_update: @@ -58,6 +64,10 @@ function getInstanceTitle(path: string, id: string): string { return `${id}: Templates`; case InstancePaths.templates_use: return `${id}: Use template`; + case InstancePaths.settings: + return `${id}: Interface`; + case InstancePaths.settings: + return `${id}: Interface`; default: return ""; } @@ -77,6 +87,7 @@ interface MenuProps { onLogout?: () => void; onShowSettings: () => void; setInstanceName: (s: string) => void; + isPasswordOk: boolean; } function WithTitle({ @@ -100,14 +111,15 @@ export function Menu({ path, admin, setInstanceName, + isPasswordOk }: MenuProps): VNode { const [mobileOpen, setMobileOpen] = useState(false); const titleWithSubtitle = title ? title : !admin - ? getInstanceTitle(path, instance) - : getAdminTitle(path, instance); + ? getInstanceTitle(path, instance) + : getAdminTitle(path, instance); const adminInstance = instance === "default"; const mimic = admin && !adminInstance; return ( @@ -129,14 +141,15 @@ export function Menu({ mimic={mimic} instance={instance} mobile={mobileOpen} + isPasswordOk={isPasswordOk} /> )} {mimic && ( <nav class="level" style={{ zIndex: 100, - position:"fixed", - width:"50%", + position: "fixed", + width: "50%", marginLeft: "20%" }}> <div class="level-item has-text-centered has-background-warning"> @@ -161,8 +174,9 @@ export function Menu({ interface NotYetReadyAppMenuProps { title: string; - onLogout?: () => void; onShowSettings: () => void; + onLogout?: () => void; + isPasswordOk: boolean; } interface NotifProps { @@ -181,8 +195,8 @@ export function NotificationCard({ n.type === "ERROR" ? "message is-danger" : n.type === "WARN" - ? "message is-warning" - : "message is-info" + ? "message is-warning" + : "message is-info" } > <div class="message-header"> @@ -201,10 +215,36 @@ export function NotificationCard({ ); } +interface NotConnectedAppMenuProps { + title: string; +} +export function NotConnectedAppMenu({ + title, +}: NotConnectedAppMenuProps): VNode { + const [mobileOpen, setMobileOpen] = useState(false); + + useEffect(() => { + document.title = `Taler Backoffice: ${title}`; + }, [title]); + + return ( + <div + class={mobileOpen ? "has-aside-mobile-expanded" : ""} + onClick={() => setMobileOpen(false)} + > + <NavigationBar + onMobileMenu={() => setMobileOpen(!mobileOpen)} + title={title} + /> + </div> + ); +} + export function NotYetReadyAppMenu({ onLogout, onShowSettings, title, + isPasswordOk }: NotYetReadyAppMenuProps): VNode { const [mobileOpen, setMobileOpen] = useState(false); @@ -222,7 +262,7 @@ export function NotYetReadyAppMenu({ title={title} /> {onLogout && ( - <Sidebar onShowSettings={onShowSettings} onLogout={onLogout} instance="" mobile={mobileOpen} /> + <Sidebar onShowSettings={onShowSettings} onLogout={onLogout} instance="" mobile={mobileOpen} isPasswordOk={isPasswordOk} /> )} </div> ); diff --git a/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx index b2ec4dd11..377d9c1ba 100644 --- a/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx +++ b/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx @@ -20,7 +20,7 @@ import { MerchantBackend, WithId } from "../../declaration.js"; import { ProductMap } from "../../paths/instance/orders/create/CreatePage.js"; import { FormErrors, FormProvider } from "../form/FormProvider.js"; import { InputNumber } from "../form/InputNumber.js"; -import { InputSearchProduct } from "../form/InputSearchProduct.js"; +import { InputSearchOnList } from "../form/InputSearchOnList.js"; type Form = { product: MerchantBackend.Products.ProductDetail & WithId; @@ -95,10 +95,12 @@ export function InventoryProductForm({ return ( <FormProvider<Form> errors={errors} object={state} valueHandler={setState}> - <InputSearchProduct + <InputSearchOnList + label={i18n.str`Search product`} selected={state.product} onChange={(p) => setState((v) => ({ ...v, product: p }))} - products={inventory} + list={inventory} + withImage /> {state.product && ( <div class="columns mt-5"> diff --git a/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx index 7956a9ea5..726a94f5e 100644 --- a/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx +++ b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx @@ -58,12 +58,12 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) { !initial || initial.total_stock === -1 ? undefined : { - current: initial.total_stock || 0, - lost: initial.total_lost || 0, - sold: initial.total_sold || 0, - address: initial.address, - nextRestock: initial.next_restock, - }, + current: initial.total_stock || 0, + lost: initial.total_lost || 0, + sold: initial.total_sold || 0, + address: initial.address, + nextRestock: initial.next_restock, + }, }); let errors: FormErrors<Entity> = {}; @@ -148,15 +148,17 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) { name="minimum_age" label={i18n.str`Age restricted`} tooltip={i18n.str`is this product restricted for customer below certain age?`} + help={i18n.str`can be overridden by the order configuration`} /> <Input<Entity> name="unit" - label={i18n.str`Unit`} + label={i18n.str`Unit name`} tooltip={i18n.str`unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers`} + help={i18n.str`exajmple: kg, items or liters`} /> <InputCurrency<Entity> name="price" - label={i18n.str`Price`} + label={i18n.str`Price per unit`} tooltip={i18n.str`sale price for customers, including taxes, for above units of the product`} /> <InputStock diff --git a/packages/merchant-backoffice-ui/src/context/backend.ts b/packages/merchant-backoffice-ui/src/context/backend.ts index f7f8afea6..43e9e4d27 100644 --- a/packages/merchant-backoffice-ui/src/context/backend.ts +++ b/packages/merchant-backoffice-ui/src/context/backend.ts @@ -28,8 +28,8 @@ interface BackendContextType { token?: string; triedToLog: boolean; resetBackend: () => void; - clearAllTokens: () => void; - addTokenCleaner: (c: () => void) => void; + // clearAllTokens: () => void; + // addTokenCleaner: (c: () => void) => void; updateLoginStatus: (url: string, token?: string) => void; updateToken: (token?: string) => void; } @@ -39,8 +39,8 @@ const BackendContext = createContext<BackendContextType>({ token: undefined, triedToLog: false, resetBackend: () => null, - clearAllTokens: () => null, - addTokenCleaner: () => null, + // clearAllTokens: () => null, + // addTokenCleaner: () => null, updateLoginStatus: () => null, updateToken: () => null, }); @@ -56,30 +56,30 @@ function useBackendContextState( _updateToken(t); }; - const tokenCleaner = useCallback(() => { - updateToken(undefined); - }, []); - const [cleaners, setCleaners] = useState([tokenCleaner]); - const addTokenCleaner = (c: () => void) => setCleaners((cs) => [...cs, c]); - const addTokenCleanerMemo = useCallback( - (c: () => void) => { - addTokenCleaner(c); - }, - [tokenCleaner], - ); + // const tokenCleaner = useCallback(() => { + // updateToken(undefined); + // }, []); + // const [cleaners, setCleaners] = useState([tokenCleaner]); + // const addTokenCleaner = (c: () => void) => setCleaners((cs) => [...cs, c]); + // const addTokenCleanerMemo = useCallback( + // (c: () => void) => { + // addTokenCleaner(c); + // }, + // [tokenCleaner], + // ); - const clearAllTokens = () => { - cleaners.forEach((c) => c()); - for (let i = 0; i < localStorage.length; i++) { - const k = localStorage.key(i); - if (k && /^backend-token/.test(k)) localStorage.removeItem(k); - } - resetBackend(); - }; + // const clearAllTokens = () => { + // cleaners.forEach((c) => c()); + // for (let i = 0; i < localStorage.length; i++) { + // const k = localStorage.key(i); + // if (k && /^backend-token/.test(k)) localStorage.removeItem(k); + // } + // resetBackend(); + // }; const updateLoginStatus = (url: string, token?: string) => { changeBackend(url); - if (token) updateToken(token); + updateToken(token); }; return { @@ -88,9 +88,9 @@ function useBackendContextState( triedToLog, updateLoginStatus, resetBackend, - clearAllTokens, + // clearAllTokens, updateToken, - addTokenCleaner: addTokenCleanerMemo, + // addTokenCleaner: addTokenCleanerMemo, }; } diff --git a/packages/merchant-backoffice-ui/src/declaration.d.ts b/packages/merchant-backoffice-ui/src/declaration.d.ts index db3122266..5ca9c1e09 100644 --- a/packages/merchant-backoffice-ui/src/declaration.d.ts +++ b/packages/merchant-backoffice-ui/src/declaration.d.ts @@ -25,6 +25,8 @@ type EddsaSignature = string; type WireTransferIdentifierRawP = string; type RelativeTime = Duration; type ImageDataUrl = string; +type MerchantUserType = "business" | "individual"; + export interface WithId { id: string; @@ -312,46 +314,8 @@ export namespace MerchantBackend { // header. token?: string; } - type FacadeCredentials = NoFacadeCredentials | BasicAuthFacadeCredentials; - - interface NoFacadeCredentials { - type: "none"; - } - - interface BasicAuthFacadeCredentials { - type: "basic"; - - // Username to use to authenticate - username: string; - - // Password to use to authenticate - password: string; - } - - interface MerchantBankAccount { - // The payto:// URI where the wallet will send coins. - payto_uri: string; - - // Optional base URL for a facade where the - // merchant backend can see incoming wire - // transfers to reconcile its accounting - // with that of the exchange. Used by - // taler-merchant-wirewatch. - credit_facade_url?: string; - - // Credentials for accessing the credit facade. - credit_facade_credentials?: FacadeCredentials; - } //POST /private/instances interface InstanceConfigurationMessage { - // Bank accounts of the merchant. A merchant may have - // multiple accounts, thus this is an array. Note that by - // removing accounts from this list the respective account is set to - // inactive and thus unavailable for new contracts, but preserved - // in the database as existing offers and contracts may still refer - // to it. - accounts: MerchantBankAccount[]; - // Name of the merchant instance to create (will become $INSTANCE). id: string; @@ -361,12 +325,16 @@ export namespace MerchantBackend { // Type of the user (business or individual). // Defaults to 'business'. Should become mandatory field // in the future, left as optional for API compatibility for now. - user_type?: string; + user_type?: MerchantUserType; + + // Merchant email for customer contact. + email?: string; + + // Merchant public website. + website?: string; - email: string; - website: string; - // An optional base64-encoded logo image - logo: ImageDataUrl; + // Merchant logo. + logo?: ImageDataUrl; // "Authentication" header required to authorize management access the instance. // Optional, if not given authentication will be disabled for @@ -381,17 +349,10 @@ export namespace MerchantBackend { // (to be put into contracts). jurisdiction: Location; - // Maximum wire fee this instance is willing to pay. - // Can be overridden by the frontend on a per-order basis. - default_max_wire_fee: Amount; - - // Default factor for wire fee amortization calculations. - // Can be overridden by the frontend on a per-order basis. - default_wire_fee_amortization: Integer; - - // Maximum deposit fee (sum over all coins) this instance is willing to pay. - // Can be overridden by the frontend on a per-order basis. - default_max_deposit_fee: Amount; + // Use STEFAN curves to determine default fees? + // If false, no fees are allowed by default. + // Can always be overridden by the frontend on a per-order basis. + use_stefan: boolean; // If the frontend does NOT specify an execution date, how long should // we tell the exchange to wait to aggregate transactions before @@ -406,11 +367,6 @@ export namespace MerchantBackend { // PATCH /private/instances/$INSTANCE interface InstanceReconfigurationMessage { - // Bank accounts of the merchant. A merchant may have - // multiple accounts, thus this is an array. Note that removing - // URIs from this list deactivates the specified accounts - // (they will no longer be used for future contracts). - accounts: MerchantBankAccount[]; // Merchant name corresponding to this instance. name: string; @@ -418,7 +374,16 @@ export namespace MerchantBackend { // Type of the user (business or individual). // Defaults to 'business'. Should become mandatory field // in the future, left as optional for API compatibility for now. - user_type?: string; + user_type?: MerchantUserType; + + // Merchant email for customer contact. + email?: string; + + // Merchant public website. + website?: string; + + // Merchant logo. + logo?: ImageDataUrl; // The merchant's physical address (to be put into contracts). address: Location; @@ -427,17 +392,10 @@ export namespace MerchantBackend { // (to be put into contracts). jurisdiction: Location; - // Maximum wire fee this instance is willing to pay. - // Can be overridden by the frontend on a per-order basis. - default_max_wire_fee: Amount; - - // Default factor for wire fee amortization calculations. - // Can be overridden by the frontend on a per-order basis. - default_wire_fee_amortization: Integer; - - // Maximum deposit fee (sum over all coins) this instance is willing to pay. - // Can be overridden by the frontend on a per-order basis. - default_max_deposit_fee: Amount; + // Use STEFAN curves to determine default fees? + // If false, no fees are allowed by default. + // Can always be overridden by the frontend on a per-order basis. + use_stefan: boolean; // If the frontend does NOT specify an execution date, how long should // we tell the exchange to wait to aggregate transactions before @@ -460,7 +418,14 @@ export namespace MerchantBackend { // Merchant name corresponding to this instance. name: string; - deleted?: boolean; + // Type of the user ("business" or "individual"). + user_type: MerchantUserType; + + // Merchant public website. + website?: string; + + // Merchant logo. + logo?: ImageDataUrl; // Merchant instance this response is about ($INSTANCE) id: string; @@ -472,8 +437,63 @@ export namespace MerchantBackend { // specify the desired payment target in /order requests. Note that // front-ends do not have to support wallets selecting payment targets. payment_targets: string[]; + + // Has this instance been deleted (but not purged)? + deleted: boolean; } + //GET /private/instances/$INSTANCE + interface QueryInstancesResponse { + + // Merchant name corresponding to this instance. + name: string; + // Type of the user ("business" or "individual"). + user_type: MerchantUserType; + + // Merchant email for customer contact. + email?: string; + + // Merchant public website. + website?: string; + + // Merchant logo. + logo?: ImageDataUrl; + + // Public key of the merchant/instance, in Crockford Base32 encoding. + merchant_pub: EddsaPublicKey; + + // The merchant's physical address (to be put into contracts). + address: Location; + + // The jurisdiction under which the merchant conducts its business + // (to be put into contracts). + jurisdiction: Location; + + // Use STEFAN curves to determine default fees? + // If false, no fees are allowed by default. + // Can always be overridden by the frontend on a per-order basis. + use_stefan: boolean; + + // If the frontend does NOT specify an execution date, how long should + // we tell the exchange to wait to aggregate transactions before + // executing the wire transfer? This delay is added to the current + // time when we generate the advisory execution time for the exchange. + default_wire_transfer_delay: RelativeTime; + + // If the frontend does NOT specify a payment deadline, how long should + // offers we make be valid by default? + default_pay_delay: RelativeTime; + + // Authentication configuration. + // Does not contain the token when token auth is configured. + auth: { + method: "external" | "token"; + }; + } + // DELETE /private/instances/$INSTANCE + } + + namespace KYC { //GET /private/instances/$INSTANCE/kyc interface AccountKycRedirects { // Array of pending KYCs. @@ -513,56 +533,76 @@ export namespace MerchantBackend { exchange_http_status: number; } - //GET /private/instances/$INSTANCE - interface QueryInstancesResponse { - // The URI where the wallet will send coins. A merchant may have - // multiple accounts, thus this is an array. - accounts: MerchantAccount[]; + } - // Merchant name corresponding to this instance. - name: string; + namespace BankAccounts { - // Public key of the merchant/instance, in Crockford Base32 encoding. - merchant_pub: EddsaPublicKey; + interface AccountAddDetails { - // The merchant's physical address (to be put into contracts). - address: Location; + // payto:// URI of the account. + payto_uri: string; - // The jurisdiction under which the merchant conducts its business - // (to be put into contracts). - jurisdiction: Location; + // URL from where the merchant can download information + // about incoming wire transfers to this account. + credit_facade_url?: string; - // Maximum wire fee this instance is willing to pay. - // Can be overridden by the frontend on a per-order basis. - default_max_wire_fee: Amount; + // Credentials to use when accessing the credit facade. + // Never returned on a GET (as this may be somewhat + // sensitive data). Can be set in POST + // or PATCH requests to update (or delete) credentials. + // To really delete credentials, set them to the type: "none". + credit_facade_credentials?: FacadeCredentials; - // Default factor for wire fee amortization calculations. - // Can be overridden by the frontend on a per-order basis. - default_wire_fee_amortization: Integer; + } - // Maximum deposit fee (sum over all coins) this instance is willing to pay. - // Can be overridden by the frontend on a per-order basis. - default_max_deposit_fee: Amount; + type FacadeCredentials = + | NoFacadeCredentials + | BasicAuthFacadeCredentials; - // If the frontend does NOT specify an execution date, how long should - // we tell the exchange to wait to aggregate transactions before - // executing the wire transfer? This delay is added to the current - // time when we generate the advisory execution time for the exchange. - default_wire_transfer_delay: RelativeTime; + interface NoFacadeCredentials { + type: "none"; + } - // If the frontend does NOT specify a payment deadline, how long should - // offers we make be valid by default? - default_pay_delay: RelativeTime; + interface BasicAuthFacadeCredentials { + type: "basic"; - // Authentication configuration. - // Does not contain the token when token auth is configured. - auth: { - method: "external" | "token"; - token?: string; - }; + // Username to use to authenticate + username: string; + + // Password to use to authenticate + password: string; + } + + interface AccountAddResponse { + // Hash over the wire details (including over the salt). + h_wire: HashCode; + + // Salt used to compute h_wire. + salt: HashCode; + } + + interface AccountPatchDetails { + + // URL from where the merchant can download information + // about incoming wire transfers to this account. + credit_facade_url?: string; + + // Credentials to use when accessing the credit facade. + // Never returned on a GET (as this may be somewhat + // sensitive data). Can be set in POST + // or PATCH requests to update (or delete) credentials. + // To really delete credentials, set them to the type: "none". + credit_facade_credentials?: FacadeCredentials; } - interface MerchantAccount { + + interface AccountsSummaryResponse { + + // List of accounts that are known for the instance. + accounts: BankAccountEntry[]; + } + + interface BankAccountEntry { // payto:// URI of the account. payto_uri: string; @@ -587,7 +627,6 @@ export namespace MerchantBackend { active: boolean; } - // DELETE /private/instances/$INSTANCE } namespace Products { @@ -957,6 +996,10 @@ export namespace MerchantBackend { // high entropy to prevent adversarial claims (like it is // if the backend auto-generates one). Default is 'true'. create_token?: boolean; + + // OTP device ID to associate with the order. + // This parameter is optional. + otp_id?: string; } type Order = MinimalOrderDetail | ContractTerms; @@ -1031,9 +1074,9 @@ export namespace MerchantBackend { } } - namespace Tips { + namespace Rewards { // GET /private/reserves - interface TippingReserveStatus { + interface RewardReserveStatus { // Array of all known reserves (possibly empty!) reserves: ReserveStatusEntry[]; } @@ -1057,7 +1100,7 @@ export namespace MerchantBackend { // Amount picked up so far. pickup_amount: Amount; - // Amount approved for tips that exceeds the pickup_amount. + // Amount approved for rewards that exceeds the pickup_amount. committed_amount: Amount; // Is this reserve active (false if it was deleted but not purged) @@ -1068,7 +1111,7 @@ export namespace MerchantBackend { // Amount that the merchant promises to put into the reserve initial_balance: Amount; - // Exchange the merchant intends to use for tipping + // Exchange the merchant intends to use for reward exchange_url: string; // Desired wire method, for example "iban" or "x-taler-bank" @@ -1081,30 +1124,30 @@ export namespace MerchantBackend { // Wire accounts of the exchange where to transfer the funds. accounts: WireAccount[]; } - interface TipCreateRequest { - // Amount that the customer should be tipped + interface RewardCreateRequest { + // Amount that the customer should be reward amount: Amount; - // Justification for giving the tip + // Justification for giving the reward justification: string; - // URL that the user should be directed to after tipping, - // will be included in the tip_token. + // URL that the user should be directed to after rewarding, + // will be included in the reward_token. next_url: string; } - interface TipCreateConfirmation { - // Unique tip identifier for the tip that was created. - tip_id: HashCode; + interface RewardCreateConfirmation { + // Unique reward identifier for the reward that was created. + reward_id: HashCode; - // taler://tip URI for the tip - taler_tip_uri: string; + // taler://reward URI for the reward + taler_reward_uri: string; // URL that will directly trigger processing - // the tip when the browser is redirected to it - tip_status_url: string; + // the reward when the browser is redirected to it + reward_status_url: string; - // when does the tip expire - tip_expiration: Timestamp; + // when does the reward expire + reward_expiration: Timestamp; } interface ReserveDetail { @@ -1124,12 +1167,12 @@ export namespace MerchantBackend { // Amount picked up so far. pickup_amount: Amount; - // Amount approved for tips that exceeds the pickup_amount. + // Amount approved for rewards that exceeds the pickup_amount. committed_amount: Amount; - // Array of all tips created by this reserves (possibly empty!). + // Array of all rewards created by this reserves (possibly empty!). // Only present if asked for explicitly. - tips?: TipStatusEntry[]; + rewards?: RewardStatusEntry[]; // Is this reserve active (false if it was deleted but not purged)? active: boolean; @@ -1144,31 +1187,31 @@ export namespace MerchantBackend { exchange_url: string; } - interface TipStatusEntry { - // Unique identifier for the tip. - tip_id: HashCode; + interface RewardStatusEntry { + // Unique identifier for the reward. + reward_id: HashCode; - // Total amount of the tip that can be withdrawn. + // Total amount of the reward that can be withdrawn. total_amount: Amount; - // Human-readable reason for why the tip was granted. + // Human-readable reason for why the reward was granted. reason: string; } - interface TipDetails { - // Amount that we authorized for this tip. + interface RewardDetails { + // Amount that we authorized for this reward. total_authorized: Amount; // Amount that was picked up by the user already. total_picked_up: Amount; - // Human-readable reason given when authorizing the tip. + // Human-readable reason given when authorizing the reward. reason: string; - // Timestamp indicating when the tip is set to expire (may be in the past). + // Timestamp indicating when the reward is set to expire (may be in the past). expiration: Timestamp; - // Reserve public key from which the tip is funded. + // Reserve public key from which the reward is funded. reserve_pub: EddsaPublicKey; // Array showing the pickup operations of the wallet (possibly empty!). @@ -1239,6 +1282,63 @@ export namespace MerchantBackend { } } + namespace OTP { + interface OtpDeviceAddDetails { + // Device ID to use. + otp_device_id: string; + + // Human-readable description for the device. + otp_description: string; + + // A base64-encoded key + otp_key: string; + + // Algorithm for computing the POS confirmation. + otp_algorithm: Integer; + + // Counter for counter-based OTP devices. + otp_ctr?: Integer; + } + + interface OtpDevicePatchDetails { + // Human-readable description for the device. + otp_description: string; + + // A base64-encoded key + otp_key: string | undefined; + + // Algorithm for computing the POS confirmation. + otp_algorithm: Integer; + + // Counter for counter-based OTP devices. + otp_ctr?: Integer; + } + + interface OtpDeviceSummaryResponse { + // Array of devices that are present in our backend. + otp_devices: OtpDeviceEntry[]; + } + interface OtpDeviceEntry { + // Device identifier. + otp_device_id: string; + + // Human-readable description for the device. + device_description: string; + } + + interface OtpDeviceDetails { + // Human-readable description for the device. + device_description: string; + + // Algorithm for computing the POS confirmation. + otp_algorithm: Integer; + + // Counter for counter-based OTP devices. + otp_ctr?: Integer; + } + + + } namespace Template { interface TemplateAddDetails { // Template ID to use. @@ -1247,12 +1347,9 @@ export namespace MerchantBackend { // Human-readable description for the template. template_description: string; - // A base64-encoded key of the point-of-sale. + // OTP device ID. // This parameter is optional. - pos_key?: string; - - // Algorithm for computing the POS confirmation, 0 for none. - pos_algorithm?: number; + otp_id?: string; // Additional information in a separate template. template_contract: TemplateContractDetails; @@ -1276,12 +1373,9 @@ export namespace MerchantBackend { // Human-readable description for the template. template_description: string; - // A base64-encoded key of the point-of-sale. + // OTP device ID. // This parameter is optional. - pos_key?: string; - - // Algorithm for computing the POS confirmation, 0 for none. - pos_algorithm?: Integer; + otp_id?: string; // Additional information in a separate template. template_contract: TemplateContractDetails; @@ -1304,12 +1398,9 @@ export namespace MerchantBackend { // Human-readable description for the template. template_description: string; - // A base64-encoded key of the point-of-sale. + // OTP device ID. // This parameter is optional. - pos_key?: string; - - // Algorithm for computing the POS confirmation, 0 for none. - pos_algorithm?: Integer; + otp_id?: string; // Additional information in a separate template. template_contract: TemplateContractDetails; @@ -1424,21 +1515,6 @@ export namespace MerchantBackend { // Maximum total deposit fee accepted by the merchant for this contract max_fee: Amount; - // Maximum wire fee accepted by the merchant (customer share to be - // divided by the 'wire_fee_amortization' factor, and further reduced - // if deposit fees are below 'max_fee'). Default if missing is zero. - max_wire_fee: Amount; - - // Over how many customer transactions does the merchant expect to - // amortize wire fees on average? If the exchange's wire fee is - // above 'max_wire_fee', the difference is divided by this number - // to compute the expected customer's contribution to the wire fee. - // The customer's contribution may further be reduced by the difference - // between the 'max_fee' and the sum of the actual deposit fees. - // Optional, default value if missing is 1. 0 and negative values are - // invalid and also interpreted as 1. - wire_fee_amortization: number; - // List of products that are part of the purchase (see Product). products: Product[]; diff --git a/packages/merchant-backoffice-ui/src/hooks/backend.ts b/packages/merchant-backoffice-ui/src/hooks/backend.ts index 145a366f6..ecd34df6d 100644 --- a/packages/merchant-backoffice-ui/src/hooks/backend.ts +++ b/packages/merchant-backoffice-ui/src/hooks/backend.ts @@ -33,8 +33,9 @@ import { } from "@gnu-taler/web-util/browser"; import { useApiContext } from "@gnu-taler/web-util/browser"; + export function useMatchMutate(): ( - re: RegExp, + re?: RegExp, value?: unknown, ) => Promise<any> { const { cache, mutate } = useSWRConfig(); @@ -45,13 +46,19 @@ export function useMatchMutate(): ( ); } - return function matchRegexMutate(re: RegExp, value?: unknown) { - const allKeys = Array.from(cache.keys()); - const keys = allKeys.filter((key) => re.test(key)); - const mutations = keys.map((key) => { - return mutate(key, value, true); + return function matchRegexMutate(re?: RegExp) { + return mutate((key) => { + // evict if no key or regex === all + if (!key || !re) return true + // match string + if (typeof key === 'string' && re.test(key)) return true + // record or object have the path at [0] + if (typeof key === 'object' && re.test(key[0])) return true + //key didn't match regex + return false + }, undefined, { + revalidate: true, }); - return Promise.all(mutations); }; } @@ -106,32 +113,32 @@ interface useBackendInstanceRequestType { ) => Promise<HttpResponseOk<T>>; fetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>; reserveDetailFetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>; - tipsDetailFetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>; - multiFetcher: <T>(url: string[]) => Promise<HttpResponseOk<T>[]>; + rewardsDetailFetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>; + multiFetcher: <T>(params: [url: string[]]) => Promise<HttpResponseOk<T>[]>; orderFetcher: <T>( - endpoint: string, - paid?: YesOrNo, - refunded?: YesOrNo, - wired?: YesOrNo, - searchDate?: Date, - delta?: number, + params: [endpoint: string, + paid?: YesOrNo, + refunded?: YesOrNo, + wired?: YesOrNo, + searchDate?: Date, + delta?: number,] ) => Promise<HttpResponseOk<T>>; transferFetcher: <T>( - endpoint: string, - payto_uri?: string, - verified?: string, - position?: string, - delta?: number, + params: [endpoint: string, + payto_uri?: string, + verified?: string, + position?: string, + delta?: number,] ) => Promise<HttpResponseOk<T>>; templateFetcher: <T>( - endpoint: string, - position?: string, - delta?: number, + params: [endpoint: string, + position?: string, + delta?: number] ) => Promise<HttpResponseOk<T>>; webhookFetcher: <T>( - endpoint: string, - position?: string, - delta?: number, + params: [endpoint: string, + position?: string, + delta?: number] ) => Promise<HttpResponseOk<T>>; } interface useBackendBaseRequestType { @@ -147,7 +154,7 @@ export function useCredentialsChecker() { const { request } = useApiContext(); //check against instance details endpoint //while merchant backend doesn't have a login endpoint - return async function testLogin( + async function testLogin( instance: string, token: string, ): Promise<{ @@ -167,6 +174,7 @@ export function useCredentialsChecker() { return { valid: false, cause: ErrorType.UNEXPECTED }; } }; + return testLogin } /** @@ -212,8 +220,9 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType { const multiFetcher = useCallback( function multiFetcherImpl<T>( - endpoints: string[], + args: [endpoints: string[]], ): Promise<HttpResponseOk<T>[]> { + const [endpoints] = args return Promise.all( endpoints.map((endpoint) => requestHandler<T>(baseUrl, endpoint, { token }), @@ -232,13 +241,14 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType { const orderFetcher = useCallback( function orderFetcherImpl<T>( - endpoint: string, - paid?: YesOrNo, - refunded?: YesOrNo, - wired?: YesOrNo, - searchDate?: Date, - delta?: number, + args: [endpoint: string, + paid?: YesOrNo, + refunded?: YesOrNo, + wired?: YesOrNo, + searchDate?: Date, + delta?: number,] ): Promise<HttpResponseOk<T>> { + const [endpoint, paid, refunded, wired, searchDate, delta] = args const date_s = delta && delta < 0 && searchDate ? (searchDate.getTime() / 1000) + 1 @@ -260,7 +270,7 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType { ): Promise<HttpResponseOk<T>> { return requestHandler<T>(baseUrl, endpoint, { params: { - tips: "yes", + rewards: "yes", }, token, }); @@ -268,8 +278,8 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType { [baseUrl, token], ); - const tipsDetailFetcher = useCallback( - function tipsDetailFetcherImpl<T>( + const rewardsDetailFetcher = useCallback( + function rewardsDetailFetcherImpl<T>( endpoint: string, ): Promise<HttpResponseOk<T>> { return requestHandler<T>(baseUrl, endpoint, { @@ -284,12 +294,13 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType { const transferFetcher = useCallback( function transferFetcherImpl<T>( - endpoint: string, - payto_uri?: string, - verified?: string, - position?: string, - delta?: number, + args: [endpoint: string, + payto_uri?: string, + verified?: string, + position?: string, + delta?: number,] ): Promise<HttpResponseOk<T>> { + const [endpoint, payto_uri, verified, position, delta] = args const params: any = {}; if (payto_uri !== undefined) params.payto_uri = payto_uri; if (verified !== undefined) params.verified = verified; @@ -305,10 +316,11 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType { const templateFetcher = useCallback( function templateFetcherImpl<T>( - endpoint: string, - position?: string, - delta?: number, + args: [endpoint: string, + position?: string, + delta?: number,] ): Promise<HttpResponseOk<T>> { + const [endpoint, position, delta] = args const params: any = {}; if (delta !== undefined) { params.limit = delta; @@ -322,10 +334,11 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType { const webhookFetcher = useCallback( function webhookFetcherImpl<T>( - endpoint: string, - position?: string, - delta?: number, + args: [endpoint: string, + position?: string, + delta?: number,] ): Promise<HttpResponseOk<T>> { + const [endpoint, position, delta] = args const params: any = {}; if (delta !== undefined) { params.limit = delta; @@ -343,7 +356,7 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType { multiFetcher, orderFetcher, reserveDetailFetcher, - tipsDetailFetcher, + rewardsDetailFetcher, transferFetcher, templateFetcher, webhookFetcher, diff --git a/packages/merchant-backoffice-ui/src/hooks/bank.ts b/packages/merchant-backoffice-ui/src/hooks/bank.ts new file mode 100644 index 000000000..03b064646 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/hooks/bank.ts @@ -0,0 +1,217 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { + HttpResponse, + HttpResponseOk, + HttpResponsePaginated, + RequestError, +} from "@gnu-taler/web-util/browser"; +import { useEffect, useState } from "preact/hooks"; +import { MerchantBackend } from "../declaration.js"; +import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; +import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; + +// FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import _useSWR, { SWRHook } from "swr"; +const useSWR = _useSWR as unknown as SWRHook; + +// const MOCKED_ACCOUNTS: Record<string, MerchantBackend.BankAccounts.AccountAddDetails> = { +// "hwire1": { +// h_wire: "hwire1", +// payto_uri: "payto://fake/iban/123", +// salt: "qwe", +// }, +// "hwire2": { +// h_wire: "hwire2", +// payto_uri: "payto://fake/iban/123", +// salt: "qwe2", +// }, +// } + +export function useBankAccountAPI(): BankAccountAPI { + const mutateAll = useMatchMutate(); + const { request } = useBackendInstanceRequest(); + + const createBankAccount = async ( + data: MerchantBackend.BankAccounts.AccountAddDetails, + ): Promise<HttpResponseOk<void>> => { + // MOCKED_ACCOUNTS[data.h_wire] = data + // return Promise.resolve({ ok: true, data: undefined }); + const res = await request<void>(`/private/accounts`, { + method: "POST", + data, + }); + await mutateAll(/.*private\/accounts.*/); + return res; + }; + + const updateBankAccount = async ( + h_wire: string, + data: MerchantBackend.BankAccounts.AccountPatchDetails, + ): Promise<HttpResponseOk<void>> => { + // MOCKED_ACCOUNTS[h_wire].credit_facade_credentials = data.credit_facade_credentials + // MOCKED_ACCOUNTS[h_wire].credit_facade_url = data.credit_facade_url + // return Promise.resolve({ ok: true, data: undefined }); + const res = await request<void>(`/private/accounts/${h_wire}`, { + method: "PATCH", + data, + }); + await mutateAll(/.*private\/accounts.*/); + return res; + }; + + const deleteBankAccount = async ( + h_wire: string, + ): Promise<HttpResponseOk<void>> => { + // delete MOCKED_ACCOUNTS[h_wire] + // return Promise.resolve({ ok: true, data: undefined }); + const res = await request<void>(`/private/accounts/${h_wire}`, { + method: "DELETE", + }); + await mutateAll(/.*private\/accounts.*/); + return res; + }; + + return { + createBankAccount, + updateBankAccount, + deleteBankAccount, + }; +} + +export interface BankAccountAPI { + createBankAccount: ( + data: MerchantBackend.BankAccounts.AccountAddDetails, + ) => Promise<HttpResponseOk<void>>; + updateBankAccount: ( + id: string, + data: MerchantBackend.BankAccounts.AccountPatchDetails, + ) => Promise<HttpResponseOk<void>>; + deleteBankAccount: (id: string) => Promise<HttpResponseOk<void>>; +} + +export interface InstanceBankAccountFilter { +} + +export function useInstanceBankAccounts( + args?: InstanceBankAccountFilter, + updatePosition?: (id: string) => void, +): HttpResponsePaginated< + MerchantBackend.BankAccounts.AccountsSummaryResponse, + MerchantBackend.ErrorDetail +> { + // return { + // ok: true, + // loadMore() { }, + // loadMorePrev() { }, + // data: { + // accounts: Object.values(MOCKED_ACCOUNTS).map(e => ({ + // ...e, + // active: true, + // })) + // } + // } + const { fetcher } = useBackendInstanceRequest(); + + const [pageAfter, setPageAfter] = useState(1); + + const totalAfter = pageAfter * PAGE_SIZE; + const { + data: afterData, + error: afterError, + isValidating: loadingAfter, + } = useSWR< + HttpResponseOk<MerchantBackend.BankAccounts.AccountsSummaryResponse>, + RequestError<MerchantBackend.ErrorDetail> + >([`/private/accounts`], fetcher); + + const [lastAfter, setLastAfter] = useState< + HttpResponse< + MerchantBackend.BankAccounts.AccountsSummaryResponse, + MerchantBackend.ErrorDetail + > + >({ loading: true }); + useEffect(() => { + if (afterData) setLastAfter(afterData); + }, [afterData /*, beforeData*/]); + + if (afterError) return afterError.cause; + + // if the query returns less that we ask, then we have reach the end or beginning + const isReachingEnd = + afterData && afterData.data.accounts.length < totalAfter; + const isReachingStart = false; + + const pagination = { + isReachingEnd, + isReachingStart, + loadMore: () => { + if (!afterData || isReachingEnd) return; + if (afterData.data.accounts.length < MAX_RESULT_SIZE) { + setPageAfter(pageAfter + 1); + } else { + const from = `${afterData.data.accounts[afterData.data.accounts.length - 1] + .h_wire + }`; + if (from && updatePosition) updatePosition(from); + } + }, + loadMorePrev: () => { + }, + }; + + const accounts = !afterData ? [] : (afterData || lastAfter).data.accounts; + if (loadingAfter /* || loadingBefore */) + return { loading: true, data: { accounts } }; + if (/*beforeData &&*/ afterData) { + return { ok: true, data: { accounts }, ...pagination }; + } + return { loading: true }; +} + +export function useBankAccountDetails( + h_wire: string, +): HttpResponse< + MerchantBackend.BankAccounts.BankAccountEntry, + MerchantBackend.ErrorDetail +> { + // return { + // ok: true, + // data: { + // ...MOCKED_ACCOUNTS[h_wire], + // active: true, + // } + // } + const { fetcher } = useBackendInstanceRequest(); + + const { data, error, isValidating } = useSWR< + HttpResponseOk<MerchantBackend.BankAccounts.BankAccountEntry>, + RequestError<MerchantBackend.ErrorDetail> + >([`/private/accounts/${h_wire}`], fetcher, { + refreshInterval: 0, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }); + + if (isValidating) return { loading: true, data: data?.data }; + if (data) { + return data; + } + if (error) return error.cause; + return { loading: true }; +} diff --git a/packages/merchant-backoffice-ui/src/hooks/index.ts b/packages/merchant-backoffice-ui/src/hooks/index.ts index b77b9dea8..79b22304a 100644 --- a/packages/merchant-backoffice-ui/src/hooks/index.ts +++ b/packages/merchant-backoffice-ui/src/hooks/index.ts @@ -19,9 +19,10 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { StateUpdater, useCallback, useState } from "preact/hooks"; +import { StateUpdater, useCallback, useEffect, useState } from "preact/hooks"; import { ValueOrFunction } from "../utils/types.js"; import { useMemoryStorage } from "@gnu-taler/web-util/browser"; +import { useMatchMutate } from "./backend.js"; const calculateRootPath = () => { const rootPath = @@ -56,8 +57,22 @@ export function useBackendDefaultToken( ): [string | undefined, ((d: string | undefined) => void)] { // uncomment for testing initialValue = "secret-token:secret" as string | undefined - const { update, value } = useMemoryStorage(`backend-token`, initialValue) - return [value, update]; + const { update: setToken, value: token, reset } = useMemoryStorage(`backend-token`, initialValue) + const clearCache = useMatchMutate() + useEffect(() => { + clearCache() + }, [token]) + + function updateToken( + value: (string | undefined) + ): void { + if (value === undefined) { + reset() + } else { + setToken(value) + } + } + return [token, updateToken]; } export function useBackendInstanceToken( @@ -73,14 +88,12 @@ export function useBackendInstanceToken( function updateToken( value: (string | undefined) ): void { - console.log("seeting token", value) if (value === undefined) { reset() } else { setToken(value) } } - console.log("token", token) return [token, updateToken]; } diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.test.ts b/packages/merchant-backoffice-ui/src/hooks/instance.test.ts index f78de85dd..d15b3f6d7 100644 --- a/packages/merchant-backoffice-ui/src/hooks/instance.test.ts +++ b/packages/merchant-backoffice-ui/src/hooks/instance.test.ts @@ -113,7 +113,7 @@ describe("instance api interaction with details", () => { name: "instance_name", auth: { method: "token", - token: "not-secret", + // token: "not-secret", }, } as MerchantBackend.Instances.QueryInstancesResponse, }); @@ -154,7 +154,7 @@ describe("instance api interaction with details", () => { name: "instance_name", auth: { method: "token", - token: "secret", + // token: "secret", }, } as MerchantBackend.Instances.QueryInstancesResponse, }); @@ -190,7 +190,7 @@ describe("instance api interaction with details", () => { name: "instance_name", auth: { method: "token", - token: "not-secret", + // token: "not-secret", }, } as MerchantBackend.Instances.QueryInstancesResponse, }); diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.ts b/packages/merchant-backoffice-ui/src/hooks/instance.ts index eae65d64c..32ed30c6f 100644 --- a/packages/merchant-backoffice-ui/src/hooks/instance.ts +++ b/packages/merchant-backoffice-ui/src/hooks/instance.ts @@ -198,6 +198,7 @@ export function useInstanceDetails(): HttpResponse< revalidateOnFocus: false, revalidateOnReconnect: false, refreshWhenOffline: false, + revalidateIfStale: false, errorRetryCount: 0, errorRetryInterval: 1, shouldRetryOnError: false, @@ -211,7 +212,7 @@ export function useInstanceDetails(): HttpResponse< type KYCStatus = | { type: "ok" } - | { type: "redirect"; status: MerchantBackend.Instances.AccountKycRedirects }; + | { type: "redirect"; status: MerchantBackend.KYC.AccountKycRedirects }; export function useInstanceKYCDetails(): HttpResponse< KYCStatus, @@ -220,7 +221,7 @@ export function useInstanceKYCDetails(): HttpResponse< const { fetcher } = useBackendInstanceRequest(); const { data, error } = useSWR< - HttpResponseOk<MerchantBackend.Instances.AccountKycRedirects>, + HttpResponseOk<MerchantBackend.KYC.AccountKycRedirects>, RequestError<MerchantBackend.ErrorDetail> >([`/private/kyc`], fetcher, { refreshInterval: 60 * 1000, diff --git a/packages/merchant-backoffice-ui/src/hooks/otp.ts b/packages/merchant-backoffice-ui/src/hooks/otp.ts new file mode 100644 index 000000000..3544b4881 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/hooks/otp.ts @@ -0,0 +1,223 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { + HttpResponse, + HttpResponseOk, + HttpResponsePaginated, + RequestError, +} from "@gnu-taler/web-util/browser"; +import { useEffect, useState } from "preact/hooks"; +import { MerchantBackend } from "../declaration.js"; +import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; +import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; + +// FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import _useSWR, { SWRHook } from "swr"; +const useSWR = _useSWR as unknown as SWRHook; + +const MOCKED_DEVICES: Record<string, MerchantBackend.OTP.OtpDeviceAddDetails> = { + "1": { + otp_description: "first device", + otp_algorithm: 1, + otp_device_id: "1", + otp_key: "123", + }, + "2": { + otp_description: "second device", + otp_algorithm: 0, + otp_device_id: "2", + otp_key: "456", + } +} + +export function useOtpDeviceAPI(): OtpDeviceAPI { + const mutateAll = useMatchMutate(); + const { request } = useBackendInstanceRequest(); + + const createOtpDevice = async ( + data: MerchantBackend.OTP.OtpDeviceAddDetails, + ): Promise<HttpResponseOk<void>> => { + // MOCKED_DEVICES[data.otp_device_id] = data + // return Promise.resolve({ ok: true, data: undefined }); + const res = await request<void>(`/private/otp-devices`, { + method: "POST", + data, + }); + await mutateAll(/.*private\/otp-devices.*/); + return res; + }; + + const updateOtpDevice = async ( + deviceId: string, + data: MerchantBackend.OTP.OtpDevicePatchDetails, + ): Promise<HttpResponseOk<void>> => { + // MOCKED_DEVICES[deviceId].otp_algorithm = data.otp_algorithm + // MOCKED_DEVICES[deviceId].otp_ctr = data.otp_ctr + // MOCKED_DEVICES[deviceId].otp_device_description = data.otp_device_description + // MOCKED_DEVICES[deviceId].otp_key = data.otp_key + // return Promise.resolve({ ok: true, data: undefined }); + const res = await request<void>(`/private/otp-devices/${deviceId}`, { + method: "PATCH", + data, + }); + await mutateAll(/.*private\/otp-devices.*/); + return res; + }; + + const deleteOtpDevice = async ( + deviceId: string, + ): Promise<HttpResponseOk<void>> => { + // delete MOCKED_DEVICES[deviceId] + // return Promise.resolve({ ok: true, data: undefined }); + const res = await request<void>(`/private/otp-devices/${deviceId}`, { + method: "DELETE", + }); + await mutateAll(/.*private\/otp-devices.*/); + return res; + }; + + return { + createOtpDevice, + updateOtpDevice, + deleteOtpDevice, + }; +} + +export interface OtpDeviceAPI { + createOtpDevice: ( + data: MerchantBackend.OTP.OtpDeviceAddDetails, + ) => Promise<HttpResponseOk<void>>; + updateOtpDevice: ( + id: string, + data: MerchantBackend.OTP.OtpDevicePatchDetails, + ) => Promise<HttpResponseOk<void>>; + deleteOtpDevice: (id: string) => Promise<HttpResponseOk<void>>; +} + +export interface InstanceOtpDeviceFilter { +} + +export function useInstanceOtpDevices( + args?: InstanceOtpDeviceFilter, + updatePosition?: (id: string) => void, +): HttpResponsePaginated< + MerchantBackend.OTP.OtpDeviceSummaryResponse, + MerchantBackend.ErrorDetail +> { + // return { + // ok: true, + // loadMore: () => { }, + // loadMorePrev: () => { }, + // data: { + // otp_devices: Object.values(MOCKED_DEVICES).map(d => ({ + // device_description: d.otp_device_description, + // otp_device_id: d.otp_device_id + // })) + // } + // } + + const { fetcher } = useBackendInstanceRequest(); + + const [pageAfter, setPageAfter] = useState(1); + + const totalAfter = pageAfter * PAGE_SIZE; + const { + data: afterData, + error: afterError, + isValidating: loadingAfter, + } = useSWR< + HttpResponseOk<MerchantBackend.OTP.OtpDeviceSummaryResponse>, + RequestError<MerchantBackend.ErrorDetail> + >([`/private/otp-devices`], fetcher); + + const [lastAfter, setLastAfter] = useState< + HttpResponse< + MerchantBackend.OTP.OtpDeviceSummaryResponse, + MerchantBackend.ErrorDetail + > + >({ loading: true }); + useEffect(() => { + if (afterData) setLastAfter(afterData); + }, [afterData /*, beforeData*/]); + + if (afterError) return afterError.cause; + + // if the query returns less that we ask, then we have reach the end or beginning + const isReachingEnd = + afterData && afterData.data.otp_devices.length < totalAfter; + const isReachingStart = false; + + const pagination = { + isReachingEnd, + isReachingStart, + loadMore: () => { + if (!afterData || isReachingEnd) return; + if (afterData.data.otp_devices.length < MAX_RESULT_SIZE) { + setPageAfter(pageAfter + 1); + } else { + const from = `${afterData.data.otp_devices[afterData.data.otp_devices.length - 1] + .otp_device_id + }`; + if (from && updatePosition) updatePosition(from); + } + }, + loadMorePrev: () => { + }, + }; + + const otp_devices = !afterData ? [] : (afterData || lastAfter).data.otp_devices; + if (loadingAfter /* || loadingBefore */) + return { loading: true, data: { otp_devices } }; + if (/*beforeData &&*/ afterData) { + return { ok: true, data: { otp_devices }, ...pagination }; + } + return { loading: true }; +} + +export function useOtpDeviceDetails( + deviceId: string, +): HttpResponse< + MerchantBackend.OTP.OtpDeviceDetails, + MerchantBackend.ErrorDetail +> { + // return { + // ok: true, + // data: { + // device_description: MOCKED_DEVICES[deviceId].otp_device_description, + // otp_algorithm: MOCKED_DEVICES[deviceId].otp_algorithm, + // otp_ctr: MOCKED_DEVICES[deviceId].otp_ctr + // } + // } + const { fetcher } = useBackendInstanceRequest(); + + const { data, error, isValidating } = useSWR< + HttpResponseOk<MerchantBackend.OTP.OtpDeviceDetails>, + RequestError<MerchantBackend.ErrorDetail> + >([`/private/otp-devices/${deviceId}`], fetcher, { + refreshInterval: 0, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }); + + if (isValidating) return { loading: true, data: data?.data }; + if (data) { + return data; + } + if (error) return error.cause; + return { loading: true }; +} diff --git a/packages/merchant-backoffice-ui/src/hooks/reserve.test.ts b/packages/merchant-backoffice-ui/src/hooks/reserve.test.ts index d2831ecff..b3eecd754 100644 --- a/packages/merchant-backoffice-ui/src/hooks/reserve.test.ts +++ b/packages/merchant-backoffice-ui/src/hooks/reserve.test.ts @@ -25,16 +25,16 @@ import { useInstanceReserves, useReserveDetails, useReservesAPI, - useTipDetails, + useRewardDetails, } from "./reserves.js"; import { ApiMockEnvironment } from "./testing.js"; import { - API_AUTHORIZE_TIP, - API_AUTHORIZE_TIP_FOR_RESERVE, + API_AUTHORIZE_REWARD, + API_AUTHORIZE_REWARD_FOR_RESERVE, API_CREATE_RESERVE, API_DELETE_RESERVE, API_GET_RESERVE_BY_ID, - API_GET_TIP_BY_ID, + API_GET_REWARD_BY_ID, API_LIST_RESERVES, } from "./urls.js"; import * as tests from "@gnu-taler/web-util/testing"; @@ -48,7 +48,7 @@ describe("reserve api interaction with listing", () => { reserves: [ { reserve_pub: "11", - } as MerchantBackend.Tips.ReserveStatusEntry, + } as MerchantBackend.Rewards.ReserveStatusEntry, ], }, }); @@ -89,10 +89,10 @@ describe("reserve api interaction with listing", () => { reserves: [ { reserve_pub: "11", - } as MerchantBackend.Tips.ReserveStatusEntry, + } as MerchantBackend.Rewards.ReserveStatusEntry, { reserve_pub: "22", - } as MerchantBackend.Tips.ReserveStatusEntry, + } as MerchantBackend.Rewards.ReserveStatusEntry, ], }, }); @@ -115,10 +115,10 @@ describe("reserve api interaction with listing", () => { reserves: [ { reserve_pub: "11", - } as MerchantBackend.Tips.ReserveStatusEntry, + } as MerchantBackend.Rewards.ReserveStatusEntry, { reserve_pub: "22", - } as MerchantBackend.Tips.ReserveStatusEntry, + } as MerchantBackend.Rewards.ReserveStatusEntry, ], }); }, @@ -138,13 +138,13 @@ describe("reserve api interaction with listing", () => { reserves: [ { reserve_pub: "11", - } as MerchantBackend.Tips.ReserveStatusEntry, + } as MerchantBackend.Rewards.ReserveStatusEntry, { reserve_pub: "22", - } as MerchantBackend.Tips.ReserveStatusEntry, + } as MerchantBackend.Rewards.ReserveStatusEntry, { reserve_pub: "33", - } as MerchantBackend.Tips.ReserveStatusEntry, + } as MerchantBackend.Rewards.ReserveStatusEntry, ], }, }); @@ -182,10 +182,10 @@ describe("reserve api interaction with listing", () => { reserves: [ { reserve_pub: "22", - } as MerchantBackend.Tips.ReserveStatusEntry, + } as MerchantBackend.Rewards.ReserveStatusEntry, { reserve_pub: "33", - } as MerchantBackend.Tips.ReserveStatusEntry, + } as MerchantBackend.Rewards.ReserveStatusEntry, ], }, }); @@ -213,16 +213,16 @@ describe("reserve api interaction with listing", () => { }); describe("reserve api interaction with details", () => { - it("should evict cache when adding a tip for a specific reserve", async () => { + it("should evict cache when adding a reward for a specific reserve", async () => { const env = new ApiMockEnvironment(); env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { response: { accounts: [{ payto_uri: "payto://here" }], - tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }], - } as MerchantBackend.Tips.ReserveDetail, + rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }], + } as MerchantBackend.Rewards.ReserveDetail, qparam: { - tips: "yes", + rewards: "yes", }, }); @@ -246,37 +246,37 @@ describe("reserve api interaction with details", () => { if (!query.ok) return; expect(query.data).deep.equals({ accounts: [{ payto_uri: "payto://here" }], - tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }], + rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }], }); - env.addRequestExpectation(API_AUTHORIZE_TIP_FOR_RESERVE("11"), { + env.addRequestExpectation(API_AUTHORIZE_REWARD_FOR_RESERVE("11"), { request: { amount: "USD:12", justification: "not", next_url: "http://taler.net", }, response: { - tip_id: "id2", - taler_tip_uri: "uri", - tip_expiration: { t_s: 1 }, - tip_status_url: "url", + reward_id: "id2", + taler_reward_uri: "uri", + reward_expiration: { t_s: 1 }, + reward_status_url: "url", }, }); env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { response: { accounts: [{ payto_uri: "payto://here" }], - tips: [ - { reason: "why?", tip_id: "id1", total_amount: "USD:10" }, - { reason: "not", tip_id: "id2", total_amount: "USD:12" }, + rewards: [ + { reason: "why?", reward_id: "id1", total_amount: "USD:10" }, + { reason: "not", reward_id: "id2", total_amount: "USD:12" }, ], - } as MerchantBackend.Tips.ReserveDetail, + } as MerchantBackend.Rewards.ReserveDetail, qparam: { - tips: "yes", + rewards: "yes", }, }); - api.authorizeTipReserve("11", { + api.authorizeRewardReserve("11", { amount: "USD:12", justification: "not", next_url: "http://taler.net", @@ -294,9 +294,9 @@ describe("reserve api interaction with details", () => { expect(query.data).deep.equals({ accounts: [{ payto_uri: "payto://here" }], - tips: [ - { reason: "why?", tip_id: "id1", total_amount: "USD:10" }, - { reason: "not", tip_id: "id2", total_amount: "USD:12" }, + rewards: [ + { reason: "why?", reward_id: "id1", total_amount: "USD:10" }, + { reason: "not", reward_id: "id2", total_amount: "USD:12" }, ], }); }, @@ -308,16 +308,16 @@ describe("reserve api interaction with details", () => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); }); - it("should evict cache when adding a tip for a random reserve", async () => { + it("should evict cache when adding a reward for a random reserve", async () => { const env = new ApiMockEnvironment(); env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { response: { accounts: [{ payto_uri: "payto://here" }], - tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }], - } as MerchantBackend.Tips.ReserveDetail, + rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }], + } as MerchantBackend.Rewards.ReserveDetail, qparam: { - tips: "yes", + rewards: "yes", }, }); @@ -341,37 +341,37 @@ describe("reserve api interaction with details", () => { if (!query.ok) return; expect(query.data).deep.equals({ accounts: [{ payto_uri: "payto://here" }], - tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }], + rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }], }); - env.addRequestExpectation(API_AUTHORIZE_TIP, { + env.addRequestExpectation(API_AUTHORIZE_REWARD, { request: { amount: "USD:12", justification: "not", next_url: "http://taler.net", }, response: { - tip_id: "id2", - taler_tip_uri: "uri", - tip_expiration: { t_s: 1 }, - tip_status_url: "url", + reward_id: "id2", + taler_reward_uri: "uri", + reward_expiration: { t_s: 1 }, + reward_status_url: "url", }, }); env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { response: { accounts: [{ payto_uri: "payto://here" }], - tips: [ - { reason: "why?", tip_id: "id1", total_amount: "USD:10" }, - { reason: "not", tip_id: "id2", total_amount: "USD:12" }, + rewards: [ + { reason: "why?", reward_id: "id1", total_amount: "USD:10" }, + { reason: "not", reward_id: "id2", total_amount: "USD:12" }, ], - } as MerchantBackend.Tips.ReserveDetail, + } as MerchantBackend.Rewards.ReserveDetail, qparam: { - tips: "yes", + rewards: "yes", }, }); - api.authorizeTip({ + api.authorizeReward({ amount: "USD:12", justification: "not", next_url: "http://taler.net", @@ -387,9 +387,9 @@ describe("reserve api interaction with details", () => { expect(query.data).deep.equals({ accounts: [{ payto_uri: "payto://here" }], - tips: [ - { reason: "why?", tip_id: "id1", total_amount: "USD:10" }, - { reason: "not", tip_id: "id2", total_amount: "USD:12" }, + rewards: [ + { reason: "why?", reward_id: "id1", total_amount: "USD:10" }, + { reason: "not", reward_id: "id2", total_amount: "USD:12" }, ], }); }, @@ -402,15 +402,15 @@ describe("reserve api interaction with details", () => { }); }); -describe("reserve api interaction with tip details", () => { - it("should list tips", async () => { +describe("reserve api interaction with reward details", () => { + it("should list rewards", async () => { const env = new ApiMockEnvironment(); - env.addRequestExpectation(API_GET_TIP_BY_ID("11"), { + env.addRequestExpectation(API_GET_REWARD_BY_ID("11"), { response: { total_picked_up: "USD:12", reason: "not", - } as MerchantBackend.Tips.TipDetails, + } as MerchantBackend.Rewards.RewardDetails, qparam: { pickups: "yes", }, @@ -418,7 +418,7 @@ describe("reserve api interaction with tip details", () => { const hookBehavior = await tests.hookBehaveLikeThis( () => { - const query = useTipDetails("11"); + const query = useRewardDetails("11"); return { query }; }, {}, diff --git a/packages/merchant-backoffice-ui/src/hooks/reserves.ts b/packages/merchant-backoffice-ui/src/hooks/reserves.ts index bb55b2474..b719bfbe6 100644 --- a/packages/merchant-backoffice-ui/src/hooks/reserves.ts +++ b/packages/merchant-backoffice-ui/src/hooks/reserves.ts @@ -31,11 +31,11 @@ export function useReservesAPI(): ReserveMutateAPI { const { request } = useBackendInstanceRequest(); const createReserve = async ( - data: MerchantBackend.Tips.ReserveCreateRequest, + data: MerchantBackend.Rewards.ReserveCreateRequest, ): Promise< - HttpResponseOk<MerchantBackend.Tips.ReserveCreateConfirmation> + HttpResponseOk<MerchantBackend.Rewards.ReserveCreateConfirmation> > => { - const res = await request<MerchantBackend.Tips.ReserveCreateConfirmation>( + const res = await request<MerchantBackend.Rewards.ReserveCreateConfirmation>( `/private/reserves`, { method: "POST", @@ -49,12 +49,12 @@ export function useReservesAPI(): ReserveMutateAPI { return res; }; - const authorizeTipReserve = async ( + const authorizeRewardReserve = async ( pub: string, - data: MerchantBackend.Tips.TipCreateRequest, - ): Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>> => { - const res = await request<MerchantBackend.Tips.TipCreateConfirmation>( - `/private/reserves/${pub}/authorize-tip`, + data: MerchantBackend.Rewards.RewardCreateRequest, + ): Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>> => { + const res = await request<MerchantBackend.Rewards.RewardCreateConfirmation>( + `/private/reserves/${pub}/authorize-reward`, { method: "POST", data, @@ -67,11 +67,11 @@ export function useReservesAPI(): ReserveMutateAPI { return res; }; - const authorizeTip = async ( - data: MerchantBackend.Tips.TipCreateRequest, - ): Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>> => { - const res = await request<MerchantBackend.Tips.TipCreateConfirmation>( - `/private/tips`, + const authorizeReward = async ( + data: MerchantBackend.Rewards.RewardCreateRequest, + ): Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>> => { + const res = await request<MerchantBackend.Rewards.RewardCreateConfirmation>( + `/private/rewards`, { method: "POST", data, @@ -97,33 +97,33 @@ export function useReservesAPI(): ReserveMutateAPI { return res; }; - return { createReserve, authorizeTip, authorizeTipReserve, deleteReserve }; + return { createReserve, authorizeReward, authorizeRewardReserve, deleteReserve }; } export interface ReserveMutateAPI { createReserve: ( - data: MerchantBackend.Tips.ReserveCreateRequest, - ) => Promise<HttpResponseOk<MerchantBackend.Tips.ReserveCreateConfirmation>>; - authorizeTipReserve: ( + data: MerchantBackend.Rewards.ReserveCreateRequest, + ) => Promise<HttpResponseOk<MerchantBackend.Rewards.ReserveCreateConfirmation>>; + authorizeRewardReserve: ( id: string, - data: MerchantBackend.Tips.TipCreateRequest, - ) => Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>>; - authorizeTip: ( - data: MerchantBackend.Tips.TipCreateRequest, - ) => Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>>; + data: MerchantBackend.Rewards.RewardCreateRequest, + ) => Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>>; + authorizeReward: ( + data: MerchantBackend.Rewards.RewardCreateRequest, + ) => Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>>; deleteReserve: ( id: string, ) => Promise<HttpResponse<void, MerchantBackend.ErrorDetail>>; } export function useInstanceReserves(): HttpResponse< - MerchantBackend.Tips.TippingReserveStatus, + MerchantBackend.Rewards.RewardReserveStatus, MerchantBackend.ErrorDetail > { const { fetcher } = useBackendInstanceRequest(); const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Tips.TippingReserveStatus>, + HttpResponseOk<MerchantBackend.Rewards.RewardReserveStatus>, RequestError<MerchantBackend.ErrorDetail> >([`/private/reserves`], fetcher); @@ -136,13 +136,13 @@ export function useInstanceReserves(): HttpResponse< export function useReserveDetails( reserveId: string, ): HttpResponse< - MerchantBackend.Tips.ReserveDetail, + MerchantBackend.Rewards.ReserveDetail, MerchantBackend.ErrorDetail > { const { reserveDetailFetcher } = useBackendInstanceRequest(); const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Tips.ReserveDetail>, + HttpResponseOk<MerchantBackend.Rewards.ReserveDetail>, RequestError<MerchantBackend.ErrorDetail> >([`/private/reserves/${reserveId}`], reserveDetailFetcher, { refreshInterval: 0, @@ -158,15 +158,15 @@ export function useReserveDetails( return { loading: true }; } -export function useTipDetails( - tipId: string, -): HttpResponse<MerchantBackend.Tips.TipDetails, MerchantBackend.ErrorDetail> { - const { tipsDetailFetcher } = useBackendInstanceRequest(); +export function useRewardDetails( + rewardId: string, +): HttpResponse<MerchantBackend.Rewards.RewardDetails, MerchantBackend.ErrorDetail> { + const { rewardsDetailFetcher } = useBackendInstanceRequest(); const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Tips.TipDetails>, + HttpResponseOk<MerchantBackend.Rewards.RewardDetails>, RequestError<MerchantBackend.ErrorDetail> - >([`/private/tips/${tipId}`], tipsDetailFetcher, { + >([`/private/rewards/${rewardId}`], rewardsDetailFetcher, { refreshInterval: 0, refreshWhenHidden: false, revalidateOnFocus: false, diff --git a/packages/merchant-backoffice-ui/src/hooks/urls.ts b/packages/merchant-backoffice-ui/src/hooks/urls.ts index 6b339c05a..00c5e95af 100644 --- a/packages/merchant-backoffice-ui/src/hooks/urls.ts +++ b/packages/merchant-backoffice-ui/src/hooks/urls.ts @@ -139,15 +139,15 @@ export const API_DELETE_PRODUCT = (id: string): Query<unknown, unknown> => ({ //////////////////// export const API_CREATE_RESERVE: Query< - MerchantBackend.Tips.ReserveCreateRequest, - MerchantBackend.Tips.ReserveCreateConfirmation + MerchantBackend.Rewards.ReserveCreateRequest, + MerchantBackend.Rewards.ReserveCreateConfirmation > = { method: "POST", url: "http://backend/instances/default/private/reserves", }; export const API_LIST_RESERVES: Query< unknown, - MerchantBackend.Tips.TippingReserveStatus + MerchantBackend.Rewards.RewardReserveStatus > = { method: "GET", url: "http://backend/instances/default/private/reserves", @@ -155,34 +155,34 @@ export const API_LIST_RESERVES: Query< export const API_GET_RESERVE_BY_ID = ( pub: string, -): Query<unknown, MerchantBackend.Tips.ReserveDetail> => ({ +): Query<unknown, MerchantBackend.Rewards.ReserveDetail> => ({ method: "GET", url: `http://backend/instances/default/private/reserves/${pub}`, }); -export const API_GET_TIP_BY_ID = ( +export const API_GET_REWARD_BY_ID = ( pub: string, -): Query<unknown, MerchantBackend.Tips.TipDetails> => ({ +): Query<unknown, MerchantBackend.Rewards.RewardDetails> => ({ method: "GET", - url: `http://backend/instances/default/private/tips/${pub}`, + url: `http://backend/instances/default/private/rewards/${pub}`, }); -export const API_AUTHORIZE_TIP_FOR_RESERVE = ( +export const API_AUTHORIZE_REWARD_FOR_RESERVE = ( pub: string, ): Query< - MerchantBackend.Tips.TipCreateRequest, - MerchantBackend.Tips.TipCreateConfirmation + MerchantBackend.Rewards.RewardCreateRequest, + MerchantBackend.Rewards.RewardCreateConfirmation > => ({ method: "POST", - url: `http://backend/instances/default/private/reserves/${pub}/authorize-tip`, + url: `http://backend/instances/default/private/reserves/${pub}/authorize-reward`, }); -export const API_AUTHORIZE_TIP: Query< - MerchantBackend.Tips.TipCreateRequest, - MerchantBackend.Tips.TipCreateConfirmation +export const API_AUTHORIZE_REWARD: Query< + MerchantBackend.Rewards.RewardCreateRequest, + MerchantBackend.Rewards.RewardCreateConfirmation > = { method: "POST", - url: `http://backend/instances/default/private/tips`, + url: `http://backend/instances/default/private/rewards`, }; export const API_DELETE_RESERVE = (id: string): Query<unknown, unknown> => ({ @@ -211,7 +211,7 @@ export const API_GET_INSTANCE_BY_ID = ( export const API_GET_INSTANCE_KYC_BY_ID = ( id: string, -): Query<unknown, MerchantBackend.Instances.AccountKycRedirects> => ({ +): Query<unknown, MerchantBackend.KYC.AccountKycRedirects> => ({ method: "GET", url: `http://backend/management/instances/${id}/kyc`, }); @@ -263,7 +263,7 @@ export const API_GET_CURRENT_INSTANCE: Query< export const API_GET_CURRENT_INSTANCE_KYC: Query< unknown, - MerchantBackend.Instances.AccountKycRedirects + MerchantBackend.KYC.AccountKycRedirects > = { method: "GET", url: `http://backend/instances/default/private/kyc`, diff --git a/packages/merchant-backoffice-ui/src/hooks/useSettings.ts b/packages/merchant-backoffice-ui/src/hooks/useSettings.ts index 5c0932f27..7dee9f896 100644 --- a/packages/merchant-backoffice-ui/src/hooks/useSettings.ts +++ b/packages/merchant-backoffice-ui/src/hooks/useSettings.ts @@ -19,6 +19,9 @@ import { Codec, buildCodecForObject, codecForBoolean, + codecForConstString, + codecForEither, + codecForString, } from "@gnu-taler/taler-util"; function parse_json_or_undefined<T>(str: string | undefined): T | undefined { @@ -31,29 +34,49 @@ function parse_json_or_undefined<T>(str: string | undefined): T | undefined { } export interface Settings { - advanceOrderMode: boolean + advanceOrderMode: boolean; + dateFormat: "ymd" | "dmy" | "mdy"; } const defaultSettings: Settings = { advanceOrderMode: false, + dateFormat: "ymd", } export const codecForSettings = (): Codec<Settings> => buildCodecForObject<Settings>() .property("advanceOrderMode", codecForBoolean()) + .property("dateFormat", codecForEither( + codecForConstString("ymd"), + codecForConstString("dmy"), + codecForConstString("mdy"), + )) .build("Settings"); const SETTINGS_KEY = buildStorageKey("merchant-settings", codecForSettings()); export function useSettings(): [ Readonly<Settings>, - <T extends keyof Settings>(key: T, value: Settings[T]) => void, + (s: Settings) => void, ] { - const { value, update } = useLocalStorage(SETTINGS_KEY); + const { value, update } = useLocalStorage(SETTINGS_KEY, defaultSettings); - const parsed: Settings = value ?? defaultSettings; - function updateField<T extends keyof Settings>(k: T, v: Settings[T]) { - update({ ...parsed, [k]: v }); + // const parsed: Settings = value ?? defaultSettings; + // function updateField<T extends keyof Settings>(k: T, v: Settings[T]) { + // const next = { ...parsed, [k]: v } + // update(next); + // } + return [value, update]; +} + +export function dateFormatForSettings(s: Settings): string { + switch (s.dateFormat) { + case "ymd": return "yyyy/MM/dd" + case "dmy": return "dd/MM/yyyy" + case "mdy": return "MM/dd/yyyy" } - return [parsed, updateField]; } + +export function datetimeFormatForSettings(s: Settings): string { + return dateFormatForSettings(s) + " HH:mm:ss" +}
\ No newline at end of file diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx index 14e2fcb46..a8108251d 100644 --- a/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx @@ -19,7 +19,6 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { Amounts } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { useState } from "preact/hooks"; @@ -29,9 +28,8 @@ import { FormProvider, } from "../../../components/form/FormProvider.js"; import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js"; -import { SetTokenNewInstanceModal } from "../../../components/modal/index.js"; import { MerchantBackend } from "../../../declaration.js"; -import { INSTANCE_ID_REGEX, PAYTO_REGEX } from "../../../utils/constants.js"; +import { INSTANCE_ID_REGEX } from "../../../utils/constants.js"; import { undefinedIfEmpty } from "../../../utils/table.js"; export type Entity = MerchantBackend.Instances.InstanceConfigurationMessage & { @@ -47,19 +45,19 @@ interface Props { function with_defaults(id?: string): Partial<Entity> { return { id, - accounts: [], + // accounts: [], user_type: "business", + use_stefan: false, default_pay_delay: { d_us: 2 * 1000 * 60 * 60 * 1000 }, // two hours - default_wire_fee_amortization: 1, default_wire_transfer_delay: { d_us: 1000 * 2 * 60 * 60 * 24 * 1000 }, // two days }; } export function CreatePage({ onCreate, onBack, forceId }: Props): VNode { const [value, valueHandler] = useState(with_defaults(forceId)); - const [isTokenSet, updateIsTokenSet] = useState<boolean>(false); - const [isTokenDialogActive, updateIsTokenDialogActive] = - useState<boolean>(false); + // const [isTokenSet, updateIsTokenSet] = useState<boolean>(false); + // const [isTokenDialogActive, updateIsTokenDialogActive] = + // useState<boolean>(false); const { i18n } = useTranslationContext(); @@ -67,42 +65,24 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode { id: !value.id ? i18n.str`required` : !INSTANCE_ID_REGEX.test(value.id) - ? i18n.str`is not valid` - : undefined, + ? i18n.str`is not valid` + : undefined, name: !value.name ? i18n.str`required` : undefined, user_type: !value.user_type ? i18n.str`required` : value.user_type !== "business" && value.user_type !== "individual" - ? i18n.str`should be business or individual` - : undefined, - accounts: - !value.accounts || !value.accounts.length - ? i18n.str`required` - : undefinedIfEmpty( - value.accounts.map((p) => { - return !PAYTO_REGEX.test(p.payto_uri) - ? i18n.str`is not valid` - : undefined; - }), - ), - default_max_deposit_fee: !value.default_max_deposit_fee - ? i18n.str`required` - : !Amounts.parse(value.default_max_deposit_fee) - ? i18n.str`invalid format` - : undefined, - default_max_wire_fee: !value.default_max_wire_fee - ? i18n.str`required` - : !Amounts.parse(value.default_max_wire_fee) - ? i18n.str`invalid format` - : undefined, - default_wire_fee_amortization: - value.default_wire_fee_amortization === undefined - ? i18n.str`required` - : isNaN(value.default_wire_fee_amortization) - ? i18n.str`is not a number` - : value.default_wire_fee_amortization < 1 - ? i18n.str`must be 1 or greater` + ? i18n.str`should be business or individual` : undefined, + // accounts: + // !value.accounts || !value.accounts.length + // ? i18n.str`required` + // : undefinedIfEmpty( + // value.accounts.map((p) => { + // return !PAYTO_REGEX.test(p.payto_uri) + // ? i18n.str`is not valid` + // : undefined; + // }), + // ), default_pay_delay: !value.default_pay_delay ? i18n.str`required` : undefined, @@ -129,12 +109,12 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode { const submit = (): Promise<void> => { // use conversion instead of this - const newToken = value.auth_token; - value.auth_token = undefined; - value.auth = - newToken === null || newToken === undefined - ? { method: "external" } - : { method: "token", token: `secret-token:${newToken}` }; + // const newToken = value.auth_token; + // value.auth_token = undefined; + value.auth = { method: "external" } + // newToken === null || newToken === undefined + // ? { method: "external" } + // : { method: "token", token: `secret-token:${newToken}` }; if (!value.address) value.address = {}; if (!value.jurisdiction) value.jurisdiction = {}; // remove above use conversion @@ -142,16 +122,16 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode { return onCreate(value as Entity); }; - function updateToken(token: string | null) { - valueHandler((old) => ({ - ...old, - auth_token: token === null ? undefined : token, - })); - } + // function updateToken(token: string | null) { + // valueHandler((old) => ({ + // ...old, + // auth_token: token === null ? undefined : token, + // })); + // } return ( <div> - <div class="columns"> + {/* <div class="columns"> <div class="column" /> <div class="column is-four-fifths"> {isTokenDialogActive && ( @@ -174,9 +154,9 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode { )} </div> <div class="column" /> - </div> + </div> */} - <section class="hero is-hero-bar"> + {/* <section class="hero is-hero-bar"> <div class="hero-body"> <div class="level"> <div class="level-item has-text-centered"> @@ -186,8 +166,8 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode { !isTokenSet ? "button is-danger has-tooltip-bottom" : !value.auth_token - ? "button has-tooltip-bottom" - : "button is-info has-tooltip-bottom" + ? "button has-tooltip-bottom" + : "button is-info has-tooltip-bottom" } data-tooltip={i18n.str`change authorization configuration`} onClick={() => updateIsTokenDialogActive(true)} @@ -228,7 +208,7 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode { </div> </div> </div> - </section> + </section> */} <section class="section is-main-section"> <div class="columns"> @@ -250,7 +230,7 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode { )} <AsyncButton onClick={submit} - disabled={!isTokenSet || hasErrors} + disabled={hasErrors} data-tooltip={ hasErrors ? i18n.str`Need to complete marked fields and choose authorization method` diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx new file mode 100644 index 000000000..3336c53a4 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx @@ -0,0 +1,28 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { CreatePage as TestedComponent } from "./CreatePage.js"; + +export default { + title: "Pages/Accounts/Create", + component: TestedComponent, +}; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx new file mode 100644 index 000000000..6e4786a47 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx @@ -0,0 +1,173 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; +import { + FormErrors, + FormProvider, +} from "../../../../components/form/FormProvider.js"; +import { Input } from "../../../../components/form/Input.js"; +import { InputPaytoForm } from "../../../../components/form/InputPaytoForm.js"; +import { InputSelector } from "../../../../components/form/InputSelector.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { undefinedIfEmpty } from "../../../../utils/table.js"; + +type Entity = MerchantBackend.BankAccounts.AccountAddDetails & { repeatPassword: string }; + +interface Props { + onCreate: (d: Entity) => Promise<void>; + onBack?: () => void; +} + +const accountAuthType = ["none", "basic"]; + +function isValidURL(s: string): boolean { + try { + const u = new URL(s) + return true; + } catch (e) { + return false; + } +} + +export function CreatePage({ onCreate, onBack }: Props): VNode { + const { i18n } = useTranslationContext(); + + const [state, setState] = useState<Partial<Entity>>({}); + const errors: FormErrors<Entity> = { + payto_uri: !state.payto_uri ? i18n.str`required` : undefined, + + credit_facade_credentials: !state.credit_facade_credentials + ? undefined + : undefinedIfEmpty({ + username: + state.credit_facade_credentials.type === "basic" && !state.credit_facade_credentials.username + ? i18n.str`required` + : undefined, + password: + state.credit_facade_credentials.type === "basic" && !state.credit_facade_credentials.password + ? i18n.str`required` + : undefined, + }), + credit_facade_url: !state.credit_facade_url + ? undefined + : !isValidURL(state.credit_facade_url) ? i18n.str`not valid url` + : undefined, + repeatPassword: + !state.credit_facade_credentials + ? undefined + : state.credit_facade_credentials.type === "basic" && (!state.credit_facade_credentials.password || state.credit_facade_credentials.password !== state.repeatPassword) + ? i18n.str`is not the same` + : undefined, + }; + + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + + const submitForm = () => { + if (hasErrors) return Promise.reject(); + delete state.repeatPassword + return onCreate(state as any); + }; + + return ( + <div> + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <FormProvider + object={state} + valueHandler={setState} + errors={errors} + > + <InputPaytoForm<Entity> + name="payto_uri" + label={i18n.str`Account`} + /> + <Input<Entity> + name="credit_facade_url" + label={i18n.str`Account info URL`} + help="https://bank.com" + expand + tooltip={i18n.str`From where the merchant can download information about incoming wire transfers to this account`} + /> + <InputSelector + name="credit_facade_credentials.type" + label={i18n.str`Auth type`} + tooltip={i18n.str`Choose the authentication type for the account info URL`} + values={accountAuthType} + toStr={(str) => { + if (str === "none") return "Without authentication"; + return "Username and password"; + }} + /> + {state.credit_facade_credentials?.type === "basic" ? ( + <Fragment> + <Input + name="credit_facade_credentials.username" + label={i18n.str`Username`} + tooltip={i18n.str`Username to access the account information.`} + /> + <Input + name="credit_facade_credentials.password" + inputType="password" + label={i18n.str`Password`} + tooltip={i18n.str`Password to access the account information.`} + /> + <Input + name="repeatPassword" + inputType="password" + label={i18n.str`Repeat password`} + /> + </Fragment> + ) : undefined} + </FormProvider> + + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <AsyncButton + disabled={hasErrors} + data-tooltip={ + hasErrors + ? i18n.str`Need to complete marked fields` + : "confirm operation" + } + onClick={submitForm} + > + <i18n.Translate>Confirm</i18n.Translate> + </AsyncButton> + </div> + </div> + <div class="column" /> + </div> + </section> + </div> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx new file mode 100644 index 000000000..7d33d25ce --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx @@ -0,0 +1,65 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useWebhookAPI } from "../../../../hooks/webhooks.js"; +import { Notification } from "../../../../utils/types.js"; +import { CreatePage } from "./CreatePage.js"; +import { useOtpDeviceAPI } from "../../../../hooks/otp.js"; +import { useBankAccountAPI } from "../../../../hooks/bank.js"; + +export type Entity = MerchantBackend.BankAccounts.AccountAddDetails; +interface Props { + onBack?: () => void; + onConfirm: () => void; +} + +export default function CreateValidator({ onConfirm, onBack }: Props): VNode { + const { createBankAccount } = useBankAccountAPI(); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); + + return ( + <> + <NotificationCard notification={notif} /> + <CreatePage + onBack={onBack} + onCreate={(request: Entity) => { + return createBankAccount(request) + .then((d) => { + onConfirm() + }) + .catch((error) => { + setNotif({ + message: i18n.str`could not create device`, + type: "ERROR", + description: error.message, + }); + }); + }} + /> + </> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx new file mode 100644 index 000000000..6b4b63735 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx @@ -0,0 +1,28 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { FunctionalComponent, h } from "preact"; +import { ListPage as TestedComponent } from "./ListPage.js"; + +export default { + title: "Pages/Accounts/List", + component: TestedComponent, +}; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx new file mode 100644 index 000000000..24da755b9 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx @@ -0,0 +1,64 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode } from "preact"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { CardTable } from "./Table.js"; + +export interface Props { + devices: MerchantBackend.BankAccounts.BankAccountEntry[]; + onLoadMoreBefore?: () => void; + onLoadMoreAfter?: () => void; + onCreate: () => void; + onDelete: (e: MerchantBackend.BankAccounts.BankAccountEntry) => void; + onSelect: (e: MerchantBackend.BankAccounts.BankAccountEntry) => void; +} + +export function ListPage({ + devices, + onCreate, + onDelete, + onSelect, + onLoadMoreBefore, + onLoadMoreAfter, +}: Props): VNode { + const form = { payto_uri: "" }; + + const { i18n } = useTranslationContext(); + return ( + <section class="section is-main-section"> + <CardTable + accounts={devices.map((o) => ({ + ...o, + id: String(o.h_wire), + }))} + onCreate={onCreate} + onDelete={onDelete} + onSelect={onSelect} + onLoadMoreBefore={onLoadMoreBefore} + hasMoreBefore={!onLoadMoreBefore} + onLoadMoreAfter={onLoadMoreAfter} + hasMoreAfter={!onLoadMoreAfter} + /> + </section> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx new file mode 100644 index 000000000..7d6db0782 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx @@ -0,0 +1,385 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { StateUpdater, useState } from "preact/hooks"; +import { MerchantBackend } from "../../../../declaration.js"; +import { parsePaytoUri, PaytoType, PaytoUri, PaytoUriBitcoin, PaytoUriIBAN, PaytoUriTalerBank, PaytoUriUnknown } from "@gnu-taler/taler-util"; + +type Entity = MerchantBackend.BankAccounts.BankAccountEntry; + +interface Props { + accounts: Entity[]; + onDelete: (e: Entity) => void; + onSelect: (e: Entity) => void; + onCreate: () => void; + onLoadMoreBefore?: () => void; + hasMoreBefore?: boolean; + hasMoreAfter?: boolean; + onLoadMoreAfter?: () => void; +} + +export function CardTable({ + accounts, + onCreate, + onDelete, + onSelect, + onLoadMoreAfter, + onLoadMoreBefore, + hasMoreAfter, + hasMoreBefore, +}: Props): VNode { + const [rowSelection, rowSelectionHandler] = useState<string[]>([]); + + const { i18n } = useTranslationContext(); + + return ( + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"> + <i class="mdi mdi-newspaper" /> + </span> + <i18n.Translate>Bank accounts</i18n.Translate> + </p> + <div class="card-header-icon" aria-label="more options"> + <span + class="has-tooltip-left" + data-tooltip={i18n.str`add new accounts`} + > + <button class="button is-info" type="button" onClick={onCreate}> + <span class="icon is-small"> + <i class="mdi mdi-plus mdi-36px" /> + </span> + </button> + </span> + </div> + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + {accounts.length > 0 ? ( + <Table + accounts={accounts} + onDelete={onDelete} + onSelect={onSelect} + rowSelection={rowSelection} + rowSelectionHandler={rowSelectionHandler} + onLoadMoreAfter={onLoadMoreAfter} + onLoadMoreBefore={onLoadMoreBefore} + hasMoreAfter={hasMoreAfter} + hasMoreBefore={hasMoreBefore} + /> + ) : ( + <EmptyTable /> + )} + </div> + </div> + </div> + </div> + ); +} +interface TableProps { + rowSelection: string[]; + accounts: Entity[]; + onDelete: (e: Entity) => void; + onSelect: (e: Entity) => void; + rowSelectionHandler: StateUpdater<string[]>; + onLoadMoreBefore?: () => void; + hasMoreBefore?: boolean; + hasMoreAfter?: boolean; + onLoadMoreAfter?: () => void; +} + +function toggleSelected<T>(id: T): (prev: T[]) => T[] { + return (prev: T[]): T[] => + prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id); +} + +function Table({ + accounts, + onLoadMoreAfter, + onDelete, + onSelect, + onLoadMoreBefore, + hasMoreAfter, + hasMoreBefore, +}: TableProps): VNode { + const { i18n } = useTranslationContext(); + const emptyList: Record<PaytoType | "unknown", { parsed: PaytoUri, acc: Entity }[]> = { "bitcoin": [], "x-taler-bank": [], "iban": [], "unknown": [], } + const accountsByType = accounts.reduce((prev, acc) => { + const parsed = parsePaytoUri(acc.payto_uri) + if (!parsed) return prev //skip + if (parsed.targetType !== "bitcoin" && parsed.targetType !== "x-taler-bank" && parsed.targetType !== "iban") { + prev["unknown"].push({ parsed, acc }) + } else { + prev[parsed.targetType].push({ parsed, acc }) + } + return prev + }, emptyList) + + const bitcoinAccounts = accountsByType["bitcoin"] + const talerbankAccounts = accountsByType["x-taler-bank"] + const ibanAccounts = accountsByType["iban"] + const unkownAccounts = accountsByType["unknown"] + + + return ( + <Fragment> + + {bitcoinAccounts.length > 0 && <div class="table-container"> + <p class="card-header-title"><i18n.Translate>Bitcoin type accounts</i18n.Translate></p> + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th> + <i18n.Translate>Address</i18n.Translate> + </th> + <th> + <i18n.Translate>Sewgit 1</i18n.Translate> + </th> + <th> + <i18n.Translate>Sewgit 2</i18n.Translate> + </th> + <th /> + </tr> + </thead> + <tbody> + {bitcoinAccounts.map(({ parsed, acc }, idx) => { + const ac = parsed as PaytoUriBitcoin + return ( + <tr key={idx}> + <td + onClick={(): void => onSelect(acc)} + style={{ cursor: "pointer" }} + > + {ac.targetPath} + </td> + <td + onClick={(): void => onSelect(acc)} + style={{ cursor: "pointer" }} + > + {ac.segwitAddrs[0]} + </td> + <td + onClick={(): void => onSelect(acc)} + style={{ cursor: "pointer" }} + > + {ac.segwitAddrs[1]} + </td> + <td class="is-actions-cell right-sticky"> + <div class="buttons is-right"> + <button + class="button is-danger is-small has-tooltip-left" + data-tooltip={i18n.str`delete selected accounts from the database`} + onClick={() => onDelete(acc)} + > + Delete + </button> + </div> + </td> + </tr> + ); + })} + </tbody> + </table> + </div>} + + + + {talerbankAccounts.length > 0 && <div class="table-container"> + <p class="card-header-title"><i18n.Translate>Taler type accounts</i18n.Translate></p> + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th> + <i18n.Translate>Host</i18n.Translate> + </th> + <th> + <i18n.Translate>Account name</i18n.Translate> + </th> + <th /> + </tr> + </thead> + <tbody> + {talerbankAccounts.map(({ parsed, acc }, idx) => { + const ac = parsed as PaytoUriTalerBank + return ( + <tr key={idx}> + <td + onClick={(): void => onSelect(acc)} + style={{ cursor: "pointer" }} + > + {ac.host} + </td> + <td + onClick={(): void => onSelect(acc)} + style={{ cursor: "pointer" }} + > + {ac.account} + </td> + <td class="is-actions-cell right-sticky"> + <div class="buttons is-right"> + <button + class="button is-danger is-small has-tooltip-left" + data-tooltip={i18n.str`delete selected accounts from the database`} + onClick={() => onDelete(acc)} + > + Delete + </button> + </div> + </td> + </tr> + ); + })} + </tbody> + </table> + </div>} + + {ibanAccounts.length > 0 && <div class="table-container"> + <p class="card-header-title"><i18n.Translate>IBAN type accounts</i18n.Translate></p> + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th> + <i18n.Translate>Account name</i18n.Translate> + </th> + <th> + <i18n.Translate>IBAN</i18n.Translate> + </th> + <th> + <i18n.Translate>BIC</i18n.Translate> + </th> + <th /> + </tr> + </thead> + <tbody> + {ibanAccounts.map(({ parsed, acc }, idx) => { + const ac = parsed as PaytoUriIBAN + return ( + <tr key={idx}> + <td + onClick={(): void => onSelect(acc)} + style={{ cursor: "pointer" }} + > + {ac.params["receiver-name"]} + </td> + <td + onClick={(): void => onSelect(acc)} + style={{ cursor: "pointer" }} + > + {ac.iban} + </td> + <td + onClick={(): void => onSelect(acc)} + style={{ cursor: "pointer" }} + > + {ac.bic ?? ""} + </td> + <td class="is-actions-cell right-sticky"> + <div class="buttons is-right"> + <button + class="button is-danger is-small has-tooltip-left" + data-tooltip={i18n.str`delete selected accounts from the database`} + onClick={() => onDelete(acc)} + > + Delete + </button> + </div> + </td> + </tr> + ); + })} + </tbody> + </table> + </div>} + + {unkownAccounts.length > 0 && <div class="table-container"> + <p class="card-header-title"><i18n.Translate>Other type accounts</i18n.Translate></p> + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th> + <i18n.Translate>Type</i18n.Translate> + </th> + <th> + <i18n.Translate>Path</i18n.Translate> + </th> + <th /> + </tr> + </thead> + <tbody> + {unkownAccounts.map(({ parsed, acc }, idx) => { + const ac = parsed as PaytoUriUnknown + return ( + <tr key={idx}> + <td + onClick={(): void => onSelect(acc)} + style={{ cursor: "pointer" }} + > + {ac.targetType} + </td> + <td + onClick={(): void => onSelect(acc)} + style={{ cursor: "pointer" }} + > + {ac.targetPath} + </td> + <td class="is-actions-cell right-sticky"> + <div class="buttons is-right"> + <button + class="button is-danger is-small has-tooltip-left" + data-tooltip={i18n.str`delete selected accounts from the database`} + onClick={() => onDelete(acc)} + > + Delete + </button> + </div> + </td> + </tr> + ); + })} + </tbody> + </table> + </div>} + </Fragment> + + ); +} + +function EmptyTable(): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="content has-text-grey has-text-centered"> + <p> + <span class="icon is-large"> + <i class="mdi mdi-emoticon-sad mdi-48px" /> + </span> + </p> + <p> + <i18n.Translate> + There is no accounts yet, add more pressing the + sign + </i18n.Translate> + </p> + </div> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx new file mode 100644 index 000000000..9788ce0ec --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx @@ -0,0 +1,107 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { + ErrorType, + HttpError, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../../components/exception/loading.js"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useInstanceOtpDevices, useOtpDeviceAPI } from "../../../../hooks/otp.js"; +import { Notification } from "../../../../utils/types.js"; +import { ListPage } from "./ListPage.js"; +import { useBankAccountAPI, useInstanceBankAccounts } from "../../../../hooks/bank.js"; + +interface Props { + onUnauthorized: () => VNode; + onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; + onNotFound: () => VNode; + onCreate: () => void; + onSelect: (id: string) => void; +} + +export default function ListValidators({ + onUnauthorized, + onLoadError, + onCreate, + onSelect, + onNotFound, +}: Props): VNode { + const [position, setPosition] = useState<string | undefined>(undefined); + const { i18n } = useTranslationContext(); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { deleteBankAccount } = useBankAccountAPI(); + const result = useInstanceBankAccounts({ position }, (id) => setPosition(id)); + + if (result.loading) return <Loading />; + if (!result.ok) { + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(result); + } + + return ( + <Fragment> + <NotificationCard notification={notif} /> + + <ListPage + devices={result.data.accounts} + onLoadMoreBefore={ + result.isReachingStart ? result.loadMorePrev : undefined + } + onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined} + onCreate={onCreate} + onSelect={(e) => { + onSelect(e.h_wire); + }} + onDelete={(e: MerchantBackend.BankAccounts.BankAccountEntry) => + deleteBankAccount(e.h_wire) + .then(() => + setNotif({ + message: i18n.str`bank account delete successfully`, + type: "SUCCESS", + }), + ) + .catch((error) => + setNotif({ + message: i18n.str`could not delete the bank account`, + type: "ERROR", + description: error.message, + }), + ) + } + /> + </Fragment> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx new file mode 100644 index 000000000..fcb77b820 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx @@ -0,0 +1,32 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { UpdatePage as TestedComponent } from "./UpdatePage.js"; + +export default { + title: "Pages/Validators/Update", + component: TestedComponent, + argTypes: { + onUpdate: { action: "onUpdate" }, + onBack: { action: "onBack" }, + }, +}; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx new file mode 100644 index 000000000..802f593cf --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx @@ -0,0 +1,114 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; +import { + FormErrors, + FormProvider, +} from "../../../../components/form/FormProvider.js"; +import { Input } from "../../../../components/form/Input.js"; +import { MerchantBackend, WithId } from "../../../../declaration.js"; + +type Entity = MerchantBackend.BankAccounts.AccountPatchDetails & WithId; + +interface Props { + onUpdate: (d: Entity) => Promise<void>; + onBack?: () => void; + account: Entity; +} +export function UpdatePage({ account, onUpdate, onBack }: Props): VNode { + const { i18n } = useTranslationContext(); + + const [state, setState] = useState<Partial<Entity>>(account); + + const errors: FormErrors<Entity> = { + }; + + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + + const submitForm = () => { + if (hasErrors) return Promise.reject(); + return onUpdate(state as any); + }; + + return ( + <div> + <section class="section"> + <section class="hero is-hero-bar"> + <div class="hero-body"> + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <span class="is-size-4"> + Account: <b>{account.id}</b> + </span> + </div> + </div> + </div> + </div> + </section> + <hr /> + + <section class="section is-main-section"> + <div class="columns"> + <div class="column is-four-fifths"> + <FormProvider + object={state} + valueHandler={setState} + errors={errors} + > + <Input<Entity> + name="credit_facade_url" + label={i18n.str`Description`} + tooltip={i18n.str`dddd`} + /> + </FormProvider> + + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <AsyncButton + disabled={hasErrors} + data-tooltip={ + hasErrors + ? i18n.str`Need to complete marked fields` + : "confirm operation" + } + onClick={submitForm} + > + <i18n.Translate>Confirm</i18n.Translate> + </AsyncButton> + </div> + </div> + </div> + </section> + </section> + </div> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx new file mode 100644 index 000000000..44dee7651 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx @@ -0,0 +1,96 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { + ErrorType, + HttpError, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../../components/exception/loading.js"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend, WithId } from "../../../../declaration.js"; +import { useBankAccountAPI, useBankAccountDetails } from "../../../../hooks/bank.js"; +import { Notification } from "../../../../utils/types.js"; +import { UpdatePage } from "./UpdatePage.js"; + +export type Entity = MerchantBackend.BankAccounts.AccountPatchDetails & WithId; + +interface Props { + onBack?: () => void; + onConfirm: () => void; + onUnauthorized: () => VNode; + onNotFound: () => VNode; + onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; + bid: string; +} +export default function UpdateValidator({ + bid, + onConfirm, + onBack, + onUnauthorized, + onNotFound, + onLoadError, +}: Props): VNode { + const { updateBankAccount } = useBankAccountAPI(); + const result = useBankAccountDetails(bid); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + + const { i18n } = useTranslationContext(); + + if (result.loading) return <Loading />; + if (!result.ok) { + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(result); + } + + return ( + <Fragment> + <NotificationCard notification={notif} /> + <UpdatePage + account={{ ...result.data, id: bid }} + onBack={onBack} + onUpdate={(data) => { + return updateBankAccount(bid, data) + .then(onConfirm) + .catch((error) => { + setNotif({ + message: i18n.str`could not update account`, + type: "ERROR", + description: error.message, + }); + }); + }} + /> + </Fragment> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/details/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/details/DetailPage.tsx index e5937ab7b..21dadb1e3 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/details/DetailPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/details/DetailPage.tsx @@ -36,14 +36,13 @@ interface Props { function convert( from: MerchantBackend.Instances.QueryInstancesResponse, ): Entity { - const { accounts: allAccounts, ...rest } = from; - const accounts = allAccounts.filter((a) => a.active); const defaults = { default_wire_fee_amortization: 1, + use_stefan: true, default_pay_delay: { d_us: 1000 * 60 * 60 * 1000 }, //one hour default_wire_transfer_delay: { d_us: 1000 * 60 * 60 * 2 * 1000 }, //two hours }; - return { ...defaults, ...rest, accounts }; + return { ...defaults, ...from }; } export function DetailPage({ selected }: Props): VNode { @@ -74,11 +73,6 @@ export function DetailPage({ selected }: Props): VNode { <div class="column is-6"> <FormProvider<Entity> object={value} valueHandler={valueHandler}> <Input<Entity> name="name" readonly label={i18n.str`Name`} /> - <Input<Entity> - name="accounts" - readonly - label={i18n.str`Account address`} - /> </FormProvider> </div> <div class="column" /> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx index 3a6e0fbfe..367fabce2 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx @@ -51,17 +51,15 @@ function createExample<Props>( export const Example = createExample(TestedComponent, { selected: { - accounts: [], name: "name", auth: { method: "external" }, address: {}, + user_type: "business", jurisdiction: {}, - default_max_deposit_fee: "TESTKUDOS:2", - default_max_wire_fee: "TESTKUDOS:1", + use_stefan: true, default_pay_delay: { d_us: 1000 * 1000, //one second }, - default_wire_fee_amortization: 1, default_wire_transfer_delay: { d_us: 1000 * 1000, //one second }, diff --git a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx index 6f50ac830..d33f64ada 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx @@ -54,5 +54,5 @@ export const Example = tests.createExample(TestedComponent, { payto_uri: "payto://iban/de123123123", }, ], - } as MerchantBackend.Instances.AccountKycRedirects, + } as MerchantBackend.KYC.AccountKycRedirects, }); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx index 67005d3cc..338081886 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx @@ -24,7 +24,7 @@ import { h, VNode } from "preact"; import { MerchantBackend } from "../../../../declaration.js"; export interface Props { - status: MerchantBackend.Instances.AccountKycRedirects; + status: MerchantBackend.KYC.AccountKycRedirects; } export function ListPage({ status }: Props): VNode { @@ -85,11 +85,11 @@ export function ListPage({ status }: Props): VNode { ); } interface PendingTableProps { - entries: MerchantBackend.Instances.MerchantAccountKycRedirect[]; + entries: MerchantBackend.KYC.MerchantAccountKycRedirect[]; } interface TimedOutTableProps { - entries: MerchantBackend.Instances.ExchangeKycTimeout[]; + entries: MerchantBackend.KYC.ExchangeKycTimeout[]; } function PendingTable({ entries }: PendingTableProps): VNode { diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx index fcf611c3c..bd9f65718 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx @@ -42,12 +42,13 @@ function createExample<Props>( export const Example = createExample(TestedComponent, { instanceConfig: { - default_max_deposit_fee: "", - default_max_wire_fee: "", default_pay_delay: { d_us: 1000 * 1000 * 60 * 60, //one hour }, - default_wire_fee_amortization: 1, + default_wire_transfer_delay: { + d_us: 1000 * 1000 * 60 * 60, //one hour + }, + use_stefan: true, }, instanceInventory: [ { diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx index fa9347c6e..ea2cf849a 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx @@ -44,6 +44,7 @@ import { OrderCreateSchema as schema } from "../../../../schemas/index.js"; import { rate } from "../../../../utils/amount.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; import { useSettings } from "../../../../hooks/useSettings.js"; +import { InputToggle } from "../../../../components/form/InputToggle.js"; interface Props { onCreate: (d: MerchantBackend.Orders.PostOrderRequest) => void; @@ -52,34 +53,38 @@ interface Props { instanceInventory: (MerchantBackend.Products.ProductDetail & WithId)[]; } interface InstanceConfig { - default_max_wire_fee: string; - default_max_deposit_fee: string; - default_wire_fee_amortization: number; + use_stefan: boolean; default_pay_delay: Duration; + default_wire_transfer_delay: Duration; } -function with_defaults(config: InstanceConfig): Partial<Entity> { +function with_defaults(config: InstanceConfig, currency: string): Partial<Entity> { const defaultPayDeadline = !config.default_pay_delay || config.default_pay_delay.d_us === "forever" ? undefined : add(new Date(), { seconds: config.default_pay_delay.d_us / (1000 * 1000), }); + const defaultWireDeadline = + !config.default_wire_transfer_delay || config.default_wire_transfer_delay.d_us === "forever" + ? undefined + : add(new Date(), { + seconds: config.default_wire_transfer_delay.d_us / (1000 * 1000), + }); return { inventoryProducts: {}, products: [], pricing: {}, payments: { - max_wire_fee: config.default_max_wire_fee, - max_fee: config.default_max_deposit_fee, - wire_fee_amortization: config.default_wire_fee_amortization, + max_fee: undefined, pay_deadline: defaultPayDeadline, refund_deadline: defaultPayDeadline, createToken: true, + wire_transfer_deadline: defaultWireDeadline, }, shipping: {}, - extra: "", + extra: {}, }; } @@ -107,8 +112,6 @@ interface Payments { wire_transfer_deadline?: Date; auto_refund_deadline?: Date; max_fee?: string; - max_wire_fee?: string; - wire_fee_amortization?: number; createToken: boolean; minimum_age?: number; } @@ -118,7 +121,7 @@ interface Entity { pricing: Partial<Pricing>; payments: Partial<Payments>; shipping: Partial<Shipping>; - extra: string; + extra: Record<string, string>; } const stringIsValidJSON = (value: string) => { @@ -136,8 +139,9 @@ export function CreatePage({ instanceConfig, instanceInventory, }: Props): VNode { - const [value, valueHandler] = useState(with_defaults(instanceConfig)); const config = useConfigContext(); + const instance_default = with_defaults(instanceConfig, config.currency) + const [value, valueHandler] = useState(instance_default); const zero = Amounts.zeroOfCurrency(config.currency); const [settings] = useSettings() const inventoryList = Object.values(value.inventoryProducts || {}); @@ -160,10 +164,10 @@ export function CreatePage({ ? i18n.str`must be greater than 0` : undefined, }), - extra: - value.extra && !stringIsValidJSON(value.extra) - ? i18n.str`not a valid json` - : undefined, + // extra: + // value.extra && !stringIsValidJSON(value.extra) + // ? i18n.str`not a valid json` + // : undefined, payments: undefinedIfEmpty({ refund_deadline: !value.payments?.refund_deadline ? undefined @@ -202,6 +206,7 @@ export function CreatePage({ ) ? i18n.str`auto refund cannot be after refund deadline` : undefined, + }), shipping: undefinedIfEmpty({ delivery_date: !value.shipping?.delivery_date @@ -225,7 +230,7 @@ export function CreatePage({ amount: order.pricing.order_price, summary: order.pricing.summary, products: productList, - extra: value.extra, + extra: JSON.stringify(value.extra), pay_deadline: value.payments.pay_deadline ? { t_s: Math.floor(value.payments.pay_deadline.getTime() / 1000), @@ -250,9 +255,7 @@ export function CreatePage({ ), } : undefined, - wire_fee_amortization: value.payments.wire_fee_amortization as number, max_fee: value.payments.max_fee as string, - max_wire_fee: value.payments.max_wire_fee as string, delivery_date: value.shipping.delivery_date ? { t_s: value.shipping.delivery_date.getTime() / 1000 } @@ -326,6 +329,8 @@ export function CreatePage({ const totalAsString = Amounts.stringify(totalPrice.amount); const allProducts = productList.concat(inventoryList.map(asProduct)); + const [newField, setNewField] = useState("") + useEffect(() => { valueHandler((v) => { return { @@ -486,16 +491,61 @@ export function CreatePage({ name="payments.pay_deadline" label={i18n.str`Payment deadline`} tooltip={i18n.str`Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.`} + side={ + <span> + <button class="button" onClick={() => { + valueHandler({ + ...value, + payments: { + ...(value.payments ?? {}), + pay_deadline: instance_default.payments?.pay_deadline + } + }) + }}> + <i18n.Translate>default</i18n.Translate> + </button> + </span> + } /> <InputDate name="payments.refund_deadline" label={i18n.str`Refund deadline`} tooltip={i18n.str`Time until which the order can be refunded by the merchant.`} + side={ + <span> + <button class="button" onClick={() => { + valueHandler({ + ...value, + payments: { + ...(value.payments ?? {}), + refund_deadline: instance_default.payments?.refund_deadline + } + }) + }}> + <i18n.Translate>default</i18n.Translate> + </button> + </span> + } /> <InputDate name="payments.wire_transfer_deadline" label={i18n.str`Wire transfer deadline`} tooltip={i18n.str`Deadline for the exchange to make the wire transfer.`} + side={ + <span> + <button class="button" onClick={() => { + valueHandler({ + ...value, + payments: { + ...(value.payments ?? {}), + wire_transfer_deadline: instance_default.payments?.wire_transfer_deadline + } + }) + }}> + <i18n.Translate>default</i18n.Translate> + </button> + </span> + } /> <InputDate name="payments.auto_refund_deadline" @@ -505,23 +555,13 @@ export function CreatePage({ <InputCurrency name="payments.max_fee" - label={i18n.str`Maximum deposit fee`} - tooltip={i18n.str`Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.`} + label={i18n.str`Maximum fee`} + tooltip={i18n.str`Maximum fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.`} /> - <InputCurrency - name="payments.max_wire_fee" - label={i18n.str`Maximum wire fee`} - tooltip={i18n.str`Maximum aggregate wire fees the merchant is willing to cover for this order. Wire fees exceeding this amount are to be covered by the customers.`} - /> - <InputNumber - name="payments.wire_fee_amortization" - label={i18n.str`Wire fee amortization`} - tooltip={i18n.str`Factor by which wire fees exceeding the above threshold are divided to determine the share of excess wire fees to be paid explicitly by the consumer.`} - /> - <InputBoolean + <InputToggle name="payments.createToken" label={i18n.str`Create token`} - tooltip={i18n.str`Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.`} + tooltip={i18n.str`If the order ID is easy to guess the token will prevent user to steal orders from others.`} /> <InputNumber name="payments.minimum_age" @@ -530,7 +570,7 @@ export function CreatePage({ help={ minAgeByProducts > 0 ? i18n.str`Min age defined by the producs is ${minAgeByProducts}` - : undefined + : i18n.str`No product with age restriction in this order` } /> </InputGroup> @@ -542,12 +582,53 @@ export function CreatePage({ label={i18n.str`Additional information`} tooltip={i18n.str`Custom information to be included in the contract for this order.`} > - <Input - name="extra" - inputType="multiline" - label={`Value`} - tooltip={i18n.str`You must enter a value in JavaScript Object Notation (JSON).`} - /> + {Object.keys(value.extra ?? {}).map((key) => { + + return <Input + name={`extra.${key}`} + inputType="multiline" + label={key} + tooltip={i18n.str`You must enter a value in JavaScript Object Notation (JSON).`} + side={ + <button class="button" onClick={(e) => { + if (value.extra && value.extra[key] !== undefined) { + console.log(value.extra) + delete value.extra[key] + } + valueHandler({ + ...value, + }) + }}>remove</button> + } + /> + })} + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + <i18n.Translate>Custom field name</i18n.Translate> + <span class="icon has-tooltip-right" data-tooltip={"new extra field"}> + <i class="mdi mdi-information" /> + </span> + </label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input class="input " value={newField} onChange={(e) => setNewField(e.currentTarget.value)} /> + </p> + </div> + </div> + <button class="button" onClick={(e) => { + setNewField("") + valueHandler({ + ...value, + extra: { + ...(value.extra ?? {}), + [newField]: "" + } + }) + }}>add</button> + </div> </InputGroup> } </FormProvider> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx index ffefd5302..2474fd042 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx @@ -38,7 +38,7 @@ export type Entity = { }; interface Props { onBack?: () => void; - onConfirm: () => void; + onConfirm: (id: string) => void; onUnauthorized: () => VNode; onNotFound: () => VNode; onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; @@ -95,7 +95,9 @@ export default function OrderCreate({ onBack={onBack} onCreate={(request: MerchantBackend.Orders.PostOrderRequest) => { createOrder(request) - .then(onConfirm) + .then((r) => { + return onConfirm(r.data.order_id) + }) .catch((error) => { setNotif({ message: "could not create order", diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx index e430ede56..6e73a01a5 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx @@ -50,13 +50,11 @@ const defaultContractTerm = { auditors: [], exchanges: [], max_fee: "TESTKUDOS:1", - max_wire_fee: "TESTKUDOS:1", merchant: {} as any, merchant_base_url: "http://merchant.url/", order_id: "2021.165-03GDFC26Y1NNG", products: [], summary: "text summary", - wire_fee_amortization: 1, wire_transfer_deadline: { t_s: "never", }, diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx index 8965d41c9..e42adc2ff 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx @@ -19,7 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { AmountJson, Amounts } from "@gnu-taler/taler-util"; +import { AmountJson, Amounts, stringifyRefundUri } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { format, formatDistance } from "date-fns"; import { Fragment, h, VNode } from "preact"; @@ -38,6 +38,7 @@ import { MerchantBackend } from "../../../../declaration.js"; import { mergeRefunds } from "../../../../utils/amount.js"; import { RefundModal } from "../list/Table.js"; import { Event, Timeline } from "./Timeline.js"; +import { dateFormatForSettings, datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; type Entity = MerchantBackend.Orders.MerchantOrderStatusResponse; type CT = MerchantBackend.ContractTerms; @@ -87,18 +88,6 @@ function ContractTerms({ value }: { value: CT }) { label={i18n.str`Max fee`} tooltip={i18n.str`maximum total deposit fee accepted by the merchant for this contract`} /> - <Input<CT> - readonly - name="max_wire_fee" - label={i18n.str`Max wire fee`} - tooltip={i18n.str`maximum wire fee accepted by the merchant`} - /> - <Input<CT> - readonly - name="wire_fee_amortization" - label={i18n.str`Wire fee amortization`} - tooltip={i18n.str`over how many customer transactions does the merchant expect to amortize wire fees on average`} - /> <InputDate<CT> readonly name="timestamp" @@ -204,6 +193,7 @@ function ClaimedPage({ const [value, valueHandler] = useState<Partial<Claimed>>(order); const { i18n } = useTranslationContext(); + const [settings] = useSettings() return ( <div> @@ -249,7 +239,7 @@ function ClaimedPage({ </b>{" "} {format( new Date(order.contract_terms.timestamp.t_s * 1000), - "yyyy-MM-dd HH:mm:ss", + datetimeFormatForSettings(settings) )} </p> </div> @@ -427,9 +417,10 @@ function PaidPage({ const [value, valueHandler] = useState<Partial<Paid>>(order); const { url } = useBackendContext(); - const refundHost = url.replace(/.*:\/\//, ""); // remove protocol part - const proto = url.startsWith("http://") ? "taler+http" : "taler"; - const refundurl = `${proto}://refund/${refundHost}/${order.contract_terms.order_id}/`; + const refundurl = stringifyRefundUri({ + merchantBaseUrl: url, + orderId: order.contract_terms.order_id + }) const refundable = new Date().getTime() < order.contract_terms.refund_deadline.t_s * 1000; const { i18n } = useTranslationContext(); @@ -618,6 +609,7 @@ function UnpaidPage({ }) { const [value, valueHandler] = useState<Partial<Unpaid>>(order); const { i18n } = useTranslationContext(); + const [settings] = useSettings() return ( <div> <section class="hero is-hero-bar"> @@ -666,7 +658,7 @@ function UnpaidPage({ ? "never" : format( new Date(order.creation_time.t_s * 1000), - "yyyy-MM-dd HH:mm:ss", + datetimeFormatForSettings(settings) )} </p> </div> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx index e68889a92..8c863f386 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx @@ -16,6 +16,7 @@ import { format } from "date-fns"; import { h } from "preact"; import { useEffect, useState } from "preact/hooks"; +import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; interface Props { events: Event[]; @@ -30,7 +31,7 @@ export function Timeline({ events: e }: Props) { }); events.sort((a, b) => a.when.getTime() - b.when.getTime()); - + const [settings] = useSettings(); const [state, setState] = useState(events); useEffect(() => { const handle = setTimeout(() => { @@ -104,7 +105,7 @@ export function Timeline({ events: e }: Props) { } })()} <div class="timeline-content"> - {e.description !== "now" && <p class="heading">{format(e.when, "yyyy/MM/dd HH:mm:ss")}</p>} + {e.description !== "now" && <p class="heading">{format(e.when, datetimeFormatForSettings(settings))}</p>} <p>{e.description}</p> </div> </div> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx index 37770d273..c29a6fa6e 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx @@ -26,19 +26,24 @@ import { useState } from "preact/hooks"; import { DatePicker } from "../../../../components/picker/DatePicker.js"; import { MerchantBackend, WithId } from "../../../../declaration.js"; import { CardTable } from "./Table.js"; +import { dateFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; export interface ListPageProps { errorOrderId: string | undefined; onShowAll: () => void; + onShowNotPaid: () => void; onShowPaid: () => void; onShowRefunded: () => void; onShowNotWired: () => void; + onShowWired: () => void; onCopyURL: (id: string) => void; isAllActive: string; isPaidActive: string; + isNotPaidActive: string; isRefundedActive: string; isNotWiredActive: string; + isWiredActive: string; jumpToDate?: Date; onSelectDate: (date?: Date) => void; @@ -66,18 +71,23 @@ export function ListPage({ onCopyURL, onShowAll, onShowPaid, + onShowNotPaid, onShowRefunded, onShowNotWired, + onShowWired, onSelectDate, isPaidActive, isRefundedActive, isNotWiredActive, onCreate, + isNotPaidActive, + isWiredActive, }: ListPageProps): VNode { const { i18n } = useTranslationContext(); const dateTooltip = i18n.str`select date to show nearby orders`; const [pickDate, setPickDate] = useState(false); const [orderId, setOrderId] = useState<string>(""); + const [settings] = useSettings(); return ( <section class="section is-main-section"> @@ -116,13 +126,13 @@ export function ListPage({ <div class="column is-two-thirds"> <div class="tabs" style={{ overflow: "inherit" }}> <ul> - <li class={isAllActive}> + <li class={isNotPaidActive}> <div class="has-tooltip-right" - data-tooltip={i18n.str`remove all filters`} + data-tooltip={i18n.str`only show paid orders`} > - <a onClick={onShowAll}> - <i18n.Translate>All</i18n.Translate> + <a onClick={onShowNotPaid}> + <i18n.Translate>New</i18n.Translate> </a> </div> </li> @@ -156,6 +166,26 @@ export function ListPage({ </a> </div> </li> + <li class={isWiredActive}> + <div + class="has-tooltip-left" + data-tooltip={i18n.str`only show orders where customers paid, but wire payments from payment provider are still pending`} + > + <a onClick={onShowWired}> + <i18n.Translate>Completed</i18n.Translate> + </a> + </div> + </li> + <li class={isAllActive}> + <div + class="has-tooltip-right" + data-tooltip={i18n.str`remove all filters`} + > + <a onClick={onShowAll}> + <i18n.Translate>All</i18n.Translate> + </a> + </div> + </li> </ul> </div> </div> @@ -180,8 +210,8 @@ export function ListPage({ class="input" type="text" readonly - value={!jumpToDate ? "" : format(jumpToDate, "yyyy/MM/dd")} - placeholder={i18n.str`date (YYYY/MM/DD)`} + value={!jumpToDate ? "" : format(jumpToDate, dateFormatForSettings(settings))} + placeholder={i18n.str`date (${dateFormatForSettings(settings)})`} onClick={() => { setPickDate(true); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx index 3c927033b..608c9b20d 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx @@ -36,6 +36,7 @@ import { ConfirmModal } from "../../../../components/modal/index.js"; import { useConfigContext } from "../../../../context/config.js"; import { MerchantBackend, WithId } from "../../../../declaration.js"; import { mergeRefunds } from "../../../../utils/amount.js"; +import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; type Entity = MerchantBackend.Orders.OrderHistoryEntry & WithId; interface Props { @@ -136,6 +137,7 @@ function Table({ hasMoreBefore, }: TableProps): VNode { const { i18n } = useTranslationContext(); + const [settings] = useSettings(); return ( <div class="table-container"> {onLoadMoreBefore && ( @@ -173,9 +175,9 @@ function Table({ {i.timestamp.t_s === "never" ? "never" : format( - new Date(i.timestamp.t_s * 1000), - "yyyy/MM/dd HH:mm:ss", - )} + new Date(i.timestamp.t_s * 1000), + datetimeFormatForSettings(settings), + )} </td> <td onClick={(): void => onSelect(i)} @@ -260,6 +262,7 @@ export function RefundModal({ }: RefundModalProps): VNode { type State = { mainReason?: string; description?: string; refund?: string }; const [form, setValue] = useState<State>({}); + const [settings] = useSettings(); const { i18n } = useTranslationContext(); // const [errors, setErrors] = useState<FormErrors<State>>({}); @@ -281,8 +284,8 @@ export function RefundModal({ const totalRefundable = !orderPrice ? Amounts.zeroOfCurrency(totalRefunded.currency) : refunds.length - ? Amounts.sub(orderPrice, totalRefunded).amount - : orderPrice; + ? Amounts.sub(orderPrice, totalRefunded).amount + : orderPrice; const isRefundable = Amounts.isNonZero(totalRefundable); const duplicatedText = i18n.str`duplicated`; @@ -296,10 +299,10 @@ export function RefundModal({ refund: !form.refund ? i18n.str`required` : !Amounts.parse(form.refund) - ? i18n.str`invalid format` - : Amounts.cmp(totalRefundable, Amounts.parse(form.refund)!) === -1 - ? i18n.str`this value exceed the refundable amount` - : undefined, + ? i18n.str`invalid format` + : Amounts.cmp(totalRefundable, Amounts.parse(form.refund)!) === -1 + ? i18n.str`this value exceed the refundable amount` + : undefined, }; const hasErrors = Object.keys(errors).some( (k) => (errors as any)[k] !== undefined, @@ -361,9 +364,9 @@ export function RefundModal({ {r.timestamp.t_s === "never" ? "never" : format( - new Date(r.timestamp.t_s * 1000), - "yyyy-MM-dd HH:mm:ss", - )} + new Date(r.timestamp.t_s * 1000), + datetimeFormatForSettings(settings), + )} </td> <td>{r.amount}</td> <td>{r.reason}</td> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx index 6888eda58..48f77e3d3 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx @@ -55,7 +55,7 @@ export default function OrderList({ onSelect, onNotFound, }: Props): VNode { - const [filter, setFilter] = useState<InstanceOrderFilter>({}); + const [filter, setFilter] = useState<InstanceOrderFilter>({ paid: "no" }); const [orderToBeRefunded, setOrderToBeRefunded] = useState< MerchantBackend.Orders.OrderHistoryEntry | undefined >(undefined); @@ -88,13 +88,15 @@ export default function OrderList({ return onLoadError(result); } - const isPaidActive = filter.paid === "yes" ? "is-active" : ""; + const isNotPaidActive = filter.paid === "no" ? "is-active" : ""; + const isPaidActive = filter.paid === "yes" && filter.wired === undefined ? "is-active" : ""; const isRefundedActive = filter.refunded === "yes" ? "is-active" : ""; - const isNotWiredActive = filter.wired === "no" ? "is-active" : ""; + const isNotWiredActive = filter.wired === "no" && filter.paid === "yes" ? "is-active" : ""; + const isWiredActive = filter.wired === "yes" ? "is-active" : ""; const isAllActive = filter.paid === undefined && - filter.refunded === undefined && - filter.wired === undefined + filter.refunded === undefined && + filter.wired === undefined ? "is-active" : ""; @@ -127,7 +129,9 @@ export default function OrderList({ errorOrderId={errorOrderId} isAllActive={isAllActive} isNotWiredActive={isNotWiredActive} + isWiredActive={isWiredActive} isPaidActive={isPaidActive} + isNotPaidActive={isNotPaidActive} isRefundedActive={isRefundedActive} jumpToDate={filter.date} onCopyURL={(id) => @@ -137,9 +141,11 @@ export default function OrderList({ onSearchOrderById={testIfOrderExistAndSelect} onSelectDate={setNewDate} onShowAll={() => setFilter({})} + onShowNotPaid={() => setFilter({ paid: "no" })} onShowPaid={() => setFilter({ paid: "yes" })} onShowRefunded={() => setFilter({ refunded: "yes" })} - onShowNotWired={() => setFilter({ wired: "no" })} + onShowNotWired={() => setFilter({ wired: "no", paid: "yes" })} + onShowWired={() => setFilter({ wired: "yes" })} /> {orderToBeRefunded && ( diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx index 6bbb89dfa..cbfe1d573 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx @@ -32,6 +32,7 @@ import { import { InputCurrency } from "../../../../components/form/InputCurrency.js"; import { InputNumber } from "../../../../components/form/InputNumber.js"; import { MerchantBackend, WithId } from "../../../../declaration.js"; +import { dateFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; type Entity = MerchantBackend.Products.ProductDetail & WithId; @@ -122,6 +123,7 @@ function Table({ onDelete, }: TableProps): VNode { const { i18n } = useTranslationContext(); + const [settings] = useSettings(); return ( <div class="table-container"> <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> @@ -134,7 +136,7 @@ function Table({ <i18n.Translate>Description</i18n.Translate> </th> <th> - <i18n.Translate>Sell</i18n.Translate> + <i18n.Translate>Price per unit</i18n.Translate> </th> <th> <i18n.Translate>Taxes</i18n.Translate> @@ -156,10 +158,10 @@ function Table({ const restStockInfo = !i.next_restock ? "" : i.next_restock.t_s === "never" - ? "never" - : `restock at ${format( + ? "never" + : `restock at ${format( new Date(i.next_restock.t_s * 1000), - "yyyy/MM/dd", + dateFormatForSettings(settings), )}`; let stockInfo: ComponentChildren = ""; if (i.total_stock < 0) { @@ -332,26 +334,35 @@ function FastProductWithInfiniteStockUpdateForm({ /> </FormProvider> - <div class="buttons is-right mt-5"> - <button class="button" onClick={onCancel}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - <span - class="has-tooltip-left" - data-tooltip={i18n.str`update product with new price`} - > - <button - class="button is-info" - onClick={() => - onUpdate({ - ...product, - price: value.price, - }) - } - > - <i18n.Translate>Confirm</i18n.Translate> + <div class="buttons is-expanded"> + + <div class="buttons mt-5"> + + <button class="button " onClick={onCancel}> + <i18n.Translate>Clone</i18n.Translate> </button> - </span> + </div> + <div class="buttons is-right mt-5"> + <button class="button" onClick={onCancel}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + <span + class="has-tooltip-left" + data-tooltip={i18n.str`update product with new price`} + > + <button + class="button is-info" + onClick={() => + onUpdate({ + ...product, + price: value.price, + }) + } + > + <i18n.Translate>Confirm update</i18n.Translate> + </button> + </span> + </div> </div> </Fragment> ); @@ -374,9 +385,8 @@ function FastProductWithManagedStockUpdateForm({ const errors: FormErrors<FastProductUpdate> = { lost: currentStock + value.incoming < value.lost - ? `lost cannot be greater that current + incoming (max ${ - currentStock + value.incoming - })` + ? `lost cannot be greater that current + incoming (max ${currentStock + value.incoming + })` : undefined, }; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx index 87efd1554..85c50e5ed 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx @@ -36,6 +36,7 @@ import { import { Notification } from "../../../../utils/types.js"; import { CardTable } from "./Table.js"; import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { ConfirmModal, DeleteModal } from "../../../../components/modal/index.js"; interface Props { onUnauthorized: () => VNode; @@ -53,6 +54,8 @@ export default function ProductList({ }: Props): VNode { const result = useInstanceProducts(); const { deleteProduct, updateProduct } = useProductAPI(); + const [deleting, setDeleting] = + useState<MerchantBackend.Products.ProductDetail & WithId | null>(null); const [notif, setNotif] = useState<Notification | undefined>(undefined); const { i18n } = useTranslationContext(); @@ -97,22 +100,43 @@ export default function ProductList({ } onSelect={(product) => onSelect(product.id)} onDelete={(prod: MerchantBackend.Products.ProductDetail & WithId) => - deleteProduct(prod.id) - .then(() => + setDeleting(prod) + } + /> + + {deleting && ( + <ConfirmModal + label={`Delete product`} + description={`Delete the product "${deleting.description}"`} + danger + active + onCancel={() => setDeleting(null)} + onConfirm={async (): Promise<void> => { + try { + await deleteProduct(deleting.id); setNotif({ - message: i18n.str`product delete successfully`, + message: i18n.str`Product "${deleting.description}" (ID: ${deleting.id}) has been deleted`, type: "SUCCESS", - }), - ) - .catch((error) => + }); + } catch (error) { setNotif({ - message: i18n.str`could not delete the product`, + message: i18n.str`Failed to delete product`, type: "ERROR", - description: error.message, - }), - ) - } - /> + description: error instanceof Error ? error.message : undefined, + }); + } + setDeleting(null); + }} + > + <p> + If you delete the product named <b>"{deleting.description}"</b> (ID:{" "} + <b>{deleting.id}</b>), the stock and related information will be lost + </p> + <p class="warning"> + Deleting an product <b>cannot be undone</b>. + </p> + </ConfirmModal> + )} </section> ); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx index fccb20121..2201e75a5 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx @@ -36,7 +36,7 @@ import { URL_REGEX, } from "../../../../utils/constants.js"; -type Entity = MerchantBackend.Tips.ReserveCreateRequest; +type Entity = MerchantBackend.Rewards.ReserveCreateRequest; interface Props { onCreate: (d: Entity) => Promise<void>; @@ -80,15 +80,15 @@ function ViewStep({ initial_balance: !reserve.initial_balance ? "cannot be empty" : !(parseInt(reserve.initial_balance.split(":")[1], 10) > 0) - ? i18n.str`it should be greater than 0` - : undefined, + ? i18n.str`it should be greater than 0` + : undefined, exchange_url: !reserve.exchange_url ? i18n.str`cannot be empty` : !URL_REGEX.test(reserve.exchange_url) - ? i18n.str`must be a valid URL` - : !exchangeQueryError - ? undefined - : exchangeQueryError, + ? i18n.str`must be a valid URL` + : !exchangeQueryError + ? undefined + : exchangeQueryError, }; const hasErrors = Object.keys(errors).some( diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx index 94fcdaff7..1d512c843 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx @@ -22,8 +22,8 @@ import { CreatedSuccessfully as Template } from "../../../../components/notifica import { MerchantBackend, WireAccount } from "../../../../declaration.js"; type Entity = { - request: MerchantBackend.Tips.ReserveCreateRequest; - response: MerchantBackend.Tips.ReserveCreateConfirmation; + request: MerchantBackend.Rewards.ReserveCreateRequest; + response: MerchantBackend.Rewards.ReserveCreateConfirmation; }; interface Props { @@ -98,15 +98,15 @@ export function ShowAccountsOfReserveAsQRWithLink({ const accountsInfo = !accounts ? [] : accounts - .map((acc) => { - const p = parsePaytoUri(acc.payto_uri); - if (p) { - p.params["message"] = message; - p.params["amount"] = amount; - } - return p; - }) - .filter(isNotUndefined); + .map((acc) => { + const p = parsePaytoUri(acc.payto_uri); + if (p) { + p.params["message"] = message; + p.params["amount"] = amount; + } + return p; + }) + .filter(isNotUndefined); const links = accountsInfo.map((a) => stringifyPaytoUri(a)); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/index.tsx index 8a4fe1565..4bbaf1459 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/index.tsx @@ -39,9 +39,9 @@ export default function CreateReserve({ onBack, onConfirm }: Props): VNode { const [createdOk, setCreatedOk] = useState< | { - request: MerchantBackend.Tips.ReserveCreateRequest; - response: MerchantBackend.Tips.ReserveCreateConfirmation; - } + request: MerchantBackend.Rewards.ReserveCreateRequest; + response: MerchantBackend.Rewards.ReserveCreateConfirmation; + } | undefined >(undefined); @@ -54,7 +54,7 @@ export default function CreateReserve({ onBack, onConfirm }: Props): VNode { <NotificationCard notification={notif} /> <CreatePage onBack={onBack} - onCreate={(request: MerchantBackend.Tips.ReserveCreateRequest) => { + onCreate={(request: MerchantBackend.Rewards.ReserveCreateRequest) => { return createReserve(request) .then((r) => setCreatedOk({ request, response: r.data })) .catch((error) => { diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx index b0173b5d3..d8840eeac 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx @@ -36,11 +36,12 @@ import { InputDate } from "../../../../components/form/InputDate.js"; import { TextField } from "../../../../components/form/TextField.js"; import { SimpleModal } from "../../../../components/modal/index.js"; import { MerchantBackend } from "../../../../declaration.js"; -import { useTipDetails } from "../../../../hooks/reserves.js"; -import { TipInfo } from "./TipInfo.js"; +import { useRewardDetails } from "../../../../hooks/reserves.js"; +import { RewardInfo } from "./RewardInfo.js"; import { ShowAccountsOfReserveAsQRWithLink } from "../create/CreatedSuccessfully.js"; +import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; -type Entity = MerchantBackend.Tips.ReserveDetail; +type Entity = MerchantBackend.Rewards.ReserveDetail; type CT = MerchantBackend.ContractTerms; interface Props { @@ -116,14 +117,14 @@ export function DetailPage({ id, selected, onBack }: Props): VNode { <span class="icon"> <i class="mdi mdi-cash-register" /> </span> - <i18n.Translate>Tips</i18n.Translate> + <i18n.Translate>Rewards</i18n.Translate> </p> </header> <div class="card-content"> <div class="b-table has-pagination"> <div class="table-wrapper has-mobile-cards"> - {selected.tips && selected.tips.length > 0 ? ( - <Table tips={selected.tips} /> + {selected.rewards && selected.rewards.length > 0 ? ( + <Table rewards={selected.rewards} /> ) : ( <EmptyTable /> )} @@ -163,7 +164,7 @@ function EmptyTable(): VNode { </p> <p> <i18n.Translate> - No tips has been authorized from this reserve + No reward has been authorized from this reserve </i18n.Translate> </p> </div> @@ -171,10 +172,10 @@ function EmptyTable(): VNode { } interface TableProps { - tips: MerchantBackend.Tips.TipStatusEntry[]; + rewards: MerchantBackend.Rewards.RewardStatusEntry[]; } -function Table({ tips }: TableProps): VNode { +function Table({ rewards }: TableProps): VNode { const { i18n } = useTranslationContext(); return ( <div class="table-container"> @@ -196,8 +197,8 @@ function Table({ tips }: TableProps): VNode { </tr> </thead> <tbody> - {tips.map((t, i) => { - return <TipRow id={t.tip_id} key={i} entry={t} />; + {rewards.map((t, i) => { + return <RewardRow id={t.reward_id} key={i} entry={t} />; })} </tbody> </table> @@ -205,15 +206,16 @@ function Table({ tips }: TableProps): VNode { ); } -function TipRow({ +function RewardRow({ id, entry, }: { id: string; - entry: MerchantBackend.Tips.TipStatusEntry; + entry: MerchantBackend.Rewards.RewardStatusEntry; }) { const [selected, setSelected] = useState(false); - const result = useTipDetails(id); + const result = useRewardDetails(id); + const [settings] = useSettings(); if (result.loading) { return ( <tr> @@ -242,11 +244,11 @@ function TipRow({ <Fragment> {selected && ( <SimpleModal - description="tip" + description="reward" active onCancel={() => setSelected(false)} > - <TipInfo id={id} amount={info.total_authorized} entity={info} /> + <RewardInfo id={id} amount={info.total_authorized} entity={info} /> </SimpleModal> )} <tr> @@ -256,7 +258,7 @@ function TipRow({ <td onClick={onSelect}> {info.expiration.t_s === "never" ? "never" - : format(info.expiration.t_s * 1000, "yyyy/MM/dd HH:mm:ss")} + : format(info.expiration.t_s * 1000, datetimeFormatForSettings(settings))} </td> </tr> </Fragment> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/Details.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/Details.stories.tsx index 2592e2c6e..41c715f20 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/Details.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/Details.stories.tsx @@ -92,7 +92,7 @@ export const NotYetFunded = createExample(TestedComponent, { }, }); -export const FundedWithEmptyTips = createExample(TestedComponent, { +export const FundedWithEmptyRewards = createExample(TestedComponent, { id: "THISISTHERESERVEID", selected: { active: true, @@ -115,10 +115,10 @@ export const FundedWithEmptyTips = createExample(TestedComponent, { }, ], exchange_url: "http://exchange.taler/", - tips: [ + rewards: [ { reason: "asdasd", - tip_id: "123", + reward_id: "123", total_amount: "TESTKUDOS:1", }, ], diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/TipInfo.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx index 360d39aba..57a051ed7 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/TipInfo.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx @@ -17,8 +17,10 @@ import { format } from "date-fns"; import { Fragment, h, VNode } from "preact"; import { useBackendContext } from "../../../../context/backend.js"; import { MerchantBackend } from "../../../../declaration.js"; +import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; +import { stringifyRewardUri } from "@gnu-taler/taler-util"; -type Entity = MerchantBackend.Tips.TipDetails; +type Entity = MerchantBackend.Rewards.RewardDetails; interface Props { id: string; @@ -26,11 +28,10 @@ interface Props { amount: string; } -export function TipInfo({ id, amount, entity }: Props): VNode { - const { url } = useBackendContext(); - const tipHost = url.replace(/.*:\/\//, ""); // remove protocol part - const proto = url.startsWith("http://") ? "taler+http" : "taler"; - const tipURL = `${proto}://tip/${tipHost}/${id}`; +export function RewardInfo({ id: merchantRewardId, amount, entity }: Props): VNode { + const { url: merchantBaseUrl } = useBackendContext(); + const [settings] = useSettings(); + const rewardURL = stringifyRewardUri({ merchantBaseUrl, merchantRewardId }) return ( <Fragment> <div class="field is-horizontal"> @@ -52,8 +53,8 @@ export function TipInfo({ id, amount, entity }: Props): VNode { <div class="field-body is-flex-grow-3"> <div class="field" style={{ overflowWrap: "anywhere" }}> <p class="control"> - <a target="_blank" rel="noreferrer" href={tipURL}> - {tipURL} + <a target="_blank" rel="noreferrer" href={rewardURL}> + {rewardURL} </a> </p> </div> @@ -73,9 +74,9 @@ export function TipInfo({ id, amount, entity }: Props): VNode { !entity.expiration || entity.expiration.t_s === "never" ? "never" : format( - entity.expiration.t_s * 1000, - "yyyy/MM/dd HH:mm:ss", - ) + entity.expiration.t_s * 1000, + datetimeFormatForSettings(settings), + ) } /> </p> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/AutorizeTipModal.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/AutorizeRewardModal.tsx index 1882f50d3..e205ee621 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/AutorizeTipModal.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/AutorizeRewardModal.tsx @@ -34,32 +34,32 @@ import { ContinueModal, } from "../../../../components/modal/index.js"; import { MerchantBackend } from "../../../../declaration.js"; -import { AuthorizeTipSchema } from "../../../../schemas/index.js"; +import { AuthorizeRewardSchema } from "../../../../schemas/index.js"; import { CreatedSuccessfully } from "./CreatedSuccessfully.js"; -interface AuthorizeTipModalProps { +interface AuthorizeRewardModalProps { onCancel: () => void; - onConfirm: (value: MerchantBackend.Tips.TipCreateRequest) => void; - tipAuthorized?: { - response: MerchantBackend.Tips.TipCreateConfirmation; - request: MerchantBackend.Tips.TipCreateRequest; + onConfirm: (value: MerchantBackend.Rewards.RewardCreateRequest) => void; + rewardAuthorized?: { + response: MerchantBackend.Rewards.RewardCreateConfirmation; + request: MerchantBackend.Rewards.RewardCreateRequest; }; } -export function AuthorizeTipModal({ +export function AuthorizeRewardModal({ onCancel, onConfirm, - tipAuthorized, -}: AuthorizeTipModalProps): VNode { + rewardAuthorized, +}: AuthorizeRewardModalProps): VNode { // const result = useOrderDetails(id) - type State = MerchantBackend.Tips.TipCreateRequest; + type State = MerchantBackend.Rewards.RewardCreateRequest; const [form, setValue] = useState<Partial<State>>({}); const { i18n } = useTranslationContext(); // const [errors, setErrors] = useState<FormErrors<State>>({}) let errors: FormErrors<State> = {}; try { - AuthorizeTipSchema.validateSync(form, { abortEarly: false }); + AuthorizeRewardSchema.validateSync(form, { abortEarly: false }); } catch (err) { if (err instanceof yup.ValidationError) { const yupErrors = err.inner as any[]; @@ -77,12 +77,12 @@ export function AuthorizeTipModal({ const validateAndConfirm = () => { onConfirm(form as State); }; - if (tipAuthorized) { + if (rewardAuthorized) { return ( - <ContinueModal description="tip" active onConfirm={onCancel}> + <ContinueModal description="reward" active onConfirm={onCancel}> <CreatedSuccessfully - entity={tipAuthorized.response} - request={tipAuthorized.request} + entity={rewardAuthorized.response} + request={rewardAuthorized.request} onConfirm={onCancel} /> </ContinueModal> @@ -91,7 +91,7 @@ export function AuthorizeTipModal({ return ( <ConfirmModal - description="tip" + description="New reward" active onCancel={onCancel} disabled={hasErrors} @@ -105,18 +105,18 @@ export function AuthorizeTipModal({ <InputCurrency<State> name="amount" label={i18n.str`Amount`} - tooltip={i18n.str`amount of tip`} + tooltip={i18n.str`amount of reward`} /> <Input<State> name="justification" label={i18n.str`Justification`} inputType="multiline" - tooltip={i18n.str`reason for the tip`} + tooltip={i18n.str`reason for the reward`} /> <Input<State> name="next_url" - label={i18n.str`URL after tip`} - tooltip={i18n.str`URL to visit after tip payment`} + label={i18n.str`URL after reward`} + tooltip={i18n.str`URL to visit after reward payment`} /> </FormProvider> </ConfirmModal> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/CreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/CreatedSuccessfully.tsx index 643651b52..b78236bc7 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/CreatedSuccessfully.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/CreatedSuccessfully.tsx @@ -17,12 +17,13 @@ import { format } from "date-fns"; import { Fragment, h, VNode } from "preact"; import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js"; import { MerchantBackend } from "../../../../declaration.js"; +import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; -type Entity = MerchantBackend.Tips.TipCreateConfirmation; +type Entity = MerchantBackend.Rewards.RewardCreateConfirmation; interface Props { entity: Entity; - request: MerchantBackend.Tips.TipCreateRequest; + request: MerchantBackend.Rewards.RewardCreateRequest; onConfirm: () => void; onCreateAnother?: () => void; } @@ -33,6 +34,7 @@ export function CreatedSuccessfully({ onConfirm, onCreateAnother, }: Props): VNode { + const [settings] = useSettings(); return ( <Fragment> <div class="field is-horizontal"> @@ -66,7 +68,7 @@ export function CreatedSuccessfully({ <div class="field-body is-flex-grow-3"> <div class="field"> <p class="control"> - <input readonly class="input" value={entity.tip_status_url} /> + <input readonly class="input" value={entity.reward_status_url} /> </p> </div> </div> @@ -82,13 +84,13 @@ export function CreatedSuccessfully({ class="input" readonly value={ - !entity.tip_expiration || - entity.tip_expiration.t_s === "never" + !entity.reward_expiration || + entity.reward_expiration.t_s === "never" ? "never" : format( - entity.tip_expiration.t_s * 1000, - "yyyy/MM/dd HH:mm:ss", - ) + entity.reward_expiration.t_s * 1000, + datetimeFormatForSettings(settings), + ) } /> </p> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/List.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/List.stories.tsx index fe305f4fd..b070bbde3 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/List.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/List.stories.tsx @@ -25,12 +25,6 @@ import { CardTable as TestedComponent } from "./Table.js"; export default { title: "Pages/Reserve/List", component: TestedComponent, - argTypes: { - onCreate: { action: "onCreate" }, - onDelete: { action: "onDelete" }, - onNewTip: { action: "onNewTip" }, - onSelect: { action: "onSelect" }, - }, }; function createExample<Props>( diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/Table.tsx index 1f229d7cb..795e7ec82 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/Table.tsx @@ -23,12 +23,13 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; import { Fragment, h, VNode } from "preact"; import { MerchantBackend, WithId } from "../../../../declaration.js"; +import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; -type Entity = MerchantBackend.Tips.ReserveStatusEntry & WithId; +type Entity = MerchantBackend.Rewards.ReserveStatusEntry & WithId; interface Props { instances: Entity[]; - onNewTip: (id: Entity) => void; + onNewReward: (id: Entity) => void; onSelect: (id: Entity) => void; onDelete: (id: Entity) => void; onCreate: () => void; @@ -38,7 +39,7 @@ export function CardTable({ instances, onCreate, onSelect, - onNewTip, + onNewReward, onDelete, }: Props): VNode { const [withoutFunds, withFunds] = instances.reduce((prev, current) => { @@ -70,7 +71,7 @@ export function CardTable({ <div class="table-wrapper has-mobile-cards"> <TableWithoutFund instances={withoutFunds} - onNewTip={onNewTip} + onNewReward={onNewReward} onSelect={onSelect} onDelete={onDelete} /> @@ -108,7 +109,7 @@ export function CardTable({ {withFunds.length > 0 ? ( <Table instances={withFunds} - onNewTip={onNewTip} + onNewReward={onNewReward} onSelect={onSelect} onDelete={onDelete} /> @@ -124,13 +125,14 @@ export function CardTable({ } interface TableProps { instances: Entity[]; - onNewTip: (id: Entity) => void; + onNewReward: (id: Entity) => void; onDelete: (id: Entity) => void; onSelect: (id: Entity) => void; } -function Table({ instances, onNewTip, onSelect, onDelete }: TableProps): VNode { +function Table({ instances, onNewReward, onSelect, onDelete }: TableProps): VNode { const { i18n } = useTranslationContext(); + const [settings] = useSettings(); return ( <div class="table-container"> <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> @@ -164,7 +166,7 @@ function Table({ instances, onNewTip, onSelect, onDelete }: TableProps): VNode { > {i.creation_time.t_s === "never" ? "never" - : format(i.creation_time.t_s * 1000, "yyyy/MM/dd HH:mm:ss")} + : format(i.creation_time.t_s * 1000, datetimeFormatForSettings(settings))} </td> <td onClick={(): void => onSelect(i)} @@ -173,9 +175,9 @@ function Table({ instances, onNewTip, onSelect, onDelete }: TableProps): VNode { {i.expiration_time.t_s === "never" ? "never" : format( - i.expiration_time.t_s * 1000, - "yyyy/MM/dd HH:mm:ss", - )} + i.expiration_time.t_s * 1000, + datetimeFormatForSettings(settings), + )} </td> <td onClick={(): void => onSelect(i)} @@ -207,11 +209,11 @@ function Table({ instances, onNewTip, onSelect, onDelete }: TableProps): VNode { </button> <button class="button is-small is-info has-tooltip-left" - data-tooltip={i18n.str`authorize new tip from selected reserve`} + data-tooltip={i18n.str`authorize new reward from selected reserve`} type="button" - onClick={(): void => onNewTip(i)} + onClick={(): void => onNewReward(i)} > - New Tip + New Reward </button> </div> </td> @@ -249,6 +251,7 @@ function TableWithoutFund({ onDelete, }: TableProps): VNode { const { i18n } = useTranslationContext(); + const [settings] = useSettings(); return ( <div class="table-container"> <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> @@ -276,7 +279,7 @@ function TableWithoutFund({ > {i.creation_time.t_s === "never" ? "never" - : format(i.creation_time.t_s * 1000, "yyyy/MM/dd HH:mm:ss")} + : format(i.creation_time.t_s * 1000, datetimeFormatForSettings(settings))} </td> <td onClick={(): void => onSelect(i)} @@ -285,9 +288,9 @@ function TableWithoutFund({ {i.expiration_time.t_s === "never" ? "never" : format( - i.expiration_time.t_s * 1000, - "yyyy/MM/dd HH:mm:ss", - )} + i.expiration_time.t_s * 1000, + datetimeFormatForSettings(settings), + )} </td> <td onClick={(): void => onSelect(i)} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/index.tsx index 14387c2a9..b26ff0000 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/index.tsx @@ -34,9 +34,10 @@ import { useReservesAPI, } from "../../../../hooks/reserves.js"; import { Notification } from "../../../../utils/types.js"; -import { AuthorizeTipModal } from "./AutorizeTipModal.js"; +import { AuthorizeRewardModal } from "./AutorizeRewardModal.js"; import { CardTable } from "./Table.js"; import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { ConfirmModal } from "../../../../components/modal/index.js"; interface Props { onUnauthorized: () => VNode; @@ -46,12 +47,12 @@ interface Props { onCreate: () => void; } -interface TipConfirmation { - response: MerchantBackend.Tips.TipCreateConfirmation; - request: MerchantBackend.Tips.TipCreateRequest; +interface RewardConfirmation { + response: MerchantBackend.Rewards.RewardCreateConfirmation; + request: MerchantBackend.Rewards.RewardCreateRequest; } -export default function ListTips({ +export default function ListRewards({ onUnauthorized, onLoadError, onNotFound, @@ -59,14 +60,16 @@ export default function ListTips({ onCreate, }: Props): VNode { const result = useInstanceReserves(); - const { deleteReserve, authorizeTipReserve } = useReservesAPI(); + const { deleteReserve, authorizeRewardReserve } = useReservesAPI(); const [notif, setNotif] = useState<Notification | undefined>(undefined); const { i18n } = useTranslationContext(); - const [reserveForTip, setReserveForTip] = useState<string | undefined>( + const [reserveForReward, setReserveForReward] = useState<string | undefined>( undefined, ); - const [tipAuthorized, setTipAuthorized] = useState< - TipConfirmation | undefined + const [deleting, setDeleting] = + useState<MerchantBackend.Rewards.ReserveStatusEntry | null>(null); + const [rewardAuthorized, setRewardAuthorized] = useState< + RewardConfirmation | undefined >(undefined); if (result.loading) return <Loading />; @@ -88,30 +91,30 @@ export default function ListTips({ <section class="section is-main-section"> <NotificationCard notification={notif} /> - {reserveForTip && ( - <AuthorizeTipModal + {reserveForReward && ( + <AuthorizeRewardModal onCancel={() => { - setReserveForTip(undefined); - setTipAuthorized(undefined); + setReserveForReward(undefined); + setRewardAuthorized(undefined); }} - tipAuthorized={tipAuthorized} + rewardAuthorized={rewardAuthorized} onConfirm={async (request) => { try { - const response = await authorizeTipReserve( - reserveForTip, + const response = await authorizeRewardReserve( + reserveForReward, request, ); - setTipAuthorized({ + setRewardAuthorized({ request, response: response.data, }); } catch (error) { setNotif({ - message: i18n.str`could not create the tip`, + message: i18n.str`could not create the reward`, type: "ERROR", description: error instanceof Error ? error.message : undefined, }); - setReserveForTip(undefined); + setReserveForReward(undefined); } }} /> @@ -122,10 +125,47 @@ export default function ListTips({ .filter((r) => r.active) .map((o) => ({ ...o, id: o.reserve_pub }))} onCreate={onCreate} - onDelete={(reserve) => deleteReserve(reserve.reserve_pub)} + onDelete={(reserve) => { + setDeleting(reserve) + }} onSelect={(reserve) => onSelect(reserve.id)} - onNewTip={(reserve) => setReserveForTip(reserve.id)} + onNewReward={(reserve) => setReserveForReward(reserve.id)} /> + + {deleting && ( + <ConfirmModal + label={`Delete reserve`} + description={`Delete the reserve`} + danger + active + onCancel={() => setDeleting(null)} + onConfirm={async (): Promise<void> => { + try { + await deleteReserve(deleting.reserve_pub); + setNotif({ + message: i18n.str`Reserve for "${deleting.merchant_initial_amount}" (ID: ${deleting.reserve_pub}) has been deleted`, + type: "SUCCESS", + }); + } catch (error) { + setNotif({ + message: i18n.str`Failed to delete reserve`, + type: "ERROR", + description: error instanceof Error ? error.message : undefined, + }); + } + setDeleting(null); + }} + > + <p> + If you delete the reserve for <b>"{deleting.merchant_initial_amount}"</b> you won't be able to create more rewards. <br /> + Reserve ID: <b>{deleting.reserve_pub}</b> + </p> + <p class="warning"> + Deleting an template <b>cannot be undone</b>. + </p> + </ConfirmModal> + )} + </section> ); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx index e20b9bc27..8629d8dee 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx @@ -24,7 +24,7 @@ import { MerchantTemplateContractDetails, } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { @@ -35,17 +35,16 @@ import { Input } from "../../../../components/form/Input.js"; import { InputCurrency } from "../../../../components/form/InputCurrency.js"; import { InputDuration } from "../../../../components/form/InputDuration.js"; import { InputNumber } from "../../../../components/form/InputNumber.js"; -import { InputSelector } from "../../../../components/form/InputSelector.js"; import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; import { useBackendContext } from "../../../../context/backend.js"; +import { useInstanceContext } from "../../../../context/instance.js"; import { MerchantBackend } from "../../../../declaration.js"; import { - isBase32RFC3548Charset, - randomBase32Key, + isBase32RFC3548Charset } from "../../../../utils/crypto.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; -import { QR } from "../../../../components/exception/QR.js"; -import { useInstanceContext } from "../../../../context/instance.js"; +import { InputSearchOnList } from "../../../../components/form/InputSearchOnList.js"; +import { useInstanceOtpDevices } from "../../../../hooks/otp.js"; type Entity = MerchantBackend.Template.TemplateAddDetails; @@ -54,16 +53,11 @@ interface Props { onBack?: () => void; } -const algorithms = [0, 1, 2]; -const algorithmsNames = ["off", "30s 8d TOTP-SHA1", "30s 8d eTOTP-SHA1"]; - export function CreatePage({ onCreate, onBack }: Props): VNode { const { i18n } = useTranslationContext(); const backend = useBackendContext(); - const { id: instanceId } = useInstanceContext(); - const issuer = new URL(backend.url).hostname; + const devices = useInstanceOtpDevices() - const [showKey, setShowKey] = useState(false); const [state, setState] = useState<Partial<Entity>>({ template_contract: { minimum_age: 0, @@ -78,7 +72,11 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { : Amounts.parse(state.template_contract?.amount); const errors: FormErrors<Entity> = { - template_id: !state.template_id ? i18n.str`should not be empty` : undefined, + template_id: !state.template_id + ? i18n.str`should not be empty` + : !/[a-zA-Z0-9]*/.test(state.template_id) + ? i18n.str`no valid. only characters and numbers` + : undefined, template_description: !state.template_description ? i18n.str`should not be empty` : undefined, @@ -104,15 +102,6 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { ? i18n.str`to short` : undefined, } as Partial<MerchantTemplateContractDetails>), - pos_key: !state.pos_key - ? !state.pos_algorithm - ? undefined - : i18n.str`required` - : !isBase32RFC3548Charset(state.pos_key) - ? i18n.str`just letters and numbers from 2 to 7` - : state.pos_key.length !== 32 - ? i18n.str`size of the key should be 32` - : undefined, }; const hasErrors = Object.keys(errors).some( @@ -124,7 +113,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { return onCreate(state as any); }; - const qrText = `otpauth://totp/${instanceId}/${state.template_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${state.pos_key}`; + const deviceList = !devices.ok ? [] : devices.data.otp_devices return ( <div> @@ -139,7 +128,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { > <InputWithAddon<Entity> name="template_id" - help={`${backend.url}/instances/templates/${state.template_id ?? ""}`} + help={`${backend.url}/templates/${state.template_id ?? ""}`} label={i18n.str`Identifier`} tooltip={i18n.str`Name of the template in URLs.`} /> @@ -172,83 +161,21 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { help="" tooltip={i18n.str`How much time has the customer to complete the payment once the order was created.`} /> - <InputSelector<Entity> - name="pos_algorithm" - label={i18n.str`Verification algorithm`} - tooltip={i18n.str`Algorithm to use to verify transaction in offline mode`} - values={algorithms} - toStr={(v) => algorithmsNames[v]} - fromStr={(v) => Number(v)} + <Input<Entity> + name="otp_id" + label={i18n.str`OTP device`} + readonly + tooltip={i18n.str`Use to verify transaction in offline mode.`} + /> + <InputSearchOnList + label={i18n.str`Search device`} + onChange={(p) => setState((v) => ({ ...v, otp_id: p?.id }))} + list={deviceList.map(e => ({ + description: e.device_description, + id: e.otp_device_id + }))} /> - {state.pos_algorithm && state.pos_algorithm > 0 ? ( - <Fragment> - <InputWithAddon<Entity> - name="pos_key" - label={i18n.str`Point-of-sale key`} - inputType={showKey ? "text" : "password"} - help="Be sure to be very hard to guess or use the random generator" - tooltip={i18n.str`Useful to validate the purchase`} - fromStr={(v) => v.toUpperCase()} - addonAfter={ - <span class="icon"> - {showKey ? ( - <i class="mdi mdi-eye" /> - ) : ( - <i class="mdi mdi-eye-off" /> - )} - </span> - } - side={ - <span style={{ display: "flex" }}> - <button - data-tooltip={i18n.str`generate random secret key`} - class="button is-info mr-3" - onClick={(e) => { - const pos_key = randomBase32Key(); - setState((s) => ({ ...s, pos_key })); - }} - > - <i18n.Translate>random</i18n.Translate> - </button> - <button - data-tooltip={ - showKey - ? i18n.str`show secret key` - : i18n.str`hide secret key` - } - class="button is-info mr-3" - onClick={(e) => { - setShowKey(!showKey); - }} - > - {showKey ? ( - <i18n.Translate>hide</i18n.Translate> - ) : ( - <i18n.Translate>show</i18n.Translate> - )} - </button> - </span> - } - /> - {showKey && ( - <Fragment> - <QR text={qrText} /> - <div - style={{ - color: "grey", - fontSize: "small", - width: 200, - textAlign: "center", - margin: "auto", - wordBreak: "break-all", - }} - > - {qrText} - </div> - </Fragment> - )} - </Fragment> - ) : undefined} + </FormProvider> <div class="buttons is-right mt-5"> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx index 2f91298bf..3c9bb231c 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx @@ -36,6 +36,7 @@ import { import { Notification } from "../../../../utils/types.js"; import { ListPage } from "./ListPage.js"; import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { ConfirmModal } from "../../../../components/modal/index.js"; interface Props { onUnauthorized: () => VNode; @@ -61,6 +62,8 @@ export default function ListTemplates({ const [notif, setNotif] = useState<Notification | undefined>(undefined); const { deleteTemplate } = useTemplateAPI(); const result = useInstanceTemplates({ position }, (id) => setPosition(id)); + const [deleting, setDeleting] = + useState<MerchantBackend.Template.TemplateEntry | null>(null); if (result.loading) return <Loading />; if (!result.ok) { @@ -97,23 +100,45 @@ export default function ListTemplates({ onQR={(e) => { onQR(e.template_id); }} - onDelete={(e: MerchantBackend.Template.TemplateEntry) => - deleteTemplate(e.template_id) - .then(() => + onDelete={(e: MerchantBackend.Template.TemplateEntry) => { + setDeleting(e) + } + } + /> + + {deleting && ( + <ConfirmModal + label={`Delete template`} + description={`Delete the template "${deleting.template_description}"`} + danger + active + onCancel={() => setDeleting(null)} + onConfirm={async (): Promise<void> => { + try { + await deleteTemplate(deleting.template_id); setNotif({ - message: i18n.str`template delete successfully`, + message: i18n.str`Template "${deleting.template_description}" (ID: ${deleting.template_id}) has been deleted`, type: "SUCCESS", - }), - ) - .catch((error) => + }); + } catch (error) { setNotif({ - message: i18n.str`could not delete the template`, + message: i18n.str`Failed to delete template`, type: "ERROR", - description: error.message, - }), - ) - } - /> + description: error instanceof Error ? error.message : undefined, + }); + } + setDeleting(null); + }} + > + <p> + If you delete the template <b>"{deleting.template_description}"</b> (ID:{" "} + <b>{deleting.template_id}</b>) you may loose information + </p> + <p class="warning"> + Deleting an template <b>cannot be undone</b>. + </p> + </ConfirmModal> + )} </Fragment> ); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx index 0f30efafd..c65cf6a19 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx @@ -19,7 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { HttpError, useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { useState } from "preact/hooks"; import { QR } from "../../../../components/exception/QR.js"; @@ -35,35 +35,32 @@ import { useConfigContext } from "../../../../context/config.js"; import { useInstanceContext } from "../../../../context/instance.js"; import { MerchantBackend } from "../../../../declaration.js"; import { stringifyPayTemplateUri } from "@gnu-taler/taler-util"; +import { useOtpDeviceDetails } from "../../../../hooks/otp.js"; +import { Loading } from "../../../../components/exception/loading.js"; type Entity = MerchantBackend.Template.UsingTemplateDetails; interface Props { - template: MerchantBackend.Template.TemplateDetails; + contract: MerchantBackend.Template.TemplateContractDetails; id: string; onBack?: () => void; } -export function QrPage({ template, id: templateId, onBack }: Props): VNode { +export function QrPage({ contract, id: templateId, onBack }: Props): VNode { const { i18n } = useTranslationContext(); const { url: backendUrl } = useBackendContext(); const { id: instanceId } = useInstanceContext(); const config = useConfigContext(); - const [setupTOTP, setSetupTOTP] = useState(false); const [state, setState] = useState<Partial<Entity>>({ - amount: template.template_contract.amount, - summary: template.template_contract.summary, + amount: contract.amount, + summary: contract.summary, }); const errors: FormErrors<Entity> = {}; - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); - - const fixedAmount = !!template.template_contract.amount; - const fixedSummary = !!template.template_contract.summary; + const fixedAmount = !!contract.amount; + const fixedSummary = !!contract.summary; const templateParams: Record<string, string> = {} if (!fixedAmount) { @@ -89,40 +86,9 @@ export function QrPage({ template, id: templateId, onBack }: Props): VNode { const issuer = encodeURIComponent( `${new URL(backendUrl).host}/${instanceId}`, ); - const oauthUri = !template.pos_algorithm - ? undefined - : template.pos_algorithm === 1 - ? `otpauth://totp/${issuer}:${templateId}?secret=${template.pos_key}&issuer=${issuer}&algorithm=SHA1&digits=8&period=30` - : template.pos_algorithm === 2 - ? `otpauth://totp/${issuer}:${templateId}?secret=${template.pos_key}&issuer=${issuer}&algorithm=SHA1&digits=8&period=30` - : undefined; - - const keySlice = template.pos_key?.substring(0, 4); - - const oauthUriWithoutSecret = !template.pos_algorithm - ? undefined - : template.pos_algorithm === 1 - ? `otpauth://totp/${issuer}:${templateId}?secret=${keySlice}...&issuer=${issuer}&algorithm=SHA1&digits=8&period=30` - : template.pos_algorithm === 2 - ? `otpauth://totp/${issuer}:${templateId}?secret=${keySlice}...&issuer=${issuer}&algorithm=SHA1&digits=8&period=30` - : undefined; + return ( <div> - {oauthUri && ( - <ConfirmModal - description="Setup TOTP" - active={setupTOTP} - onCancel={() => { - setSetupTOTP(false); - }} - > - <p>Scan this qr code with your TOTP device</p> - <QR text={oauthUri} /> - <pre style={{ textAlign: "center" }}> - <a href={oauthUri}>{oauthUriWithoutSecret}</a> - </pre> - </ConfirmModal> - )} <section class="section is-main-section"> <div class="columns"> <div class="column" /> @@ -176,14 +142,6 @@ export function QrPage({ template, id: templateId, onBack }: Props): VNode { > <i18n.Translate>Print</i18n.Translate> </button> - {oauthUri && ( - <button - class="button is-info" - onClick={() => setSetupTOTP(true)} - > - <i18n.Translate>Setup TOTP</i18n.Translate> - </button> - )} </div> </div> <div class="column" /> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx index 1f74afc2b..7db7478f7 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx @@ -74,7 +74,7 @@ export default function TemplateQrPage({ return ( <> <NotificationCard notification={notif} /> - <QrPage template={result.data} id={tid} onBack={onBack} /> + <QrPage contract={result.data.template_contract} id={tid} onBack={onBack} /> </> ); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx index 30e5502bb..30d47385c 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx @@ -61,10 +61,7 @@ const algorithmsNames = ["off", "30s 8d TOTP-SHA1", "30s 8d eTOTP-SHA1"]; export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { const { i18n } = useTranslationContext(); const backend = useBackendContext(); - const { id: instanceId } = useInstanceContext(); - const issuer = new URL(backend.url).hostname; - const [showKey, setShowKey] = useState(false); const [state, setState] = useState<Partial<Entity>>(template); const parsedPrice = !state.template_contract?.amount @@ -78,34 +75,25 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { template_contract: !state.template_contract ? undefined : undefinedIfEmpty({ - amount: !state.template_contract?.amount - ? undefined - : !parsedPrice + amount: !state.template_contract?.amount + ? undefined + : !parsedPrice ? i18n.str`not valid` : Amounts.isZero(parsedPrice) - ? i18n.str`must be greater than 0` - : undefined, - minimum_age: - state.template_contract.minimum_age < 0 - ? i18n.str`should be greater that 0` + ? i18n.str`must be greater than 0` : undefined, - pay_duration: !state.template_contract.pay_duration - ? i18n.str`can't be empty` - : state.template_contract.pay_duration.d_us === "forever" + minimum_age: + state.template_contract.minimum_age < 0 + ? i18n.str`should be greater that 0` + : undefined, + pay_duration: !state.template_contract.pay_duration + ? i18n.str`can't be empty` + : state.template_contract.pay_duration.d_us === "forever" ? undefined : state.template_contract.pay_duration.d_us < 1000 * 1000 // less than one second - ? i18n.str`to short` - : undefined, - } as Partial<MerchantTemplateContractDetails>), - pos_key: !state.pos_key - ? !state.pos_algorithm - ? undefined - : i18n.str`required` - : !isBase32RFC3548Charset(state.pos_key) - ? i18n.str`just letters and numbers from 2 to 7` - : state.pos_key.length !== 32 - ? i18n.str`size of the key should be 32` - : undefined, + ? i18n.str`to short` + : undefined, + } as Partial<MerchantTemplateContractDetails>), }; const hasErrors = Object.keys(errors).some( @@ -117,7 +105,6 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { return onUpdate(state as any); }; - const qrText = `otpauth://totp/${instanceId}/${state.id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${state.pos_key}`; return ( <div> @@ -128,7 +115,7 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { <div class="level-left"> <div class="level-item"> <span class="is-size-4"> - {backend.url}/instances/template/{template.id} + {backend.url}/templates/{template.id} </span> </div> </div> @@ -182,84 +169,6 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { help="" tooltip={i18n.str`How much time has the customer to complete the payment once the order was created.`} /> - <InputSelector<Entity> - name="pos_algorithm" - label={i18n.str`Verification algorithm`} - tooltip={i18n.str`Algorithm to use to verify transaction in offline mode`} - values={algorithms} - toStr={(v) => algorithmsNames[v]} - fromStr={(v) => Number(v)} - /> - {state.pos_algorithm && state.pos_algorithm > 0 ? ( - <Fragment> - <InputWithAddon<Entity> - name="pos_key" - label={i18n.str`Point-of-sale key`} - inputType={showKey ? "text" : "password"} - help="Be sure to be very hard to guess or use the random generator" - expand - tooltip={i18n.str`Useful to validate the purchase`} - fromStr={(v) => v.toUpperCase()} - addonAfter={ - <span class="icon"> - {showKey ? ( - <i class="mdi mdi-eye" /> - ) : ( - <i class="mdi mdi-eye-off" /> - )} - </span> - } - side={ - <span style={{ display: "flex" }}> - <button - data-tooltip={i18n.str`generate random secret key`} - class="button is-info mr-3" - onClick={(e) => { - const pos_key = randomBase32Key(); - setState((s) => ({ ...s, pos_key })); - }} - > - <i18n.Translate>random</i18n.Translate> - </button> - <button - data-tooltip={ - showKey - ? i18n.str`show secret key` - : i18n.str`hide secret key` - } - class="button is-info mr-3" - onClick={(e) => { - setShowKey(!showKey); - }} - > - {showKey ? ( - <i18n.Translate>hide</i18n.Translate> - ) : ( - <i18n.Translate>show</i18n.Translate> - )} - </button> - </span> - } - /> - {showKey && ( - <Fragment> - <QR text={qrText} /> - <div - style={{ - color: "grey", - fontSize: "small", - width: 200, - textAlign: "center", - margin: "auto", - wordBreak: "break-all", - }} - > - {qrText} - </div> - </Fragment> - )} - </Fragment> - ) : undefined} </FormProvider> <div class="buttons is-right mt-5"> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx new file mode 100644 index 000000000..984880752 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx @@ -0,0 +1,165 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { AsyncButton } from "../../../components/exception/AsyncButton.js"; +import { FormProvider } from "../../../components/form/FormProvider.js"; +import { Input } from "../../../components/form/Input.js"; +import { useInstanceContext } from "../../../context/instance.js"; + +interface Props { + instanceId: string; + currentToken: string | undefined; + onClearToken: () => void; + onNewToken: (s: string) => void; + onBack?: () => void; +} + +export function DetailPage({ instanceId, currentToken: oldToken, onBack, onNewToken, onClearToken }: Props): VNode { + type State = { old_token: string; new_token: string; repeat_token: string }; + const [form, setValue] = useState<Partial<State>>({ + old_token: "", + new_token: "", + repeat_token: "", + }); + const { i18n } = useTranslationContext(); + + const hasOldtoken = !!oldToken + const hasInputTheCorrectOldToken = hasOldtoken && oldToken !== form.old_token; + const errors = { + old_token: hasInputTheCorrectOldToken + ? i18n.str`is not the same as the current access token` + : undefined, + new_token: !form.new_token + ? i18n.str`cannot be empty` + : form.new_token === form.old_token + ? i18n.str`cannot be the same as the old token` + : undefined, + repeat_token: + form.new_token !== form.repeat_token + ? i18n.str`is not the same` + : undefined, + }; + + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + + const instance = useInstanceContext(); + + const text = i18n.str`You are updating the access token from instance with id ${instance.id}`; + + async function submitForm() { + if (hasErrors) return; + onNewToken(form.new_token as any) + } + + return ( + <div> + <section class="section"> + <section class="hero is-hero-bar"> + <div class="hero-body"> + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <span class="is-size-4"> + Instance id: <b>{instanceId}</b> + </span> + </div> + </div> + </div> + </div> + </section> + <hr /> + + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <FormProvider errors={errors} object={form} valueHandler={setValue}> + {hasOldtoken && ( + <Input<State> + name="old_token" + label={i18n.str`Current access token`} + tooltip={i18n.str`access token currently in use`} + inputType="password" + /> + )} + {!hasInputTheCorrectOldToken && <Fragment> + {hasOldtoken && <Fragment> + <p> + <i18n.Translate> + Clearing the access token will mean public access to the instance. + </i18n.Translate> + </p> + <div class="buttons is-right mt-5"> + <button + disabled={!!hasInputTheCorrectOldToken} + class="button" + onClick={onClearToken} + > + <i18n.Translate>Clear token</i18n.Translate> + </button> + </div> + </Fragment> + } + + <Input<State> + name="new_token" + label={i18n.str`New access token`} + tooltip={i18n.str`next access token to be used`} + inputType="password" + /> + <Input<State> + name="repeat_token" + label={i18n.str`Repeat access token`} + tooltip={i18n.str`confirm the same access token`} + inputType="password" + /> + </Fragment>} + </FormProvider> + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <AsyncButton + disabled={hasErrors} + data-tooltip={ + hasErrors + ? i18n.str`Need to complete marked fields` + : "confirm operation" + } + onClick={submitForm} + > + <i18n.Translate>Confirm change</i18n.Translate> + </AsyncButton> + </div> + </div> + <div class="column" /> + </div> + + </section> + </div> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx new file mode 100644 index 000000000..d5910361b --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx @@ -0,0 +1,90 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { ErrorType, HttpError, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { Loading } from "../../../components/exception/loading.js"; +import { MerchantBackend } from "../../../declaration.js"; +import { useInstanceAPI, useInstanceDetails } from "../../../hooks/instance.js"; +import { DetailPage } from "./DetailPage.js"; +import { useInstanceContext } from "../../../context/instance.js"; +import { useState } from "preact/hooks"; +import { NotificationCard } from "../../../components/menu/index.js"; +import { Notification } from "../../../utils/types.js"; +import { useBackendContext } from "../../../context/backend.js"; + +interface Props { + onUnauthorized: () => VNode; + onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; + onChange: () => void; + onNotFound: () => VNode; +} + +const PREFIX = "secret-token:" + +export default function Token({ + onLoadError, + onChange, + onUnauthorized, + onNotFound, +}: Props): VNode { + const { i18n } = useTranslationContext(); + + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { clearToken, setNewToken } = useInstanceAPI(); + const { token: rootToken } = useBackendContext(); + const { token: instanceToken, id, admin } = useInstanceContext(); + + const currentToken = !admin ? rootToken : instanceToken + const hasPrefix = currentToken !== undefined && currentToken.startsWith(PREFIX) + return ( + <Fragment> + <NotificationCard notification={notif} /> + <DetailPage + instanceId={id} + currentToken={hasPrefix ? currentToken.substring(PREFIX.length) : currentToken} + onClearToken={async (): Promise<void> => { + try { + await clearToken(); + onChange(); + } catch (error) { + if (error instanceof Error) { + setNotif({ + message: i18n.str`Failed to clear token`, + type: "ERROR", + description: error.message, + }); + } + } + }} + onNewToken={async (newToken): Promise<void> => { + try { + await setNewToken(`secret-token:${newToken}`); + onChange(); + } catch (error) { + if (error instanceof Error) { + setNotif({ + message: i18n.str`Failed to set new token`, + type: "ERROR", + description: error.message, + }); + } + } + }} + /> + </Fragment> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/token/stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/token/stories.tsx new file mode 100644 index 000000000..5f0f56f2d --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/token/stories.tsx @@ -0,0 +1,28 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { DetailPage as TestedComponent } from "./DetailPage.js"; + +export default { + title: "Pages/Token", + component: TestedComponent, +}; + diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx index f218f4ead..25551a031 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx @@ -28,6 +28,7 @@ import { useInstanceDetails } from "../../../../hooks/instance.js"; import { useTransferAPI } from "../../../../hooks/transfer.js"; import { Notification } from "../../../../utils/types.js"; import { CreatePage } from "./CreatePage.js"; +import { useBankAccountDetails, useInstanceBankAccounts } from "../../../../hooks/bank.js"; export type Entity = MerchantBackend.Transfers.TransferInformation; interface Props { @@ -39,7 +40,7 @@ export default function CreateTransfer({ onConfirm, onBack }: Props): VNode { const { informTransfer } = useTransferAPI(); const [notif, setNotif] = useState<Notification | undefined>(undefined); const { i18n } = useTranslationContext(); - const instance = useInstanceDetails(); + const instance = useInstanceBankAccounts(); const accounts = !instance.ok ? [] : instance.data.accounts.map((a) => a.payto_uri); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx index a2e93d598..1c464cbc7 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx @@ -24,6 +24,7 @@ import { format } from "date-fns"; import { h, VNode } from "preact"; import { StateUpdater, useState } from "preact/hooks"; import { MerchantBackend, WithId } from "../../../../declaration.js"; +import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; type Entity = MerchantBackend.Transfers.TransferDetails & WithId; @@ -56,7 +57,7 @@ export function CardTable({ <header class="card-header"> <p class="card-header-title"> <span class="icon"> - <i class="mdi mdi-bank" /> + <i class="mdi mdi-arrow-left-right" /> </span> <i18n.Translate>Transfers</i18n.Translate> </p> @@ -121,6 +122,7 @@ function Table({ hasMoreBefore, }: TableProps): VNode { const { i18n } = useTranslationContext(); + const [settings] = useSettings(); return ( <div class="table-container"> {onLoadMoreBefore && ( @@ -175,9 +177,9 @@ function Table({ ? i.execution_time.t_s == "never" ? i18n.str`never` : format( - i.execution_time.t_s * 1000, - "yyyy/MM/dd HH:mm:ss", - ) + i.execution_time.t_s * 1000, + datetimeFormatForSettings(settings), + ) : i18n.str`unknown`} </td> <td> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx index 29e860342..1bc1673ba 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx @@ -28,6 +28,7 @@ import { useInstanceDetails } from "../../../../hooks/instance.js"; import { useInstanceTransfers } from "../../../../hooks/transfer.js"; import { ListPage } from "./ListPage.js"; import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { useInstanceBankAccounts } from "../../../../hooks/bank.js"; interface Props { onUnauthorized: () => VNode; @@ -51,7 +52,7 @@ export default function ListTransfer({ const [position, setPosition] = useState<string | undefined>(undefined); - const instance = useInstanceDetails(); + const instance = useInstanceBankAccounts(); const accounts = !instance.ok ? [] : instance.data.accounts.map((a) => a.payto_uri); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/Update.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/Update.stories.tsx index 045c96c2c..817a7025c 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/update/Update.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/update/Update.stories.tsx @@ -42,17 +42,15 @@ function createExample<Props>( export const Example = createExample(TestedComponent, { selected: { - accounts: [], name: "name", auth: { method: "external" }, address: {}, + user_type: "business", + use_stefan: true, jurisdiction: {}, - default_max_deposit_fee: "TESTKUDOS:2", - default_max_wire_fee: "TESTKUDOS:1", default_pay_delay: { d_us: 1000 * 1000, //one second }, - default_wire_fee_amortization: 1, default_wire_transfer_delay: { d_us: 1000 * 1000, //one second }, diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx index 547b40f07..a1c608f15 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx @@ -19,7 +19,6 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { Amounts } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { useState } from "preact/hooks"; @@ -29,10 +28,8 @@ import { FormProvider, } from "../../../components/form/FormProvider.js"; import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js"; -import { UpdateTokenModal } from "../../../components/modal/index.js"; import { useInstanceContext } from "../../../context/instance.js"; import { MerchantBackend } from "../../../declaration.js"; -import { PAYTO_REGEX } from "../../../utils/constants.js"; import { undefinedIfEmpty } from "../../../utils/table.js"; type Entity = MerchantBackend.Instances.InstanceReconfigurationMessage & { @@ -53,23 +50,23 @@ interface Props { function convert( from: MerchantBackend.Instances.QueryInstancesResponse, ): Entity { - const { accounts: qAccounts, ...rest } = from; - const accounts = qAccounts - .filter((a) => a.active) - .map( - (a) => - ({ - payto_uri: a.payto_uri, - credit_facade_url: a.credit_facade_url, - credit_facade_credentials: a.credit_facade_credentials, - } as MerchantBackend.Instances.MerchantBankAccount), - ); + const { ...rest } = from; + // const accounts = qAccounts + // .filter((a) => a.active) + // .map( + // (a) => + // ({ + // payto_uri: a.payto_uri, + // credit_facade_url: a.credit_facade_url, + // credit_facade_credentials: a.credit_facade_credentials, + // } as MerchantBackend.Instances.MerchantBankAccount), + // ); const defaults = { - default_wire_fee_amortization: 1, + use_stefan: false, default_pay_delay: { d_us: 2 * 1000 * 1000 * 60 * 60 }, //two hours default_wire_transfer_delay: { d_us: 2 * 1000 * 1000 * 60 * 60 * 2 }, //two hours }; - return { ...defaults, ...rest, accounts }; + return { ...defaults, ...rest }; } function getTokenValuePart(t?: string): string | undefined { @@ -85,21 +82,21 @@ export function UpdatePage({ selected, onBack, }: Props): VNode { - const { id, token } = useInstanceContext(); - const currentTokenValue = getTokenValuePart(token); - - function updateToken(token: string | undefined | null) { - const value = - token && token.startsWith("secret-token:") - ? token.substring("secret-token:".length) - : token; - - if (!token) { - onChangeAuth({ method: "external" }); - } else { - onChangeAuth({ method: "token", token: `secret-token:${value}` }); - } - } + const { id } = useInstanceContext(); + // const currentTokenValue = getTokenValuePart(token); + + // function updateToken(token: string | undefined | null) { + // const value = + // token && token.startsWith("secret-token:") + // ? token.substring("secret-token:".length) + // : token; + + // if (!token) { + // onChangeAuth({ method: "external" }); + // } else { + // onChangeAuth({ method: "token", token: `secret-token:${value}` }); + // } + // } const [value, valueHandler] = useState<Partial<Entity>>(convert(selected)); @@ -110,35 +107,7 @@ export function UpdatePage({ user_type: !value.user_type ? i18n.str`required` : value.user_type !== "business" && value.user_type !== "individual" - ? i18n.str`should be business or individual` - : undefined, - accounts: - !value.accounts || !value.accounts.length - ? i18n.str`required` - : undefinedIfEmpty( - value.accounts.map((p) => { - return !PAYTO_REGEX.test(p.payto_uri) - ? i18n.str`is not valid` - : undefined; - }), - ), - default_max_deposit_fee: !value.default_max_deposit_fee - ? i18n.str`required` - : !Amounts.parse(value.default_max_deposit_fee) - ? i18n.str`invalid format` - : undefined, - default_max_wire_fee: !value.default_max_wire_fee - ? i18n.str`required` - : !Amounts.parse(value.default_max_wire_fee) - ? i18n.str`invalid format` - : undefined, - default_wire_fee_amortization: - value.default_wire_fee_amortization === undefined - ? i18n.str`required` - : isNaN(value.default_wire_fee_amortization) - ? i18n.str`is not a number` - : value.default_wire_fee_amortization < 1 - ? i18n.str`must be 1 or greater` + ? i18n.str`should be business or individual` : undefined, default_pay_delay: !value.default_pay_delay ? i18n.str`required` @@ -163,10 +132,11 @@ export function UpdatePage({ const hasErrors = Object.keys(errors).some( (k) => (errors as any)[k] !== undefined, ); + const submit = async (): Promise<void> => { await onUpdate(value as Entity); }; - const [active, setActive] = useState(false); + // const [active, setActive] = useState(false); return ( <div> @@ -181,7 +151,7 @@ export function UpdatePage({ </span> </div> </div> - <div class="level-right"> + {/* <div class="level-right"> <div class="level-item"> <h1 class="title"> <button @@ -200,33 +170,11 @@ export function UpdatePage({ </button> </h1> </div> - </div> + </div> */} </div> </div> </section> - <div class="columns"> - <div class="column" /> - <div class="column is-four-fifths"> - {active && ( - <UpdateTokenModal - oldToken={currentTokenValue} - onCancel={() => { - setActive(false); - }} - onClear={() => { - updateToken(null); - setActive(false); - }} - onConfirm={(newToken) => { - updateToken(newToken); - setActive(false); - }} - /> - )} - </div> - <div class="column" /> - </div> <hr /> <div class="columns"> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/validators/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/validators/create/Create.stories.tsx new file mode 100644 index 000000000..56762db7b --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/validators/create/Create.stories.tsx @@ -0,0 +1,28 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { CreatePage as TestedComponent } from "./CreatePage.js"; + +export default { + title: "Pages/Validators/Create", + component: TestedComponent, +}; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatePage.tsx new file mode 100644 index 000000000..bdc86d226 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatePage.tsx @@ -0,0 +1,195 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; +import { + FormErrors, + FormProvider, +} from "../../../../components/form/FormProvider.js"; +import { Input } from "../../../../components/form/Input.js"; +import { InputCurrency } from "../../../../components/form/InputCurrency.js"; +import { InputDuration } from "../../../../components/form/InputDuration.js"; +import { InputNumber } from "../../../../components/form/InputNumber.js"; +import { useBackendContext } from "../../../../context/backend.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { InputSelector } from "../../../../components/form/InputSelector.js"; +import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; +import { isBase32RFC3548Charset, randomBase32Key } from "../../../../utils/crypto.js"; +import { QR } from "../../../../components/exception/QR.js"; +import { useInstanceContext } from "../../../../context/instance.js"; + +type Entity = MerchantBackend.OTP.OtpDeviceAddDetails; + +interface Props { + onCreate: (d: Entity) => Promise<void>; + onBack?: () => void; +} + +const algorithms = [0, 1, 2]; +const algorithmsNames = ["off", "30s 8d TOTP-SHA1", "30s 8d eTOTP-SHA1"]; + + +export function CreatePage({ onCreate, onBack }: Props): VNode { + const { i18n } = useTranslationContext(); + const backend = useBackendContext(); + + const [state, setState] = useState<Partial<Entity>>({}); + + const [showKey, setShowKey] = useState(false); + + const errors: FormErrors<Entity> = { + otp_device_id: !state.otp_device_id ? i18n.str`required` + : !/[a-zA-Z0-9]*/.test(state.otp_device_id) + ? i18n.str`no valid. only characters and numbers` + : undefined, + otp_algorithm: !state.otp_algorithm ? i18n.str`required` : undefined, + otp_key: !state.otp_key ? i18n.str`required` : + !isBase32RFC3548Charset(state.otp_key) + ? i18n.str`just letters and numbers from 2 to 7` + : state.otp_key.length !== 32 + ? i18n.str`size of the key should be 32` + : undefined, + otp_description: !state.otp_description ? i18n.str`required` + : !/[a-zA-Z0-9]*/.test(state.otp_description) + ? i18n.str`no valid. only characters and numbers` + : undefined, + + }; + + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + + const submitForm = () => { + if (hasErrors) return Promise.reject(); + return onCreate(state as any); + }; + + return ( + <div> + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <FormProvider + object={state} + valueHandler={setState} + errors={errors} + > + <Input<Entity> + name="otp_device_id" + label={i18n.str`ID`} + tooltip={i18n.str`Internal id on the system`} + /> + <Input<Entity> + name="otp_description" + label={i18n.str`Descripiton`} + tooltip={i18n.str`Useful to identify the device physically`} + /> + <InputSelector<Entity> + name="otp_algorithm" + label={i18n.str`Verification algorithm`} + tooltip={i18n.str`Algorithm to use to verify transaction in offline mode`} + values={algorithms} + toStr={(v) => algorithmsNames[v]} + fromStr={(v) => Number(v)} + /> + {state.otp_algorithm && state.otp_algorithm > 0 ? ( + <Fragment> + <InputWithAddon<Entity> + name="otp_key" + label={i18n.str`Device key`} + inputType={showKey ? "text" : "password"} + help="Be sure to be very hard to guess or use the random generator" + tooltip={i18n.str`Your device need to have exactly the same value`} + fromStr={(v) => v.toUpperCase()} + addonAfter={ + <span class="icon"> + {showKey ? ( + <i class="mdi mdi-eye" /> + ) : ( + <i class="mdi mdi-eye-off" /> + )} + </span> + } + side={ + <span style={{ display: "flex" }}> + <button + data-tooltip={i18n.str`generate random secret key`} + class="button is-info mr-3" + onClick={(e) => { + setState((s) => ({ ...s, otp_key: randomBase32Key() })); + }} + > + <i18n.Translate>random</i18n.Translate> + </button> + <button + data-tooltip={ + showKey + ? i18n.str`show secret key` + : i18n.str`hide secret key` + } + class="button is-info mr-3" + onClick={(e) => { + setShowKey(!showKey); + }} + > + {showKey ? ( + <i18n.Translate>hide</i18n.Translate> + ) : ( + <i18n.Translate>show</i18n.Translate> + )} + </button> + </span> + } + /> + </Fragment> + ) : undefined} + </FormProvider> + + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <AsyncButton + disabled={hasErrors} + data-tooltip={ + hasErrors + ? i18n.str`Need to complete marked fields` + : "confirm operation" + } + onClick={submitForm} + > + <i18n.Translate>Confirm</i18n.Translate> + </AsyncButton> + </div> + </div> + <div class="column" /> + </div> + </section> + </div> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatedSuccessfully.tsx new file mode 100644 index 000000000..3ad3cb3a3 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatedSuccessfully.tsx @@ -0,0 +1,104 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { QR } from "../../../../components/exception/QR.js"; +import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js"; +import { useBackendContext } from "../../../../context/backend.js"; +import { useInstanceContext } from "../../../../context/instance.js"; +import { MerchantBackend } from "../../../../declaration.js"; + +type Entity = MerchantBackend.OTP.OtpDeviceAddDetails; + +interface Props { + entity: Entity; + onConfirm: () => void; +} + +function isNotUndefined<X>(x: X | undefined): x is X { + return !!x; +} + +export function CreatedSuccessfully({ + entity, + onConfirm, +}: Props): VNode { + const { i18n } = useTranslationContext(); + const backend = useBackendContext(); + const { id: instanceId } = useInstanceContext(); + const issuer = new URL(backend.url).hostname; + const qrText = `otpauth://totp/${instanceId}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key}`; + const qrTextSafe = `otpauth://totp/${instanceId}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key.substring(0, 6)}...`; + + return ( + <Template onConfirm={onConfirm} > + <p class="is-size-5"> + <i18n.Translate> + You can scan the next QR code with your device or safe the key before continue. + </i18n.Translate> + </p> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">ID</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input + readonly + class="input" + value={entity.otp_device_id} + /> + </p> + </div> + </div> + </div> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"><i18n.Translate>Description</i18n.Translate></label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input + class="input" + readonly + value={entity.otp_description} + /> + </p> + </div> + </div> + </div> + <QR + text={qrText} + /> + <div + style={{ + color: "grey", + fontSize: "small", + width: 200, + textAlign: "center", + margin: "auto", + wordBreak: "break-all", + }} + > + {qrTextSafe} + </div> + </Template> + ); +} + diff --git a/packages/merchant-backoffice-ui/src/paths/instance/validators/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/validators/create/index.tsx new file mode 100644 index 000000000..648846793 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/validators/create/index.tsx @@ -0,0 +1,70 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useWebhookAPI } from "../../../../hooks/webhooks.js"; +import { Notification } from "../../../../utils/types.js"; +import { CreatePage } from "./CreatePage.js"; +import { useOtpDeviceAPI } from "../../../../hooks/otp.js"; +import { CreatedSuccessfully } from "./CreatedSuccessfully.js"; + +export type Entity = MerchantBackend.OTP.OtpDeviceAddDetails; +interface Props { + onBack?: () => void; + onConfirm: () => void; +} + +export default function CreateValidator({ onConfirm, onBack }: Props): VNode { + const { createOtpDevice } = useOtpDeviceAPI(); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); + const [created, setCreated] = useState<MerchantBackend.OTP.OtpDeviceAddDetails | null>(null) + + if (created) { + return <CreatedSuccessfully entity={created} onConfirm={onConfirm} /> + } + + return ( + <> + <NotificationCard notification={notif} /> + <CreatePage + onBack={onBack} + onCreate={(request: Entity) => { + return createOtpDevice(request) + .then((d) => { + setCreated(request) + }) + .catch((error) => { + setNotif({ + message: i18n.str`could not create device`, + type: "ERROR", + description: error.message, + }); + }); + }} + /> + </> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/validators/list/List.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/validators/list/List.stories.tsx new file mode 100644 index 000000000..3aa491c53 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/validators/list/List.stories.tsx @@ -0,0 +1,28 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { FunctionalComponent, h } from "preact"; +import { ListPage as TestedComponent } from "./ListPage.js"; + +export default { + title: "Pages/Validators/List", + component: TestedComponent, +}; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/validators/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/validators/list/ListPage.tsx new file mode 100644 index 000000000..4efee9781 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/validators/list/ListPage.tsx @@ -0,0 +1,64 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode } from "preact"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { CardTable } from "./Table.js"; + +export interface Props { + devices: MerchantBackend.OTP.OtpDeviceEntry[]; + onLoadMoreBefore?: () => void; + onLoadMoreAfter?: () => void; + onCreate: () => void; + onDelete: (e: MerchantBackend.OTP.OtpDeviceEntry) => void; + onSelect: (e: MerchantBackend.OTP.OtpDeviceEntry) => void; +} + +export function ListPage({ + devices, + onCreate, + onDelete, + onSelect, + onLoadMoreBefore, + onLoadMoreAfter, +}: Props): VNode { + const form = { payto_uri: "" }; + + const { i18n } = useTranslationContext(); + return ( + <section class="section is-main-section"> + <CardTable + devices={devices.map((o) => ({ + ...o, + id: String(o.otp_device_id), + }))} + onCreate={onCreate} + onDelete={onDelete} + onSelect={onSelect} + onLoadMoreBefore={onLoadMoreBefore} + hasMoreBefore={!onLoadMoreBefore} + onLoadMoreAfter={onLoadMoreAfter} + hasMoreAfter={!onLoadMoreAfter} + /> + </section> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/validators/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/validators/list/Table.tsx new file mode 100644 index 000000000..b639a6134 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/validators/list/Table.tsx @@ -0,0 +1,213 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { StateUpdater, useState } from "preact/hooks"; +import { MerchantBackend } from "../../../../declaration.js"; + +type Entity = MerchantBackend.OTP.OtpDeviceEntry; + +interface Props { + devices: Entity[]; + onDelete: (e: Entity) => void; + onSelect: (e: Entity) => void; + onCreate: () => void; + onLoadMoreBefore?: () => void; + hasMoreBefore?: boolean; + hasMoreAfter?: boolean; + onLoadMoreAfter?: () => void; +} + +export function CardTable({ + devices, + onCreate, + onDelete, + onSelect, + onLoadMoreAfter, + onLoadMoreBefore, + hasMoreAfter, + hasMoreBefore, +}: Props): VNode { + const [rowSelection, rowSelectionHandler] = useState<string[]>([]); + + const { i18n } = useTranslationContext(); + + return ( + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"> + <i class="mdi mdi-newspaper" /> + </span> + <i18n.Translate>OTP Devices</i18n.Translate> + </p> + <div class="card-header-icon" aria-label="more options"> + <span + class="has-tooltip-left" + data-tooltip={i18n.str`add new devices`} + > + <button class="button is-info" type="button" onClick={onCreate}> + <span class="icon is-small"> + <i class="mdi mdi-plus mdi-36px" /> + </span> + </button> + </span> + </div> + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + {devices.length > 0 ? ( + <Table + instances={devices} + onDelete={onDelete} + onSelect={onSelect} + rowSelection={rowSelection} + rowSelectionHandler={rowSelectionHandler} + onLoadMoreAfter={onLoadMoreAfter} + onLoadMoreBefore={onLoadMoreBefore} + hasMoreAfter={hasMoreAfter} + hasMoreBefore={hasMoreBefore} + /> + ) : ( + <EmptyTable /> + )} + </div> + </div> + </div> + </div> + ); +} +interface TableProps { + rowSelection: string[]; + instances: Entity[]; + onDelete: (e: Entity) => void; + onSelect: (e: Entity) => void; + rowSelectionHandler: StateUpdater<string[]>; + onLoadMoreBefore?: () => void; + hasMoreBefore?: boolean; + hasMoreAfter?: boolean; + onLoadMoreAfter?: () => void; +} + +function toggleSelected<T>(id: T): (prev: T[]) => T[] { + return (prev: T[]): T[] => + prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id); +} + +function Table({ + instances, + onLoadMoreAfter, + onDelete, + onSelect, + onLoadMoreBefore, + hasMoreAfter, + hasMoreBefore, +}: TableProps): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="table-container"> + {onLoadMoreBefore && ( + <button + class="button is-fullwidth" + data-tooltip={i18n.str`load more devices before the first one`} + disabled={!hasMoreBefore} + onClick={onLoadMoreBefore} + > + <i18n.Translate>load newer devices</i18n.Translate> + </button> + )} + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th> + <i18n.Translate>ID</i18n.Translate> + </th> + <th> + <i18n.Translate>Description</i18n.Translate> + </th> + <th /> + </tr> + </thead> + <tbody> + {instances.map((i) => { + return ( + <tr key={i.otp_device_id}> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.otp_device_id} + </td> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.otp_device_id} + </td> + <td class="is-actions-cell right-sticky"> + <div class="buttons is-right"> + <button + class="button is-danger is-small has-tooltip-left" + data-tooltip={i18n.str`delete selected devices from the database`} + onClick={() => onDelete(i)} + > + Delete + </button> + </div> + </td> + </tr> + ); + })} + </tbody> + </table> + {onLoadMoreAfter && ( + <button + class="button is-fullwidth" + data-tooltip={i18n.str`load more devices after the last one`} + disabled={!hasMoreAfter} + onClick={onLoadMoreAfter} + > + <i18n.Translate>load older devices</i18n.Translate> + </button> + )} + </div> + ); +} + +function EmptyTable(): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="content has-text-grey has-text-centered"> + <p> + <span class="icon is-large"> + <i class="mdi mdi-emoticon-sad mdi-48px" /> + </span> + </p> + <p> + <i18n.Translate> + There is no devices yet, add more pressing the + sign + </i18n.Translate> + </p> + </div> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/validators/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/validators/list/index.tsx new file mode 100644 index 000000000..8837c848b --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/validators/list/index.tsx @@ -0,0 +1,106 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { + ErrorType, + HttpError, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../../components/exception/loading.js"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useInstanceOtpDevices, useOtpDeviceAPI } from "../../../../hooks/otp.js"; +import { Notification } from "../../../../utils/types.js"; +import { ListPage } from "./ListPage.js"; + +interface Props { + onUnauthorized: () => VNode; + onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; + onNotFound: () => VNode; + onCreate: () => void; + onSelect: (id: string) => void; +} + +export default function ListValidators({ + onUnauthorized, + onLoadError, + onCreate, + onSelect, + onNotFound, +}: Props): VNode { + const [position, setPosition] = useState<string | undefined>(undefined); + const { i18n } = useTranslationContext(); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { deleteOtpDevice } = useOtpDeviceAPI(); + const result = useInstanceOtpDevices({ position }, (id) => setPosition(id)); + + if (result.loading) return <Loading />; + if (!result.ok) { + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(result); + } + + return ( + <Fragment> + <NotificationCard notification={notif} /> + + <ListPage + devices={result.data.otp_devices} + onLoadMoreBefore={ + result.isReachingStart ? result.loadMorePrev : undefined + } + onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined} + onCreate={onCreate} + onSelect={(e) => { + onSelect(e.otp_device_id); + }} + onDelete={(e: MerchantBackend.OTP.OtpDeviceEntry) => + deleteOtpDevice(e.otp_device_id) + .then(() => + setNotif({ + message: i18n.str`validator delete successfully`, + type: "SUCCESS", + }), + ) + .catch((error) => + setNotif({ + message: i18n.str`could not delete the validator`, + type: "ERROR", + description: error.message, + }), + ) + } + /> + </Fragment> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/validators/update/Update.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/validators/update/Update.stories.tsx new file mode 100644 index 000000000..fcb77b820 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/validators/update/Update.stories.tsx @@ -0,0 +1,32 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { UpdatePage as TestedComponent } from "./UpdatePage.js"; + +export default { + title: "Pages/Validators/Update", + component: TestedComponent, + argTypes: { + onUpdate: { action: "onUpdate" }, + onBack: { action: "onBack" }, + }, +}; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/validators/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/validators/update/UpdatePage.tsx new file mode 100644 index 000000000..585c12e11 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/validators/update/UpdatePage.tsx @@ -0,0 +1,185 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; +import { + FormErrors, + FormProvider, +} from "../../../../components/form/FormProvider.js"; +import { Input } from "../../../../components/form/Input.js"; +import { MerchantBackend, WithId } from "../../../../declaration.js"; +import { InputSelector } from "../../../../components/form/InputSelector.js"; +import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; +import { randomBase32Key } from "../../../../utils/crypto.js"; + +type Entity = MerchantBackend.OTP.OtpDevicePatchDetails & WithId; + +interface Props { + onUpdate: (d: Entity) => Promise<void>; + onBack?: () => void; + device: Entity; +} +const algorithms = [0, 1, 2]; +const algorithmsNames = ["off", "30s 8d TOTP-SHA1", "30s 8d eTOTP-SHA1"]; +export function UpdatePage({ device, onUpdate, onBack }: Props): VNode { + const { i18n } = useTranslationContext(); + + const [state, setState] = useState<Partial<Entity>>(device); + const [showKey, setShowKey] = useState(false); + + const errors: FormErrors<Entity> = { + }; + + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + + const submitForm = () => { + if (hasErrors) return Promise.reject(); + return onUpdate(state as any); + }; + + return ( + <div> + <section class="section"> + <section class="hero is-hero-bar"> + <div class="hero-body"> + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <span class="is-size-4"> + Device: <b>{device.id}</b> + </span> + </div> + </div> + </div> + </div> + </section> + <hr /> + + <section class="section is-main-section"> + <div class="columns"> + <div class="column is-four-fifths"> + <FormProvider + object={state} + valueHandler={setState} + errors={errors} + > + <Input<Entity> + name="otp_description" + label={i18n.str`Description`} + tooltip={i18n.str`dddd`} + /> + <InputSelector<Entity> + name="otp_algorithm" + label={i18n.str`Verification algorithm`} + tooltip={i18n.str`Algorithm to use to verify transaction in offline mode`} + values={algorithms} + toStr={(v) => algorithmsNames[v]} + fromStr={(v) => Number(v)} + /> + {state.otp_algorithm && state.otp_algorithm > 0 ? ( + <Fragment> + <InputWithAddon<Entity> + name="otp_key" + label={i18n.str`Device key`} + readonly={state.otp_key === undefined} + inputType={showKey ? "text" : "password"} + help={state.otp_key === undefined ? "Not modified" : "Be sure to be very hard to guess or use the random generator"} + tooltip={i18n.str`Your device need to have exactly the same value`} + fromStr={(v) => v.toUpperCase()} + addonAfter={ + <span class="icon"> + {showKey ? ( + <i class="mdi mdi-eye" /> + ) : ( + <i class="mdi mdi-eye-off" /> + )} + </span> + } + side={ + state.otp_key === undefined ? <button + + onClick={(e) => { + setState((s) => ({ ...s, otp_key: "" })); + }} + class="button">change key</button> : + <span style={{ display: "flex" }}> + <button + data-tooltip={i18n.str`generate random secret key`} + class="button is-info mr-3" + onClick={(e) => { + setState((s) => ({ ...s, otp_key: randomBase32Key() })); + }} + > + <i18n.Translate>random</i18n.Translate> + </button> + <button + data-tooltip={ + showKey + ? i18n.str`show secret key` + : i18n.str`hide secret key` + } + class="button is-info mr-3" + onClick={(e) => { + setShowKey(!showKey); + }} + > + {showKey ? ( + <i18n.Translate>hide</i18n.Translate> + ) : ( + <i18n.Translate>show</i18n.Translate> + )} + </button> + </span> + } + /> + </Fragment> + ) : undefined} </FormProvider> + + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <AsyncButton + disabled={hasErrors} + data-tooltip={ + hasErrors + ? i18n.str`Need to complete marked fields` + : "confirm operation" + } + onClick={submitForm} + > + <i18n.Translate>Confirm</i18n.Translate> + </AsyncButton> + </div> + </div> + </div> + </section> + </section> + </div> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/validators/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/validators/update/index.tsx new file mode 100644 index 000000000..9a27ccfee --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/validators/update/index.tsx @@ -0,0 +1,102 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { + ErrorType, + HttpError, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../../components/exception/loading.js"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend, WithId } from "../../../../declaration.js"; +import { Notification } from "../../../../utils/types.js"; +import { UpdatePage } from "./UpdatePage.js"; +import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { useOtpDeviceAPI, useOtpDeviceDetails } from "../../../../hooks/otp.js"; + +export type Entity = MerchantBackend.OTP.OtpDevicePatchDetails & WithId; + +interface Props { + onBack?: () => void; + onConfirm: () => void; + onUnauthorized: () => VNode; + onNotFound: () => VNode; + onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; + vid: string; +} +export default function UpdateValidator({ + vid, + onConfirm, + onBack, + onUnauthorized, + onNotFound, + onLoadError, +}: Props): VNode { + const { updateOtpDevice } = useOtpDeviceAPI(); + const result = useOtpDeviceDetails(vid); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + + const { i18n } = useTranslationContext(); + + if (result.loading) return <Loading />; + if (!result.ok) { + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(result); + } + + return ( + <Fragment> + <NotificationCard notification={notif} /> + <UpdatePage + device={{ + id: vid, + otp_algorithm: result.data.otp_algorithm, + otp_description: result.data.device_description, + otp_key: undefined, + otp_ctr: result.data.otp_ctr + }} + onBack={onBack} + onUpdate={(data) => { + return updateOtpDevice(vid, data) + .then(onConfirm) + .catch((error) => { + setNotif({ + message: i18n.str`could not update template`, + type: "ERROR", + description: error.message, + }); + }); + }} + /> + </Fragment> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx index fd7b08875..124ced1f1 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx @@ -81,9 +81,6 @@ export function CardTable({ instances={webhooks} onDelete={onDelete} onSelect={onSelect} - onNewOrder={(d) => { - console.log("test", d); - }} rowSelection={rowSelection} rowSelectionHandler={rowSelectionHandler} onLoadMoreAfter={onLoadMoreAfter} @@ -104,7 +101,6 @@ interface TableProps { rowSelection: string[]; instances: Entity[]; onDelete: (e: Entity) => void; - onNewOrder: (e: Entity) => void; onSelect: (e: Entity) => void; rowSelectionHandler: StateUpdater<string[]>; onLoadMoreBefore?: () => void; @@ -122,7 +118,6 @@ function Table({ instances, onLoadMoreAfter, onDelete, - onNewOrder, onSelect, onLoadMoreBefore, hasMoreAfter, diff --git a/packages/merchant-backoffice-ui/src/paths/settings/index.tsx b/packages/merchant-backoffice-ui/src/paths/settings/index.tsx index 128450553..0d514f2df 100644 --- a/packages/merchant-backoffice-ui/src/paths/settings/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/settings/index.tsx @@ -1,10 +1,10 @@ -import { VNode, h } from "preact"; -import { LangSelector } from "../../components/menu/LangSelector.js"; import { useLang, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { VNode, h } from "preact"; +import { FormErrors, FormProvider } from "../../components/form/FormProvider.js"; +import { InputSelector } from "../../components/form/InputSelector.js"; import { InputToggle } from "../../components/form/InputToggle.js"; +import { LangSelector } from "../../components/menu/LangSelector.js"; import { Settings, useSettings } from "../../hooks/useSettings.js"; -import { FormErrors, FormProvider } from "../../components/form/FormProvider.js"; -import { useState } from "preact/hooks"; function getBrowserLang(): string | undefined { if (typeof window === "undefined") return undefined; @@ -24,7 +24,11 @@ export function Settings(): VNode { function valueHandler(s: (d: Partial<Settings>) => Partial<Settings>): void { const next = s(value) - updateValue("advanceOrderMode", next.advanceOrderMode ?? false) + const v: Settings = { + advanceOrderMode: next.advanceOrderMode ?? false, + dateFormat: next.dateFormat ?? "ymd" + } + updateValue(v) } return <div> @@ -32,41 +36,64 @@ export function Settings(): VNode { <div class="columns"> <div class="column" /> <div class="column is-four-fifths"> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label" style={{ width: 200 }}> - <i18n.Translate>Language</i18n.Translate> - <span class="icon has-tooltip-right" data-tooltip={"Force language setting instance of taking the browser"}> - <i class="mdi mdi-information" /> - </span> - </label> - </div> - <div class="field has-addons"> - <LangSelector /> - - {borwserLang !== undefined && <button - data-tooltip={i18n.str`generate random secret key`} - class="button is-info mr-3" - onClick={(e) => { - update(borwserLang.substring(0, 2)) + <div> + + <FormProvider<Settings> + name="settings" + errors={errors} + object={value} + valueHandler={valueHandler} + > + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + <i18n.Translate>Language</i18n.Translate> + <span class="icon has-tooltip-right" data-tooltip={"Force language setting instance of taking the browser"}> + <i class="mdi mdi-information" /> + </span> + </label> + </div> + <div class="field field-body has-addons is-flex-grow-3"> + <LangSelector /> + + {borwserLang !== undefined && <button + data-tooltip={i18n.str`generate random secret key`} + class="button is-info mr-2" + onClick={(e) => { + update(borwserLang.substring(0, 2)) + }} + > + <i18n.Translate>Set default</i18n.Translate> + </button>} + </div> + </div> + <InputToggle<Settings> + label={i18n.str`Advance order creation`} + tooltip={i18n.str`Shows more options in the order creation form`} + name="advanceOrderMode" + /> + <InputSelector<Settings> + name="dateFormat" + label={i18n.str`Date format`} + expand={true} + help={ + value.dateFormat === "dmy" ? "31/12/2001" : value.dateFormat === "mdy" ? "12/31/2001" : value.dateFormat === "ymd" ? "2001/12/31" : "" + } + toStr={(e) => { + if (e === "ymd") return "year month day" + if (e === "mdy") return "month day year" + if (e === "dmy") return "day month year" + return "choose one" }} - > - <i18n.Translate>Set default</i18n.Translate> - </button>} - </div> + values={[ + "ymd", + "mdy", + "dmy", + ]} + tooltip={i18n.str`how the date is going to be displayed`} + /> + </FormProvider> </div> - <FormProvider<Settings> - name="settings" - errors={errors} - object={value} - valueHandler={valueHandler} - > - <InputToggle<Settings> - label={i18n.str`Advance order creation`} - tooltip={i18n.str`Shows more options in the order creation form`} - name="advanceOrderMode" - /> - </FormProvider> </div> diff --git a/packages/merchant-backoffice-ui/src/schemas/index.ts b/packages/merchant-backoffice-ui/src/schemas/index.ts index 149761c55..4be77595b 100644 --- a/packages/merchant-backoffice-ui/src/schemas/index.ts +++ b/packages/merchant-backoffice-ui/src/schemas/index.ts @@ -123,7 +123,7 @@ export const InstanceSchema = yup.object().shape({ export const InstanceUpdateSchema = InstanceSchema.clone().omit(["id"]); export const InstanceCreateSchema = InstanceSchema.clone(); -export const AuthorizeTipSchema = yup.object().shape({ +export const AuthorizeRewardSchema = yup.object().shape({ justification: yup.string().required(), amount: yup .string() @@ -161,7 +161,7 @@ export const OrderCreateSchema = yup.object().shape({ currencyGreaterThan0, ), }), - extra: yup.string().test("extra", "is not a JSON format", stringIsValidJSON), + // extra: yup.object().test("extra", "is not a JSON format", stringIsValidJSON), payments: yup .object() .required() diff --git a/packages/merchant-backoffice-ui/tsconfig.json b/packages/merchant-backoffice-ui/tsconfig.json index 02c562f96..396f1e9e7 100644 --- a/packages/merchant-backoffice-ui/tsconfig.json +++ b/packages/merchant-backoffice-ui/tsconfig.json @@ -1,61 +1,58 @@ { - "compilerOptions": { - /* Basic Options */ - "target": "ES6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ - "module": "ESNext", /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ - "lib": [ - "es2021", - "dom" - ], /* Specify library files to be included in the compilation: */ - // "allowJs": true, /* Allow javascript files to be compiled. */ - // "checkJs": true, /* Report errors in .js files. */ - "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ - "jsxFactory": "h", /* Specify the JSX factory function to use when targeting react JSX emit, e.g. React.createElement or h. */ - "jsxFragmentFactory": "Fragment", // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#custom-jsx-factories - // "declaration": true, /* Generates corresponding '.d.ts' file. */ - // "sourceMap": true, /* Generates corresponding '.map' file. */ - // "outFile": "./", /* Concatenate and emit output to single file. */ - // "outDir": "./", /* Redirect output structure to the directory. */ - // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ - // "removeComments": true, /* Do not emit comments to output. */ - "noEmit": true, /* Do not emit outputs. */ - // "importHelpers": true, /* Import emit helpers from 'tslib'. */ - // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ - // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ - /* Strict Type-Checking Options */ - "strict": true, /* Enable all strict type-checking options. */ - // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* Enable strict null checks. */ - // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ - // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ - /* Additional Checks */ - // "noUnusedLocals": true, /* Report errors on unused locals. */ - // "noUnusedParameters": true, /* Report errors on unused parameters. */ - // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - /* Module Resolution Options */ - "moduleResolution": "node16", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ - "esModuleInterop": true, /* */ - // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ - // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ - // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ - // "typeRoots": [], /* List of folders to include type definitions from. */ - // "types": [], /* Type declaration files to be included in compilation. */ - // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ - // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ - /* Source Map Options */ - // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ - // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ - // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ - /* Experimental Options */ - // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ - // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ - /* Advanced Options */ - "skipLibCheck": true /* Skip type checking of declaration files. */ - }, - "include": [ - "src/**/*", - "tests/**/*" - ] -}
\ No newline at end of file + "compilerOptions": { + /* Basic Options */ + "target": "ES2020" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */, + "module": "Node16" /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, + "lib": [ + "es2020", + "dom" + ] /* Specify library files to be included in the compilation: */, + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, + "jsxFactory": "h" /* Specify the JSX factory function to use when targeting react JSX emit, e.g. React.createElement or h. */, + "jsxFragmentFactory": "Fragment", // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#custom-jsx-factories + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "removeComments": true, /* Do not emit comments to output. */ + "noEmit": true /* Do not emit outputs. */, + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + /* Strict Type-Checking Options */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + /* Module Resolution Options */ + "moduleResolution": "node16" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, + "esModuleInterop": true /* */, + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + /* Source Map Options */ + // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + /* Advanced Options */ + "skipLibCheck": true /* Skip type checking of declaration files. */ + }, + "include": ["src/**/*", "tests/**/*"] +} diff --git a/packages/pogen/package.json b/packages/pogen/package.json index ae1dfac3b..9160e699c 100644 --- a/packages/pogen/package.json +++ b/packages/pogen/package.json @@ -11,7 +11,7 @@ }, "devDependencies": { "po2json": "^0.4.5", - "typescript": "^5.1.3" + "typescript": "^5.2.2" }, "dependencies": { "@types/node": "^18.11.17", diff --git a/packages/pogen/tsconfig.json b/packages/pogen/tsconfig.json index 68225832d..74cdc6ad7 100644 --- a/packages/pogen/tsconfig.json +++ b/packages/pogen/tsconfig.json @@ -7,7 +7,7 @@ "incremental": true, "moduleResolution": "node", "sourceMap": true, - "lib": ["es6"], + "lib": ["ES2020"], "types": ["node"] }, "include": ["src/**/*.ts"] diff --git a/packages/taler-harness/package.json b/packages/taler-harness/package.json index 8916e5e9e..9f2b0d8e3 100644 --- a/packages/taler-harness/package.json +++ b/packages/taler-harness/package.json @@ -35,7 +35,7 @@ "esbuild": "^0.17.7", "prettier": "^2.8.8", "rimraf": "^3.0.2", - "typescript": "^5.1.3" + "typescript": "^5.2.2" }, "dependencies": { "@gnu-taler/taler-util": "workspace:*", diff --git a/packages/taler-harness/src/bench2.ts b/packages/taler-harness/src/bench2.ts index 48ac76b9f..53db6f6c1 100644 --- a/packages/taler-harness/src/bench2.ts +++ b/packages/taler-harness/src/bench2.ts @@ -57,7 +57,6 @@ export async function runBench2(configJson: any): Promise<void> { const http = createPlatformHttpLib({ enableThrottling: false, - allowHttp: true, }); const numIter = benchConf.iterations ?? 1; diff --git a/packages/taler-harness/src/bench3.ts b/packages/taler-harness/src/bench3.ts index c7eca90a8..0b5371af5 100644 --- a/packages/taler-harness/src/bench3.ts +++ b/packages/taler-harness/src/bench3.ts @@ -52,7 +52,6 @@ export async function runBench3(configJson: any): Promise<void> { const myHttpLib = createPlatformHttpLib({ enableThrottling: false, - allowHttp: true, }); const numIter = b3conf.iterations ?? 1; diff --git a/packages/taler-harness/src/env-full.ts b/packages/taler-harness/src/env-full.ts index 3a684db0b..210d38e32 100644 --- a/packages/taler-harness/src/env-full.ts +++ b/packages/taler-harness/src/env-full.ts @@ -79,7 +79,7 @@ export async function runEnvFull(t: GlobalTestState): Promise<void> { await merchant.start(); await merchant.pingUntilAvailable(); - await merchant.addInstance({ + await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", paytoUris: [getPayto("merchant-default")], @@ -88,7 +88,7 @@ export async function runEnvFull(t: GlobalTestState): Promise<void> { ), }); - await merchant.addInstance({ + await merchant.addInstanceWithWireAccount({ id: "minst1", name: "minst1", paytoUris: [getPayto("minst1")], diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts index 7db9d82bd..24e42099e 100644 --- a/packages/taler-harness/src/harness/harness.ts +++ b/packages/taler-harness/src/harness/harness.ts @@ -25,59 +25,45 @@ * Imports */ import { + AccountAddDetails, AmountJson, Amounts, - AmountString, - codecForMerchantOrderPrivateStatusResponse, - codecForMerchantPostOrderResponse, - codecForMerchantReserveCreateConfirmation, + BankAccessApiClient, Configuration, CoreApiResponse, - createEddsaKeyPair, Duration, - eddsaGetPublic, EddsaKeyPair, + Logger, + MerchantInstanceConfig, + PartialMerchantInstanceConfig, + TalerError, + WalletNotification, + createEddsaKeyPair, + eddsaGetPublic, encodeCrock, hash, j2s, - Logger, - MerchantInstancesResponse, - MerchantOrderPrivateStatusResponse, - MerchantPostOrderRequest, - MerchantPostOrderResponse, - MerchantReserveCreateConfirmation, - MerchantTemplateAddDetails, parsePaytoUri, stringToBytes, - TalerError, - TalerProtocolDuration, - RewardCreateConfirmation, - RewardCreateRequest, - TippingReserveStatus, - WalletNotification, - codecForAny, } from "@gnu-taler/taler-util"; import { + HttpRequestLibrary, createPlatformHttpLib, expectSuccessResponseOrThrow, - readSuccessResponseJsonOrThrow, } from "@gnu-taler/taler-util/http"; import { - BankApi, - BankServiceHandle, - HarnessExchangeBankAccount, - openPromise, WalletCoreApiClient, WalletCoreRequestType, WalletCoreResponseType, WalletOperations, + openPromise, } from "@gnu-taler/taler-wallet-core"; import { + RemoteWallet, + WalletNotificationWaiter, createRemoteWallet, getClientFromRemoteWallet, makeNotificationWaiter, - RemoteWallet, - WalletNotificationWaiter, } from "@gnu-taler/taler-wallet-core/remote"; import { deepStrictEqual } from "assert"; import { ChildProcess, spawn } from "child_process"; @@ -383,7 +369,11 @@ export class GlobalTestState { logger.warn(`could not start process (${command})`, err); }); proc.on("exit", (code, signal) => { - logger.warn(`process ${logName} exited ${j2s({ code, signal })}`); + if (code == 0 && signal == null) { + logger.info(`process ${logName} exited with success`); + } else { + logger.warn(`process ${logName} exited ${j2s({ code, signal })}`); + } }); const stderrLogFileName = this.testDir + `/${logName}-stderr.log`; const stderrLog = fs.createWriteStream(stderrLogFileName, { @@ -578,6 +568,13 @@ class BankServiceBase { ) {} } +export interface HarnessExchangeBankAccount { + accountName: string; + accountPassword: string; + accountPaytoUri: string; + wireGatewayApiBaseUrl: string; +} + /** * Implementation of the bank service using the "taler-fakebank-run" tool. */ @@ -587,7 +584,7 @@ export class FakebankService { proc: ProcessWrapper | undefined; - http = createPlatformHttpLib({ allowHttp: true, enableThrottling: false }); + http = createPlatformHttpLib({ enableThrottling: false }); // We store "created" accounts during setup and // register them after startup. @@ -695,13 +692,9 @@ export class FakebankService "bank", ); await this.pingUntilAvailable(); + const bankClient = new BankAccessApiClient(this.bankAccessApiBaseUrl); for (const acc of this.accounts) { - await BankApi.registerAccount( - this, - acc.accountName, - acc.accountPassword, - {}, - ); + await bankClient.registerAccount(acc.accountName, acc.accountPassword); } } @@ -714,6 +707,11 @@ export class FakebankService // Use libeufin bank instead of pybank. const useLibeufinBank = false; +export interface BankServiceHandle { + readonly bankAccessApiBaseUrl: string; + readonly http: HttpRequestLibrary; +} + export type BankService = BankServiceHandle; export const BankService = FakebankService; @@ -760,19 +758,19 @@ export class ExchangeService implements ExchangeServiceInterface { return new ExchangeService(gc, ec, cfgFilename, keyPair); } - private currentTimetravel: Duration | undefined; + private currentTimetravelOffsetMs: number | undefined; - setTimetravel(t: Duration | undefined): void { + setTimetravel(t: number | undefined): void { if (this.isRunning()) { throw Error("can't set time travel while the exchange is running"); } - this.currentTimetravel = t; + this.currentTimetravelOffsetMs = t; } private get timetravelArg(): string | undefined { - if (this.currentTimetravel && this.currentTimetravel.d_ms !== "forever") { + if (this.currentTimetravelOffsetMs != null) { // Convert to microseconds - return `--timetravel=+${this.currentTimetravel.d_ms * 1000}`; + return `--timetravel=+${this.currentTimetravelOffsetMs * 1000}`; } return undefined; } @@ -1334,282 +1332,19 @@ export interface MerchantConfig { overrideTestDir?: string; } -export interface PrivateOrderStatusQuery { - instance?: string; - orderId: string; - sessionId?: string; -} - export interface MerchantServiceInterface { makeInstanceBaseUrl(instanceName?: string): string; readonly port: number; readonly name: string; } -export interface DeleteTippingReserveArgs { - reservePub: string; - purge?: boolean; -} - /** * Default HTTP client handle for the integration test harness. */ export const harnessHttpLib = createPlatformHttpLib({ - allowHttp: true, enableThrottling: false, }); -export class MerchantApiClient { - constructor( - private baseUrl: string, - public readonly auth: MerchantAuthConfiguration, - ) {} - - httpClient = createPlatformHttpLib({ allowHttp: true, enableThrottling: false }); - - async changeAuth(auth: MerchantAuthConfiguration): Promise<void> { - const url = new URL("private/auth", this.baseUrl); - const res = await this.httpClient.fetch(url.href, { - method: "POST", - body: auth, - headers: this.makeAuthHeader(), - }); - await expectSuccessResponseOrThrow(res); - } - - async deleteTippingReserve(req: DeleteTippingReserveArgs): Promise<void> { - const url = new URL(`private/reserves/${req.reservePub}`, this.baseUrl); - if (req.purge) { - url.searchParams.set("purge", "YES"); - } - const resp = await this.httpClient.fetch(url.href, { - method: "DELETE", - headers: this.makeAuthHeader(), - }); - logger.info(`delete status: ${resp.status}`); - return; - } - - async createTippingReserve( - req: CreateMerchantTippingReserveRequest, - ): Promise<MerchantReserveCreateConfirmation> { - const url = new URL("private/reserves", this.baseUrl); - const resp = await this.httpClient.fetch(url.href, { - method: "POST", - body: req, - headers: this.makeAuthHeader(), - }); - const respData = readSuccessResponseJsonOrThrow( - resp, - codecForMerchantReserveCreateConfirmation(), - ); - return respData; - } - - async getPrivateInstanceInfo(): Promise<any> { - console.log(this.makeAuthHeader()); - const url = new URL("private", this.baseUrl); - logger.info(`request url ${url.href}`); - const resp = await this.httpClient.fetch(url.href, { - method: "GET", - headers: this.makeAuthHeader(), - }); - return await resp.json(); - } - - async getPrivateTipReserves(): Promise<TippingReserveStatus> { - console.log(this.makeAuthHeader()); - const url = new URL("private/reserves", this.baseUrl); - const resp = await this.httpClient.fetch(url.href, { - method: "GET", - headers: this.makeAuthHeader(), - }); - // FIXME: Validate! - return await resp.json(); - } - - async deleteInstance(instanceId: string) { - const url = new URL(`management/instances/${instanceId}`, this.baseUrl); - const resp = await this.httpClient.fetch(url.href, { - method: "DELETE", - headers: this.makeAuthHeader(), - }); - await expectSuccessResponseOrThrow(resp); - } - - async createInstance(req: MerchantInstanceConfig): Promise<void> { - const url = new URL("management/instances", this.baseUrl); - await this.httpClient.fetch(url.href, { - method: "POST", - body: req, - headers: this.makeAuthHeader(), - }); - } - - async getInstances(): Promise<MerchantInstancesResponse> { - const url = new URL("management/instances", this.baseUrl); - const resp = await this.httpClient.fetch(url.href, { - headers: this.makeAuthHeader(), - }); - return resp.json(); - } - - async getInstanceFullDetails(instanceId: string): Promise<any> { - const url = new URL(`management/instances/${instanceId}`, this.baseUrl); - try { - const resp = await this.httpClient.fetch(url.href, { - headers: this.makeAuthHeader(), - }); - return resp.json(); - } catch (e) { - throw e; - } - } - - makeAuthHeader(): Record<string, string> { - switch (this.auth.method) { - case "external": - return {}; - case "token": - return { - Authorization: `Bearer ${this.auth.token}`, - }; - } - } -} - -/** - * FIXME: This should be deprecated in favor of MerchantApiClient - * - * @deprecated use MerchantApiClient instead - */ -export namespace MerchantPrivateApi { - export async function createOrder( - merchantService: MerchantServiceInterface, - instanceName: string, - req: MerchantPostOrderRequest, - withAuthorization: WithAuthorization = {}, - ): Promise<MerchantPostOrderResponse> { - const baseUrl = merchantService.makeInstanceBaseUrl(instanceName); - let url = new URL("private/orders", baseUrl); - const resp = await harnessHttpLib.fetch(url.href, { - method: "POST", - body: req, - headers: withAuthorization as Record<string, string>, - }); - return readSuccessResponseJsonOrThrow( - resp, - codecForMerchantPostOrderResponse(), - ); - } - - export async function createTemplate( - merchantService: MerchantServiceInterface, - instanceName: string, - req: MerchantTemplateAddDetails, - withAuthorization: WithAuthorization = {}, - ) { - const baseUrl = merchantService.makeInstanceBaseUrl(instanceName); - let url = new URL("private/templates", baseUrl); - const resp = await harnessHttpLib.fetch(url.href, { - method: "POST", - body: req, - headers: withAuthorization as Record<string, string>, - }); - if (resp.status !== 204) { - throw Error("failed to create template"); - } - } - - export async function queryPrivateOrderStatus( - merchantService: MerchantServiceInterface, - query: PrivateOrderStatusQuery, - withAuthorization: WithAuthorization = {}, - ): Promise<MerchantOrderPrivateStatusResponse> { - const reqUrl = new URL( - `private/orders/${query.orderId}`, - merchantService.makeInstanceBaseUrl(query.instance), - ); - if (query.sessionId) { - reqUrl.searchParams.set("session_id", query.sessionId); - } - const resp = await harnessHttpLib.fetch(reqUrl.href, { - headers: withAuthorization as Record<string, string>, - }); - return readSuccessResponseJsonOrThrow( - resp, - codecForMerchantOrderPrivateStatusResponse(), - ); - } - - export async function giveRefund( - merchantService: MerchantServiceInterface, - r: { - instance: string; - orderId: string; - amount: string; - justification: string; - }, - ): Promise<{ talerRefundUri: string }> { - const reqUrl = new URL( - `private/orders/${r.orderId}/refund`, - merchantService.makeInstanceBaseUrl(r.instance), - ); - const resp = await harnessHttpLib.fetch(reqUrl.href, { - method: "POST", - body: { - refund: r.amount, - reason: r.justification, - }, - }); - const respBody = await resp.json(); - return { - talerRefundUri: respBody.taler_refund_uri, - }; - } - - export async function queryTippingReserves( - merchantService: MerchantServiceInterface, - instance: string, - ): Promise<TippingReserveStatus> { - const reqUrl = new URL( - `private/reserves`, - merchantService.makeInstanceBaseUrl(instance), - ); - const resp = await harnessHttpLib.fetch(reqUrl.href); - // FIXME: validate - return resp.json(); - } - - export async function giveTip( - merchantService: MerchantServiceInterface, - instance: string, - req: RewardCreateRequest, - ): Promise<RewardCreateConfirmation> { - const reqUrl = new URL( - `private/tips`, - merchantService.makeInstanceBaseUrl(instance), - ); - const resp = await harnessHttpLib.fetch(reqUrl.href, { - method: "POST", - body: req, - }); - // FIXME: validate - return resp.json(); - } -} - -export interface CreateMerchantTippingReserveRequest { - // Amount that the merchant promises to put into the reserve - initial_balance: AmountString; - - // Exchange the merchant intends to use for tipping - exchange_url: string; - - // Desired wire method, for example "iban" or "x-taler-bank" - wire_method: string; -} - export class MerchantService implements MerchantServiceInterface { static fromExistingConfig( gc: GlobalTestState, @@ -1636,23 +1371,23 @@ export class MerchantService implements MerchantServiceInterface { private configFilename: string, ) {} - private currentTimetravel: Duration | undefined; + private currentTimetravelOffsetMs: number | undefined; private isRunning(): boolean { return !!this.proc; } - setTimetravel(t: Duration | undefined): void { + setTimetravel(t: number | undefined): void { if (this.isRunning()) { throw Error("can't set time travel while the exchange is running"); } - this.currentTimetravel = t; + this.currentTimetravelOffsetMs = t; } private get timetravelArg(): string | undefined { - if (this.currentTimetravel && this.currentTimetravel.d_ms !== "forever") { + if (this.currentTimetravelOffsetMs != null) { // Convert to microseconds - return `--timetravel=+${this.currentTimetravel.d_ms * 1000}`; + return `--timetravel=+${this.currentTimetravelOffsetMs * 1000}`; } return undefined; } @@ -1759,7 +1494,7 @@ export class MerchantService implements MerchantServiceInterface { } async addDefaultInstance(): Promise<void> { - return await this.addInstance({ + return await this.addInstanceWithWireAccount({ id: "default", name: "Default Instance", paytoUris: [getPayto("merchant-default")], @@ -1769,13 +1504,16 @@ export class MerchantService implements MerchantServiceInterface { }); } - async addInstance( + /** + * Add an instance together with a wire account. + */ + async addInstanceWithWireAccount( instanceConfig: PartialMerchantInstanceConfig, ): Promise<void> { if (!this.proc) { throw Error("merchant must be running to add instance"); } - logger.info("adding instance"); + logger.info(`adding instance '${instanceConfig.id}'`); const url = `http://localhost:${this.merchantConfig.httpPort}/management/instances`; const auth = instanceConfig.auth ?? { method: "external" }; @@ -1801,12 +1539,20 @@ export class MerchantService implements MerchantServiceInterface { instanceConfig.defaultPayDelay ?? Duration.toTalerProtocolDuration(Duration.getForever()), }; - const httpLib = createPlatformHttpLib({ - allowHttp: true, - enableThrottling: false, - }); - const resp = await httpLib.fetch(url, { method: "POST", body }); + const resp = await harnessHttpLib.fetch(url, { method: "POST", body }); await expectSuccessResponseOrThrow(resp); + + const accountCreateUrl = `http://localhost:${this.merchantConfig.httpPort}/instances/${instanceConfig.id}/private/accounts`; + for (const paytoUri of instanceConfig.paytoUris) { + const accountReq: AccountAddDetails = { + payto_uri: paytoUri, + }; + const acctResp = await harnessHttpLib.fetch(accountCreateUrl, { + method: "POST", + body: accountReq, + }); + await expectSuccessResponseOrThrow(acctResp); + } } makeInstanceBaseUrl(instanceName?: string): string { @@ -1823,66 +1569,6 @@ export class MerchantService implements MerchantServiceInterface { } } -export interface MerchantAuthConfiguration { - method: "external" | "token"; - token?: string; -} - -// FIXME: Why do we need this? Describe / fix! -export interface PartialMerchantInstanceConfig { - auth?: MerchantAuthConfiguration; - id: string; - name: string; - paytoUris: string[]; - address?: unknown; - jurisdiction?: unknown; - defaultWireTransferDelay?: TalerProtocolDuration; - defaultPayDelay?: TalerProtocolDuration; -} - -// FIXME: Move all these types into merchant-api-types.ts! - -type FacadeCredentials = NoFacadeCredentials | BasicAuthFacadeCredentials; -interface NoFacadeCredentials { - type: "none"; -} -interface BasicAuthFacadeCredentials { - type: "basic"; - - // Username to use to authenticate - username: string; - - // Password to use to authenticate - password: string; -} - -interface MerchantBankAccount { - // The payto:// URI where the wallet will send coins. - payto_uri: string; - - // Optional base URL for a facade where the - // merchant backend can see incoming wire - // transfers to reconcile its accounting - // with that of the exchange. Used by - // taler-merchant-wirewatch. - credit_facade_url?: string; - - // Credentials for accessing the credit facade. - credit_facade_credentials?: FacadeCredentials; -} - -export interface MerchantInstanceConfig { - accounts: MerchantBankAccount[]; - auth: MerchantAuthConfiguration; - id: string; - name: string; - address: unknown; - jurisdiction: unknown; - use_stefan: boolean; - default_wire_transfer_delay: TalerProtocolDuration; - default_pay_delay: TalerProtocolDuration; -} - type TestStatus = "pass" | "fail" | "skip"; export interface TestRunResult { diff --git a/packages/taler-harness/src/harness/helpers.ts b/packages/taler-harness/src/harness/helpers.ts index d1d0ea104..9892e600b 100644 --- a/packages/taler-harness/src/harness/helpers.ts +++ b/packages/taler-harness/src/harness/helpers.ts @@ -25,21 +25,18 @@ */ import { AmountString, + BankAccessApiClient, ConfirmPayResultType, - MerchantContractTerms, Duration, - PreparePayResultType, + Logger, + MerchantApiClient, + MerchantContractTerms, NotificationType, - WalletNotification, + PreparePayResultType, TransactionMajorState, - Logger, + WalletNotification, } from "@gnu-taler/taler-util"; -import { - BankAccessApi, - BankApi, - HarnessExchangeBankAccount, - WalletApiOperation, -} from "@gnu-taler/taler-wallet-core"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { CoinConfig, defaultCoinConfig } from "./denomStructures.js"; import { FaultInjectedExchangeService, @@ -51,17 +48,17 @@ import { ExchangeService, ExchangeServiceInterface, FakebankService, - getPayto, GlobalTestState, - MerchantPrivateApi, + HarnessExchangeBankAccount, MerchantService, MerchantServiceInterface, - setupDb, - setupSharedDb, WalletCli, WalletClient, WalletService, WithAuthorization, + getPayto, + setupDb, + setupSharedDb, } from "./harness.js"; import * as fs from "fs"; @@ -107,114 +104,6 @@ export interface EnvOptions { additionalBankConfig?(b: BankService): void; } -/** - * Run a test case with a simple TESTKUDOS Taler environment, consisting - * of one exchange, one bank and one merchant. - * - * @deprecated use {@link createSimpleTestkudosEnvironmentV2} instead - */ -export async function createSimpleTestkudosEnvironment( - t: GlobalTestState, - coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")), - opts: EnvOptions = {}, -): Promise<SimpleTestEnvironment> { - const db = await setupDb(t); - - const bank = await BankService.create(t, { - allowRegistrations: true, - currency: "TESTKUDOS", - database: db.connStr, - httpPort: 8082, - }); - - const exchange = ExchangeService.create(t, { - name: "testexchange-1", - currency: "TESTKUDOS", - httpPort: 8081, - database: db.connStr, - }); - - const merchant = await MerchantService.create(t, { - name: "testmerchant-1", - currency: "TESTKUDOS", - httpPort: 8083, - database: db.connStr, - }); - - const exchangeBankAccount = await bank.createExchangeAccount( - "myexchange", - "x", - ); - await exchange.addBankAccount("1", exchangeBankAccount); - - bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); - - await bank.start(); - - await bank.pingUntilAvailable(); - - const ageMaskSpec = opts.ageMaskSpec; - - if (ageMaskSpec) { - exchange.enableAgeRestrictions(ageMaskSpec); - // Enable age restriction for all coins. - exchange.addCoinConfigList( - coinConfig.map((x) => ({ - ...x, - name: `${x.name}-age`, - ageRestricted: true, - })), - ); - // For mixed age restrictions, we also offer coins without age restrictions - if (opts.mixedAgeRestriction) { - exchange.addCoinConfigList( - coinConfig.map((x) => ({ ...x, ageRestricted: false })), - ); - } - } else { - exchange.addCoinConfigList(coinConfig); - } - - await exchange.start(); - await exchange.pingUntilAvailable(); - - merchant.addExchange(exchange); - - await merchant.start(); - await merchant.pingUntilAvailable(); - - await merchant.addInstance({ - id: "default", - name: "Default Instance", - paytoUris: [getPayto("merchant-default")], - defaultWireTransferDelay: Duration.toTalerProtocolDuration( - Duration.fromSpec({ minutes: 1 }), - ), - }); - - await merchant.addInstance({ - id: "minst1", - name: "minst1", - paytoUris: [getPayto("minst1")], - defaultWireTransferDelay: Duration.toTalerProtocolDuration( - Duration.fromSpec({ minutes: 1 }), - ), - }); - - console.log("setup done!"); - - const wallet = new WalletCli(t); - - return { - commonDb: db, - exchange, - merchant, - wallet, - bank, - exchangeBankAccount, - }; -} - export function getSharedTestDir(): string { return `/tmp/taler-harness@${process.env.USER}`; } @@ -344,7 +233,7 @@ export async function useSharedTestkudosEnvironment(t: GlobalTestState) { await merchant.pingUntilAvailable(); if (!prevSetupDone) { - await merchant.addInstance({ + await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", paytoUris: [getPayto("merchant-default")], @@ -353,7 +242,7 @@ export async function useSharedTestkudosEnvironment(t: GlobalTestState) { ), }); - await merchant.addInstance({ + await merchant.addInstanceWithWireAccount({ id: "minst1", name: "minst1", paytoUris: [getPayto("minst1")], @@ -476,7 +365,7 @@ export async function createSimpleTestkudosEnvironmentV2( await merchant.start(); await merchant.pingUntilAvailable(); - await merchant.addInstance({ + await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", paytoUris: [getPayto("merchant-default")], @@ -485,7 +374,7 @@ export async function createSimpleTestkudosEnvironmentV2( ), }); - await merchant.addInstance({ + await merchant.addInstanceWithWireAccount({ id: "minst1", name: "minst1", paytoUris: [getPayto("minst1")], @@ -554,7 +443,7 @@ export interface FaultyMerchantTestEnvironment { exchangeBankAccount: HarnessExchangeBankAccount; merchant: MerchantService; faultyMerchant: FaultInjectedMerchantService; - wallet: WalletCli; + walletClient: WalletClient; } /** @@ -620,13 +509,13 @@ export async function createFaultInjectedMerchantTestkudosEnvironment( await merchant.start(); await merchant.pingUntilAvailable(); - await merchant.addInstance({ + await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", paytoUris: [getPayto("merchant-default")], }); - await merchant.addInstance({ + await merchant.addInstanceWithWireAccount({ id: "minst1", name: "minst1", paytoUris: [getPayto("minst1")], @@ -634,13 +523,15 @@ export async function createFaultInjectedMerchantTestkudosEnvironment( console.log("setup done!"); - const wallet = new WalletCli(t); + const { walletClient } = await createWalletDaemonWithClient(t, { + name: "default", + }); return { commonDb: db, exchange, merchant, - wallet, + walletClient, bank, exchangeBankAccount, faultyMerchant, @@ -648,51 +539,6 @@ export async function createFaultInjectedMerchantTestkudosEnvironment( }; } -/** - * Start withdrawing into the wallet. - * - * Only starts the operation, does not wait for it to finish. - */ -export async function startWithdrawViaBank( - t: GlobalTestState, - p: { - wallet: WalletCli; - bank: BankService; - exchange: ExchangeServiceInterface; - amount: AmountString; - restrictAge?: number; - }, -): Promise<void> { - const { wallet, bank, exchange, amount } = p; - - const user = await BankApi.createRandomBankUser(bank); - const wop = await BankAccessApi.createWithdrawalOperation(bank, user, amount); - - // Hand it to the wallet - - await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, { - talerWithdrawUri: wop.taler_withdraw_uri, - restrictAge: p.restrictAge, - }); - - await wallet.runPending(); - - // Withdraw (AKA select) - - await wallet.client.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, { - exchangeBaseUrl: exchange.baseUrl, - talerWithdrawUri: wop.taler_withdraw_uri, - restrictAge: p.restrictAge, - }); - - // Confirm it - - await BankApi.confirmWithdrawalOperation(bank, user, wop); - - // We do *not* call runPending / runUntilDone on the wallet here. - // Some tests rely on the final withdraw failing. -} - export interface WithdrawViaBankResult { withdrawalFinishedCond: Promise<true>; } @@ -714,8 +560,10 @@ export async function withdrawViaBankV2( ): Promise<WithdrawViaBankResult> { const { walletClient: wallet, bank, exchange, amount } = p; - const user = await BankApi.createRandomBankUser(bank); - const wop = await BankAccessApi.createWithdrawalOperation(bank, user, amount); + const bankClient = new BankAccessApiClient(bank.bankAccessApiBaseUrl); + + const user = await bankClient.createRandomBankUser(); + const wop = await bankClient.createWithdrawalOperation(user.username, amount); // Hand it to the wallet @@ -744,140 +592,44 @@ export async function withdrawViaBankV2( // Confirm it - await BankApi.confirmWithdrawalOperation(bank, user, wop); + await bankClient.confirmWithdrawalOperation(user.username, wop); return { withdrawalFinishedCond, }; } -/** - * Withdraw balance. - * - * @deprecated use {@link withdrawViaBankV2 instead} - */ -export async function withdrawViaBank( - t: GlobalTestState, - p: { - wallet: WalletCli; - bank: BankService; - exchange: ExchangeServiceInterface; - amount: AmountString; - restrictAge?: number; - }, -): Promise<void> { - const { wallet } = p; - - await startWithdrawViaBank(t, p); - - await wallet.runUntilDone(); - - // Check balance - - await wallet.client.call(WalletApiOperation.GetBalances, {}); -} - -export async function applyTimeTravel( - timetravelDuration: Duration, +export async function applyTimeTravelV2( + timetravelOffsetMs: number, s: { exchange?: ExchangeService; merchant?: MerchantService; - wallet?: WalletCli; + walletClient?: WalletClient; }, ): Promise<void> { if (s.exchange) { await s.exchange.stop(); - s.exchange.setTimetravel(timetravelDuration); + s.exchange.setTimetravel(timetravelOffsetMs); await s.exchange.start(); await s.exchange.pingUntilAvailable(); } if (s.merchant) { await s.merchant.stop(); - s.merchant.setTimetravel(timetravelDuration); + s.merchant.setTimetravel(timetravelOffsetMs); await s.merchant.start(); await s.merchant.pingUntilAvailable(); } - if (s.wallet) { - s.wallet.setTimetravel(timetravelDuration); + if (s.walletClient) { + await s.walletClient.call(WalletApiOperation.TestingSetTimetravel, { + offsetMs: timetravelOffsetMs, + }); } } /** * Make a simple payment and check that it succeeded. - * - * @deprecated - */ -export async function makeTestPayment( - t: GlobalTestState, - args: { - merchant: MerchantServiceInterface; - wallet: WalletCli; - order: Partial<MerchantContractTerms>; - instance?: string; - }, - auth: WithAuthorization = {}, -): Promise<void> { - // Set up order. - - const { wallet, merchant } = args; - const instance = args.instance ?? "default"; - - const orderResp = await MerchantPrivateApi.createOrder( - merchant, - instance, - { - order: args.order, - }, - auth, - ); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus( - merchant, - { - orderId: orderResp.order_id, - }, - auth, - ); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - // Make wallet pay for the order - - const preparePayResult = await wallet.client.call( - WalletApiOperation.PreparePayForUri, - { - talerPayUri: orderStatus.taler_pay_uri, - }, - ); - - t.assertTrue( - preparePayResult.status === PreparePayResultType.PaymentPossible, - ); - - const r2 = await wallet.client.call(WalletApiOperation.ConfirmPay, { - proposalId: preparePayResult.proposalId, - }); - - t.assertTrue(r2.type === ConfirmPayResultType.Done); - - // Check if payment was successful. - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus( - merchant, - { - orderId: orderResp.order_id, - instance, - }, - auth, - ); - - t.assertTrue(orderStatus.order_status === "paid"); -} - -/** - * Make a simple payment and check that it succeeded. */ export async function makeTestPaymentV2( t: GlobalTestState, @@ -891,25 +643,19 @@ export async function makeTestPaymentV2( ): Promise<void> { // Set up order. - const { walletClient, merchant } = args; - const instance = args.instance ?? "default"; + const { walletClient, merchant, instance } = args; - const orderResp = await MerchantPrivateApi.createOrder( - merchant, - instance, - { - order: args.order, - }, - auth, + const merchantClient = new MerchantApiClient( + merchant.makeInstanceBaseUrl(instance), ); - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus( - merchant, - { - orderId: orderResp.order_id, - }, - auth, - ); + const orderResp = await merchantClient.createOrder({ + order: args.order, + }); + + let orderStatus = await merchantClient.queryPrivateOrderStatus({ + orderId: orderResp.order_id, + }); t.assertTrue(orderStatus.order_status === "unpaid"); @@ -934,14 +680,10 @@ export async function makeTestPaymentV2( // Check if payment was successful. - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus( - merchant, - { - orderId: orderResp.order_id, - instance, - }, - auth, - ); + orderStatus = await merchantClient.queryPrivateOrderStatus({ + orderId: orderResp.order_id, + instance, + }); t.assertTrue(orderStatus.order_status === "paid"); } diff --git a/packages/taler-harness/src/harness/libeufin-apis.ts b/packages/taler-harness/src/harness/libeufin-apis.ts index 3c57eee07..0193f9252 100644 --- a/packages/taler-harness/src/harness/libeufin-apis.ts +++ b/packages/taler-harness/src/harness/libeufin-apis.ts @@ -176,7 +176,7 @@ export interface LibeufinSandboxAddIncomingRequest { direction: string; } -const libeufinHttpLib = createPlatformHttpLib(); +const libeufinHarnessHttpLib = createPlatformHttpLib(); /** * APIs spread across Legacy and Access, it is therefore @@ -192,7 +192,7 @@ export namespace LibeufinSandboxApi { iban: string | null = null, ): Promise<void> { let url = new URL("testing/register", libeufinSandboxService.baseUrl); - await libeufinHttpLib.fetch(url.href, { + await libeufinHarnessHttpLib.fetch(url.href, { method: "POST", body: { username: username, @@ -211,7 +211,7 @@ export namespace LibeufinSandboxApi { ): Promise<void> { // baseUrl should already be pointed to one demobank. let url = new URL("ebics/subscribers", libeufinSandboxService.baseUrl); - await libeufinHttpLib.fetch(url.href, { + await libeufinHarnessHttpLib.fetch(url.href, { method: "POST", body: { userID: req.userID, @@ -228,7 +228,7 @@ export namespace LibeufinSandboxApi { ): Promise<void> { const baseUrl = libeufinSandboxService.baseUrl; let url = new URL(`admin/ebics/hosts/${hostID}/rotate-keys`, baseUrl); - await libeufinHttpLib.fetch(url.href, { + await libeufinHarnessHttpLib.fetch(url.href, { method: "POST", body: {}, }); @@ -239,7 +239,7 @@ export namespace LibeufinSandboxApi { ): Promise<void> { const baseUrl = libeufinSandboxService.baseUrl; let url = new URL("admin/ebics/hosts", baseUrl); - await libeufinHttpLib.fetch(url.href, { + await libeufinHarnessHttpLib.fetch(url.href, { method: "POST", body: { hostID, @@ -255,7 +255,7 @@ export namespace LibeufinSandboxApi { ): Promise<void> { const baseUrl = libeufinSandboxService.baseUrl; let url = new URL(`admin/bank-accounts/${req.label}`, baseUrl); - await libeufinHttpLib.fetch(url.href, { + await libeufinHarnessHttpLib.fetch(url.href, { method: "POST", body: req, headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, @@ -272,7 +272,7 @@ export namespace LibeufinSandboxApi { ): Promise<void> { const baseUrl = libeufinSandboxService.baseUrl; let url = new URL("admin/ebics/subscribers", baseUrl); - await libeufinHttpLib.fetch(url.href, { + await libeufinHarnessHttpLib.fetch(url.href, { method: "POST", body: req, headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, @@ -289,7 +289,7 @@ export namespace LibeufinSandboxApi { ): Promise<void> { const baseUrl = libeufinSandboxService.baseUrl; let url = new URL("admin/ebics/bank-accounts", baseUrl); - await libeufinHttpLib.fetch(url.href, { + await libeufinHarnessHttpLib.fetch(url.href, { method: "POST", body: req, headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, @@ -306,7 +306,7 @@ export namespace LibeufinSandboxApi { `admin/bank-accounts/${accountLabel}/simulate-incoming-transaction`, baseUrl, ); - await libeufinHttpLib.fetch(url.href, { + await libeufinHarnessHttpLib.fetch(url.href, { method: "POST", body: req, headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, @@ -322,7 +322,7 @@ export namespace LibeufinSandboxApi { `admin/bank-accounts/${accountLabel}/transactions`, baseUrl, ); - const res = await libeufinHttpLib.fetch(url.href, { + const res = await libeufinHarnessHttpLib.fetch(url.href, { headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, }); return (await res.json()) as SandboxAccountTransactions; @@ -334,7 +334,7 @@ export namespace LibeufinSandboxApi { ): Promise<any> { const baseUrl = libeufinSandboxService.baseUrl; let url = new URL("admin/payments/camt", baseUrl); - return await libeufinHttpLib.fetch(url.href, { + return await libeufinHarnessHttpLib.fetch(url.href, { method: "POST", headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, body: { @@ -350,7 +350,7 @@ export namespace LibeufinSandboxApi { ): Promise<LibeufinSandboxAdminBankAccountBalance> { const baseUrl = libeufinSandboxService.baseUrl; let url = new URL(`admin/bank-accounts/${accountLabel}`, baseUrl); - const res = await libeufinHttpLib.fetch(url.href, { + const res = await libeufinHarnessHttpLib.fetch(url.href, { headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, }); return res.json(); @@ -362,7 +362,7 @@ export namespace LibeufinNexusApi { nexus: LibeufinNexusServiceInterface, ): Promise<NexusBankConnections> { let url = new URL("bank-connections", nexus.baseUrl); - const res = await libeufinHttpLib.fetch(url.href, { + const res = await libeufinHarnessHttpLib.fetch(url.href, { headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, }); return res.json(); @@ -374,7 +374,7 @@ export namespace LibeufinNexusApi { ): Promise<void> { const baseUrl = libeufinNexusService.baseUrl; let url = new URL("bank-connections/delete-connection", baseUrl); - await libeufinHttpLib.fetch(url.href, { + await libeufinHarnessHttpLib.fetch(url.href, { method: "POST", headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, body: req, @@ -387,7 +387,7 @@ export namespace LibeufinNexusApi { ): Promise<void> { const baseUrl = libeufinNexusService.baseUrl; let url = new URL("bank-connections", baseUrl); - await libeufinHttpLib.fetch(url.href, { + await libeufinHarnessHttpLib.fetch(url.href, { method: "POST", headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, body: { @@ -411,7 +411,7 @@ export namespace LibeufinNexusApi { ): Promise<any> { const baseUrl = libeufinNexusService.baseUrl; let url = new URL(`bank-accounts/${accountName}`, baseUrl); - const resp = await libeufinHttpLib.fetch(url.href, { + const resp = await libeufinHarnessHttpLib.fetch(url.href, { headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, }); return resp.json(); @@ -427,7 +427,7 @@ export namespace LibeufinNexusApi { `bank-accounts/${accountName}/payment-initiations/${paymentId}/submit`, baseUrl, ); - await libeufinHttpLib.fetch(url.href, { + await libeufinHarnessHttpLib.fetch(url.href, { method: "POST", headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, body: {}, @@ -443,7 +443,7 @@ export namespace LibeufinNexusApi { `bank-connections/${connectionName}/fetch-accounts`, baseUrl, ); - await libeufinHttpLib.fetch(url.href, { + await libeufinHarnessHttpLib.fetch(url.href, { method: "POST", headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, body: {}, @@ -461,7 +461,7 @@ export namespace LibeufinNexusApi { `bank-connections/${connectionName}/import-account`, baseUrl, ); - await libeufinHttpLib.fetch(url.href, { + await libeufinHarnessHttpLib.fetch(url.href, { method: "POST", headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, body: { @@ -477,7 +477,7 @@ export namespace LibeufinNexusApi { ): Promise<void> { const baseUrl = libeufinNexusService.baseUrl; let url = new URL(`bank-connections/${connectionName}/connect`, baseUrl); - await libeufinHttpLib.fetch(url.href, { + await libeufinHarnessHttpLib.fetch(url.href, { method: "POST", headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, body: {}, @@ -495,7 +495,7 @@ export namespace LibeufinNexusApi { `/bank-accounts/${accountName}/payment-initiations`, baseUrl, ); - let response = await libeufinHttpLib.fetch(url.href, { + let response = await libeufinHarnessHttpLib.fetch(url.href, { headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, }); const respJson = await response.json(); @@ -518,7 +518,7 @@ export namespace LibeufinNexusApi { for (const [k, v] of Object.entries(params)) { url.searchParams.set(k, String(v)); } - let response = await libeufinHttpLib.fetch(url.href, { + let response = await libeufinHarnessHttpLib.fetch(url.href, { headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, }); return response.json(); @@ -534,7 +534,7 @@ export namespace LibeufinNexusApi { ): Promise<LibeufinNexusTransactions> { const baseUrl = libeufinNexusService.baseUrl; let url = new URL(`/bank-accounts/${accountName}/transactions`, baseUrl); - let response = await libeufinHttpLib.fetch(url.href, { + let response = await libeufinHarnessHttpLib.fetch(url.href, { headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, }); return response.json(); @@ -553,7 +553,7 @@ export namespace LibeufinNexusApi { `/bank-accounts/${accountName}/fetch-transactions`, baseUrl, ); - const resp = await libeufinHttpLib.fetch(url.href, { + const resp = await libeufinHarnessHttpLib.fetch(url.href, { method: "POST", headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, body: { @@ -572,7 +572,7 @@ export namespace LibeufinNexusApi { ): Promise<void> { const baseUrl = libeufinNexusService.baseUrl; let url = new URL(`/users/${username}/password`, baseUrl); - await libeufinHttpLib.fetch(url.href, { + await libeufinHarnessHttpLib.fetch(url.href, { method: "POST", headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, body: req, @@ -585,7 +585,7 @@ export namespace LibeufinNexusApi { ): Promise<NexusUserResponse> { const baseUrl = libeufinNexusService.baseUrl; let url = new URL(`/user`, baseUrl); - const resp = await libeufinHttpLib.fetch(url.href, { + const resp = await libeufinHarnessHttpLib.fetch(url.href, { headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, }); return resp.json(); @@ -597,7 +597,7 @@ export namespace LibeufinNexusApi { ): Promise<void> { const baseUrl = libeufinNexusService.baseUrl; let url = new URL(`/users`, baseUrl); - await libeufinHttpLib.fetch(url.href, { + await libeufinHarnessHttpLib.fetch(url.href, { method: "POST", headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, body: req, @@ -609,7 +609,7 @@ export namespace LibeufinNexusApi { ): Promise<NexusGetPermissionsResponse> { const baseUrl = libeufinNexusService.baseUrl; let url = new URL(`/permissions`, baseUrl); - const resp = await libeufinHttpLib.fetch(url.href, { + const resp = await libeufinHarnessHttpLib.fetch(url.href, { headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, }); return resp.json(); @@ -621,7 +621,7 @@ export namespace LibeufinNexusApi { ): Promise<void> { const baseUrl = libeufinNexusService.baseUrl; let url = new URL(`/permissions`, baseUrl); - await libeufinHttpLib.fetch(url.href, { + await libeufinHarnessHttpLib.fetch(url.href, { method: "POST", headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, body: req, @@ -634,7 +634,7 @@ export namespace LibeufinNexusApi { ): Promise<NexusTaskCollection> { const baseUrl = libeufinNexusService.baseUrl; let url = new URL(`/bank-accounts/${bankAccountName}/schedule`, baseUrl); - const resp = await libeufinHttpLib.fetch(url.href, { + const resp = await libeufinHarnessHttpLib.fetch(url.href, { headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, }); return resp.json(); @@ -653,7 +653,7 @@ export namespace LibeufinNexusApi { baseUrl, ); if (taskName) url = new URL(taskName, `${url.href}/`); - const resp = await libeufinHttpLib.fetch(url.href, { + const resp = await libeufinHarnessHttpLib.fetch(url.href, { headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, }); return resp.json(); @@ -669,7 +669,7 @@ export namespace LibeufinNexusApi { `/bank-accounts/${bankAccountName}/schedule/${taskName}`, baseUrl, ); - await libeufinHttpLib.fetch(url.href, { + await libeufinHarnessHttpLib.fetch(url.href, { method: "DELETE", headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, }); @@ -682,7 +682,7 @@ export namespace LibeufinNexusApi { ): Promise<void> { const baseUrl = libeufinNexusService.baseUrl; let url = new URL(`/bank-accounts/${bankAccountName}/schedule`, baseUrl); - await libeufinHttpLib.fetch(url.href, { + await libeufinHarnessHttpLib.fetch(url.href, { method: "POST", headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, body: req, @@ -695,7 +695,7 @@ export namespace LibeufinNexusApi { ): Promise<void> { const baseUrl = libeufinNexusService.baseUrl; let url = new URL(`facades/${facadeName}`, baseUrl); - await libeufinHttpLib.fetch(url.href, { + await libeufinHarnessHttpLib.fetch(url.href, { method: "DELETE", headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, }); @@ -706,7 +706,7 @@ export namespace LibeufinNexusApi { ): Promise<NexusFacadeListResponse> { const baseUrl = libeufinNexusService.baseUrl; let url = new URL("facades", baseUrl); - const resp = await libeufinHttpLib.fetch(url.href, { + const resp = await libeufinHarnessHttpLib.fetch(url.href, { headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, }); // FIXME: Just return validated, typed response here! @@ -719,7 +719,7 @@ export namespace LibeufinNexusApi { ): Promise<void> { const baseUrl = libeufinNexusService.baseUrl; let url = new URL("facades", baseUrl); - await libeufinHttpLib.fetch(url.href, { + await libeufinHarnessHttpLib.fetch(url.href, { method: "POST", headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, body: { @@ -741,7 +741,7 @@ export namespace LibeufinNexusApi { ): Promise<void> { const baseUrl = libeufinNexusService.baseUrl; let url = new URL("facades", baseUrl); - await libeufinHttpLib.fetch(url.href, { + await libeufinHarnessHttpLib.fetch(url.href, { method: "POST", headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, body: { @@ -766,7 +766,7 @@ export namespace LibeufinNexusApi { `/bank-accounts/${accountId}/submit-all-payment-initiations`, baseUrl, ); - await libeufinHttpLib.fetch(url.href, { + await libeufinHarnessHttpLib.fetch(url.href, { method: "POST", headers: { Authorization: makeBasicAuthHeader("admin", "secret") }, body: {}, diff --git a/packages/taler-harness/src/index.ts b/packages/taler-harness/src/index.ts index cd688ed89..f5d4fd2c2 100644 --- a/packages/taler-harness/src/index.ts +++ b/packages/taler-harness/src/index.ts @@ -20,17 +20,18 @@ import { addPaytoQueryParams, Amounts, + BankAccessApiClient, Configuration, decodeCrock, j2s, Logger, + MerchantApiClient, rsaBlind, setGlobalLogLevelFromString, } from "@gnu-taler/taler-util"; import { clk } from "@gnu-taler/taler-util/clk"; import { createPlatformHttpLib } from "@gnu-taler/taler-util/http"; import { - BankAccessApiClient, CryptoDispatcher, downloadExchangeInfo, SynchronousCryptoWorkerFactoryPlain, @@ -45,11 +46,7 @@ import { runBench2 } from "./bench2.js"; import { runBench3 } from "./bench3.js"; import { runEnvFull } from "./env-full.js"; import { runEnv1 } from "./env1.js"; -import { - GlobalTestState, - MerchantApiClient, - runTestWithState, -} from "./harness/harness.js"; +import { GlobalTestState, runTestWithState } from "./harness/harness.js"; import { getTestInfo, runTests } from "./integrationtests/testrunner.js"; import { lintExchangeDeployment } from "./lint.js"; @@ -239,14 +236,15 @@ deploymentCli console.log(tipReserveResp); - const bankAccessApiClient = new BankAccessApiClient({ - baseUrl: args.tipTopup.bankAccessUrl, - auth: { - username: args.tipTopup.bankAccount, - password: args.tipTopup.bankPassword, + const bankAccessApiClient = new BankAccessApiClient( + args.tipTopup.bankAccessUrl, + { + auth: { + username: args.tipTopup.bankAccount, + password: args.tipTopup.bankPassword, + }, }, - allowHttp: true, - }); + ); const paytoUri = addPaytoQueryParams(tipReserveResp.accounts[0].payto_uri, { message: `tip-reserve ${tipReserveResp.reserve_pub}`, @@ -402,7 +400,6 @@ deploymentCli ); const res = await merchantClient.getPrivateInstanceInfo(); - console.log(res); const tipRes = await merchantClient.getPrivateTipReserves(); console.log(j2s(tipRes)); @@ -506,9 +503,6 @@ testingCli if (t.suites.length > 0) { s += ` (suites: ${t.suites.join(",")})`; } - if (t.excludeByDefault) { - s += ` [excluded by default]`; - } if (t.experimental) { s += ` [experimental]`; } diff --git a/packages/taler-harness/src/integrationtests/test-age-restrictions-merchant.ts b/packages/taler-harness/src/integrationtests/test-age-restrictions-merchant.ts index 919097deb..7f936a479 100644 --- a/packages/taler-harness/src/integrationtests/test-age-restrictions-merchant.ts +++ b/packages/taler-harness/src/integrationtests/test-age-restrictions-merchant.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2023 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -17,24 +17,20 @@ /** * Imports. */ -import { - BankApi, - WalletApiOperation, - WireGatewayApiClient, -} from "@gnu-taler/taler-wallet-core"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { defaultCoinConfig } from "../harness/denomStructures.js"; +import { getWireMethodForTest, GlobalTestState } from "../harness/harness.js"; import { - getWireMethodForTest, - GlobalTestState, - MerchantApiClient, - MerchantPrivateApi, - WalletCli, -} from "../harness/harness.js"; -import { - createSimpleTestkudosEnvironment, - withdrawViaBank, - makeTestPayment, + createSimpleTestkudosEnvironmentV2, + createWalletDaemonWithClient, + makeTestPaymentV2, + withdrawViaBankV2, } from "../harness/helpers.js"; +import { + BankAccessApiClient, + MerchantApiClient, + WireGatewayApiClient, +} from "@gnu-taler/taler-util"; /** * Run test for basic, bank-integrated withdrawal and payment. @@ -43,12 +39,12 @@ export async function runAgeRestrictionsMerchantTest(t: GlobalTestState) { // Set up test environment const { - wallet: walletOne, + walletClient: walletClientOne, bank, exchange, merchant, exchangeBankAccount, - } = await createSimpleTestkudosEnvironment( + } = await createSimpleTestkudosEnvironmentV2( t, defaultCoinConfig.map((x) => x("TESTKUDOS")), { @@ -58,24 +54,34 @@ export async function runAgeRestrictionsMerchantTest(t: GlobalTestState) { const merchantClient = new MerchantApiClient( merchant.makeInstanceBaseUrl("default"), + ); + + const { walletClient: walletClientTwo } = await createWalletDaemonWithClient( + t, { - method: "external", + name: "w2", }, ); - const walletTwo = new WalletCli(t, "walletTwo"); - const walletThree = new WalletCli(t, "walletThree"); + const { walletClient: walletClientThree } = + await createWalletDaemonWithClient(t, { + name: "w3", + }); { - const walletZero = new WalletCli(t, "walletZero"); + const { walletClient: walletClientZero } = + await createWalletDaemonWithClient(t, { + name: "w0", + }); - await withdrawViaBank(t, { - wallet: walletZero, + const wres = await withdrawViaBankV2(t, { + walletClient: walletClientZero, bank, exchange, amount: "TESTKUDOS:20", restrictAge: 13, }); + await wres.withdrawalFinishedCond; const order = { summary: "Buy me!", @@ -84,20 +90,28 @@ export async function runAgeRestrictionsMerchantTest(t: GlobalTestState) { minimum_age: 9, }; - await makeTestPayment(t, { wallet: walletZero, merchant, order }); - await walletZero.runUntilDone(); + await makeTestPaymentV2(t, { + walletClient: walletClientZero, + merchant, + order, + }); + await walletClientZero.call( + WalletApiOperation.TestingWaitTransactionsFinal, + {}, + ); } { - const wallet = walletOne; + const walletClient = walletClientOne; - await withdrawViaBank(t, { - wallet, + const wres = await withdrawViaBankV2(t, { + walletClient, bank, exchange, amount: "TESTKUDOS:20", restrictAge: 13, }); + await wres.withdrawalFinishedCond; const order = { summary: "Buy me!", @@ -106,20 +120,24 @@ export async function runAgeRestrictionsMerchantTest(t: GlobalTestState) { minimum_age: 9, }; - await makeTestPayment(t, { wallet, merchant, order }); - await wallet.runUntilDone(); + await makeTestPaymentV2(t, { walletClient, merchant, order }); + await walletClient.call( + WalletApiOperation.TestingWaitTransactionsFinal, + {}, + ); } { - const wallet = walletTwo; + const walletClient = walletClientTwo; - await withdrawViaBank(t, { - wallet, + const wres = await withdrawViaBankV2(t, { + walletClient, bank, exchange, amount: "TESTKUDOS:20", restrictAge: 13, }); + await wres.withdrawalFinishedCond; const order = { summary: "Buy me!", @@ -127,19 +145,23 @@ export async function runAgeRestrictionsMerchantTest(t: GlobalTestState) { fulfillment_url: "taler://fulfillment-success/thx", }; - await makeTestPayment(t, { wallet, merchant, order }); - await wallet.runUntilDone(); + await makeTestPaymentV2(t, { walletClient, merchant, order }); + await walletClient.call( + WalletApiOperation.TestingWaitTransactionsFinal, + {}, + ); } { - const wallet = walletThree; + const walletClient = walletClientThree; - await withdrawViaBank(t, { - wallet, + const wres = await withdrawViaBankV2(t, { + walletClient, bank, exchange, amount: "TESTKUDOS:20", }); + await wres.withdrawalFinishedCond; const order = { summary: "Buy me!", @@ -148,32 +170,37 @@ export async function runAgeRestrictionsMerchantTest(t: GlobalTestState) { minimum_age: 9, }; - await makeTestPayment(t, { wallet, merchant, order }); - await wallet.runUntilDone(); + await makeTestPaymentV2(t, { walletClient, merchant, order }); + await walletClient.call( + WalletApiOperation.TestingWaitTransactionsFinal, + {}, + ); } // Pay with coin from tipping { - const mbu = await BankApi.createRandomBankUser(bank); - const tipReserveResp = await merchantClient.createTippingReserve( - { - exchange_url: exchange.baseUrl, - initial_balance: "TESTKUDOS:10", - wire_method: getWireMethodForTest(), - }, - ); + const bankClient = new BankAccessApiClient(bank.bankAccessApiBaseUrl); + const mbu = await bankClient.createRandomBankUser(); + const tipReserveResp = await merchantClient.createTippingReserve({ + exchange_url: exchange.baseUrl, + initial_balance: "TESTKUDOS:10", + wire_method: getWireMethodForTest(), + }); t.assertDeepEqual( tipReserveResp.accounts[0].payto_uri, exchangeBankAccount.accountPaytoUri, ); - const wireGatewayApiClient = new WireGatewayApiClient({ - wireGatewayApiBaseUrl: exchangeBankAccount.wireGatewayApiBaseUrl, - accountName: exchangeBankAccount.accountName, - accountPassword: exchangeBankAccount.accountPassword, - allowHttp: true, - }); + const wireGatewayApiClient = new WireGatewayApiClient( + exchangeBankAccount.wireGatewayApiBaseUrl, + { + auth: { + username: exchangeBankAccount.accountName, + password: exchangeBankAccount.accountPassword, + }, + }, + ); await wireGatewayApiClient.adminAddIncoming({ amount: "TESTKUDOS:10", @@ -183,23 +210,32 @@ export async function runAgeRestrictionsMerchantTest(t: GlobalTestState) { await exchange.runWirewatchOnce(); - const tip = await MerchantPrivateApi.giveTip(merchant, "default", { + const tip = await merchantClient.giveTip({ amount: "TESTKUDOS:5", justification: "why not?", next_url: "https://example.com/after-tip", }); - const walletTipping = new WalletCli(t, "age-tipping"); + const { walletClient: walletClientTipping } = + await createWalletDaemonWithClient(t, { + name: "age-tipping", + }); - const ptr = await walletTipping.client.call(WalletApiOperation.PrepareReward, { - talerRewardUri: tip.taler_reward_uri, - }); + const ptr = await walletClientTipping.call( + WalletApiOperation.PrepareReward, + { + talerRewardUri: tip.taler_reward_uri, + }, + ); - await walletTipping.client.call(WalletApiOperation.AcceptReward, { + await walletClientTipping.call(WalletApiOperation.AcceptReward, { walletRewardId: ptr.walletRewardId, }); - await walletTipping.runUntilDone(); + await walletClientTipping.call( + WalletApiOperation.TestingWaitTransactionsFinal, + {}, + ); const order = { summary: "Buy me!", @@ -208,8 +244,15 @@ export async function runAgeRestrictionsMerchantTest(t: GlobalTestState) { minimum_age: 9, }; - await makeTestPayment(t, { wallet: walletTipping, merchant, order }); - await walletTipping.runUntilDone(); + await makeTestPaymentV2(t, { + walletClient: walletClientTipping, + merchant, + order, + }); + await walletClientTipping.call( + WalletApiOperation.TestingWaitTransactionsFinal, + {}, + ); } } diff --git a/packages/taler-harness/src/integrationtests/test-bank-api.ts b/packages/taler-harness/src/integrationtests/test-bank-api.ts index 9ac16980b..a13ff63c7 100644 --- a/packages/taler-harness/src/integrationtests/test-bank-api.ts +++ b/packages/taler-harness/src/integrationtests/test-bank-api.ts @@ -17,13 +17,13 @@ /** * Imports. */ -import { createEddsaKeyPair, encodeCrock } from "@gnu-taler/taler-util"; import { - BankAccessApi, - BankApi, + BankAccessApiClient, CreditDebitIndicator, WireGatewayApiClient, -} from "@gnu-taler/taler-wallet-core"; + createEddsaKeyPair, + encodeCrock, +} from "@gnu-taler/taler-util"; import { defaultCoinConfig } from "../harness/denomStructures.js"; import { BankService, @@ -84,32 +84,34 @@ export async function runBankApiTest(t: GlobalTestState) { await merchant.start(); await merchant.pingUntilAvailable(); - await merchant.addDefaultInstance(); - await merchant.addInstance({ - id: "minst1", - name: "minst1", - paytoUris: [getPayto("minst1")], - }); - await merchant.addInstance({ + await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", paytoUris: [getPayto("merchant-default")], }); + await merchant.addInstanceWithWireAccount({ + id: "minst1", + name: "minst1", + paytoUris: [getPayto("minst1")], + }); + console.log("setup done!"); - const bankUser = await BankApi.registerAccount(bank, "user1", "pw1", {}); + const bankClient = new BankAccessApiClient(bank.bankAccessApiBaseUrl); + + const bankUser = await bankClient.registerAccount("user1", "pw1"); // Make sure that registering twice results in a 409 Conflict { const e = await t.assertThrowsTalerErrorAsync(async () => { - await BankApi.registerAccount(bank, "user1", "pw2", {}); + await bankClient.registerAccount("user1", "pw2"); }); t.assertTrue(e.errorDetail.httpStatusCode === 409); } - let balResp = await BankAccessApi.getAccountBalance(bank, bankUser); + let balResp = await bankClient.getAccountBalance(bankUser.username); console.log(balResp); @@ -121,12 +123,15 @@ export async function runBankApiTest(t: GlobalTestState) { const res = createEddsaKeyPair(); - const wireGatewayApiClient = new WireGatewayApiClient({ - wireGatewayApiBaseUrl: exchangeBankAccount.wireGatewayApiBaseUrl, - accountName: exchangeBankAccount.accountName, - accountPassword: exchangeBankAccount.accountPassword, - allowHttp: true, - }); + const wireGatewayApiClient = new WireGatewayApiClient( + exchangeBankAccount.wireGatewayApiBaseUrl, + { + auth: { + username: exchangeBankAccount.accountName, + password: exchangeBankAccount.accountPassword, + }, + }, + ); await wireGatewayApiClient.adminAddIncoming({ amount: "TESTKUDOS:115", @@ -134,9 +139,11 @@ export async function runBankApiTest(t: GlobalTestState) { reservePub: encodeCrock(res.eddsaPub), }); - balResp = await BankAccessApi.getAccountBalance(bank, bankUser); + balResp = await bankClient.getAccountBalance(bankUser.username); t.assertAmountEquals(balResp.balance.amount, "TESTKUDOS:15"); t.assertTrue( balResp.balance.credit_debit_indicator === CreditDebitIndicator.Debit, ); } + +runBankApiTest.suites = ["fakebank"]
\ No newline at end of file diff --git a/packages/taler-harness/src/integrationtests/test-claim-loop.ts b/packages/taler-harness/src/integrationtests/test-claim-loop.ts index 32706c28b..a424e0101 100644 --- a/packages/taler-harness/src/integrationtests/test-claim-loop.ts +++ b/packages/taler-harness/src/integrationtests/test-claim-loop.ts @@ -19,11 +19,12 @@ */ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { URL } from "url"; -import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; +import { GlobalTestState } from "../harness/harness.js"; import { createSimpleTestkudosEnvironmentV2, withdrawViaBankV2, } from "../harness/helpers.js"; +import { MerchantApiClient } from "@gnu-taler/taler-util"; /** * Run test for the merchant's order lifecycle. @@ -44,8 +45,10 @@ export async function runClaimLoopTest(t: GlobalTestState) { amount: "TESTKUDOS:20", }); + const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); + // Set up order. - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + const orderResp = await merchantClient.createOrder({ order: { summary: "Buy me!", amount: "TESTKUDOS:5", @@ -54,12 +57,9 @@ export async function runClaimLoopTest(t: GlobalTestState) { }); // Query private order status before claiming it. - let orderStatusBefore = await MerchantPrivateApi.queryPrivateOrderStatus( - merchant, - { - orderId: orderResp.order_id, - }, - ); + let orderStatusBefore = await merchantClient.queryPrivateOrderStatus({ + orderId: orderResp.order_id, + }); t.assertTrue(orderStatusBefore.order_status === "unpaid"); let statusUrlBefore = new URL(orderStatusBefore.order_status_url); @@ -71,13 +71,12 @@ export async function runClaimLoopTest(t: GlobalTestState) { }); // Query private order status after claiming it. - let orderStatusAfter = await MerchantPrivateApi.queryPrivateOrderStatus( - merchant, - { - orderId: orderResp.order_id, - }, - ); + let orderStatusAfter = await merchantClient.queryPrivateOrderStatus({ + orderId: orderResp.order_id, + }); t.assertTrue(orderStatusAfter.order_status === "claimed"); await t.shutdown(); } + +runClaimLoopTest.suites = ["merchant"];
\ No newline at end of file diff --git a/packages/taler-harness/src/integrationtests/test-clause-schnorr.ts b/packages/taler-harness/src/integrationtests/test-clause-schnorr.ts index bf42dc4c6..a5ad382a7 100644 --- a/packages/taler-harness/src/integrationtests/test-clause-schnorr.ts +++ b/packages/taler-harness/src/integrationtests/test-clause-schnorr.ts @@ -17,12 +17,13 @@ /** * Imports. */ +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; import { GlobalTestState } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironment, - withdrawViaBank, - makeTestPayment, + createSimpleTestkudosEnvironmentV2, + makeTestPaymentV2, + withdrawViaBankV2, } from "../harness/helpers.js"; /** @@ -53,12 +54,18 @@ export async function runClauseSchnorrTest(t: GlobalTestState) { name: "rsa_dummy", }); - const { wallet, bank, exchange, merchant } = - await createSimpleTestkudosEnvironment(t, coinConfig); + const { walletClient, bank, exchange, merchant } = + await createSimpleTestkudosEnvironmentV2(t, coinConfig); // Withdraw digital cash into the wallet. - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); + const wres = await withdrawViaBankV2(t, { + walletClient, + bank, + exchange, + amount: "TESTKUDOS:20", + }); + await wres.withdrawalFinishedCond; const order = { summary: "Buy me!", @@ -66,8 +73,8 @@ export async function runClauseSchnorrTest(t: GlobalTestState) { fulfillment_url: "taler://fulfillment-success/thx", }; - await makeTestPayment(t, { wallet, merchant, order }); - await wallet.runUntilDone(); + await makeTestPaymentV2(t, { walletClient, merchant, order }); + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); // Test JSON normalization of contract terms: Does the wallet // agree with the merchant? @@ -77,8 +84,8 @@ export async function runClauseSchnorrTest(t: GlobalTestState) { fulfillment_url: "taler://fulfillment-success/thx", }; - await makeTestPayment(t, { wallet, merchant, order: order2 }); - await wallet.runUntilDone(); + await makeTestPaymentV2(t, { walletClient, merchant, order: order2 }); + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); // Test JSON normalization of contract terms: Does the wallet // agree with the merchant? @@ -88,10 +95,9 @@ export async function runClauseSchnorrTest(t: GlobalTestState) { fulfillment_url: "taler://fulfillment-success/thx", }; - await makeTestPayment(t, { wallet, merchant, order: order3 }); - - await wallet.runUntilDone(); + await makeTestPaymentV2(t, { walletClient, merchant, order: order3 }); + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); } runClauseSchnorrTest.suites = ["experimental-wallet"]; -runClauseSchnorrTest.excludeByDefault = true; +runClauseSchnorrTest.experimental = true; diff --git a/packages/taler-harness/src/integrationtests/test-denom-unoffered.ts b/packages/taler-harness/src/integrationtests/test-denom-unoffered.ts index 5a471b9aa..b4268ee42 100644 --- a/packages/taler-harness/src/integrationtests/test-denom-unoffered.ts +++ b/packages/taler-harness/src/integrationtests/test-denom-unoffered.ts @@ -17,9 +17,13 @@ /** * Imports. */ -import { PreparePayResultType, TalerErrorCode } from "@gnu-taler/taler-util"; +import { + MerchantApiClient, + PreparePayResultType, + TalerErrorCode, +} from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; +import { GlobalTestState } from "../harness/harness.js"; import { createSimpleTestkudosEnvironmentV2, withdrawViaBankV2, @@ -46,13 +50,14 @@ export async function runDenomUnofferedTest(t: GlobalTestState) { // Effectively we completely reset the exchange, // but keep the exchange master public key. + await merchant.stop(); + await exchange.stop(); await exchange.purgeDatabase(); await exchange.purgeSecmodKeys(); await exchange.start(); await exchange.pingUntilAvailable(); - await merchant.stop(); await merchant.start(); await merchant.pingUntilAvailable(); @@ -62,11 +67,13 @@ export async function runDenomUnofferedTest(t: GlobalTestState) { fulfillment_url: "taler://fulfillment-success/thx", }; - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); + + const orderResp = await merchantClient.createOrder({ order: order, }); - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + let orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, }); diff --git a/packages/taler-harness/src/integrationtests/test-deposit.ts b/packages/taler-harness/src/integrationtests/test-deposit.ts index 8ea3fc12e..7e1bb2a5c 100644 --- a/packages/taler-harness/src/integrationtests/test-deposit.ts +++ b/packages/taler-harness/src/integrationtests/test-deposit.ts @@ -102,3 +102,5 @@ export async function runDepositTest(t: GlobalTestState) { // deposit and wire fees. t.assertDeepEqual(transactions.transactions[1].amountRaw, "TESTKUDOS:9.79"); } + +runDepositTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-exchange-deposit.ts b/packages/taler-harness/src/integrationtests/test-exchange-deposit.ts index 05bbbfaa1..96255f5b5 100644 --- a/packages/taler-harness/src/integrationtests/test-exchange-deposit.ts +++ b/packages/taler-harness/src/integrationtests/test-exchange-deposit.ts @@ -49,7 +49,6 @@ export async function runExchangeDepositTest(t: GlobalTestState) { const { bank, exchange } = await createSimpleTestkudosEnvironmentV2(t); const http = createPlatformHttpLib({ - allowHttp: true, enableThrottling: false, }); const cryptiDisp = new CryptoDispatcher( diff --git a/packages/taler-harness/src/integrationtests/test-exchange-management.ts b/packages/taler-harness/src/integrationtests/test-exchange-management.ts index c67f861e1..9338a8988 100644 --- a/packages/taler-harness/src/integrationtests/test-exchange-management.ts +++ b/packages/taler-harness/src/integrationtests/test-exchange-management.ts @@ -18,30 +18,27 @@ * Imports. */ import { - GlobalTestState, - WalletCli, - setupDb, - BankService, - ExchangeService, - MerchantService, - getPayto, -} from "../harness/harness.js"; -import { - WalletApiOperation, - BankApi, - BankAccessApi, -} from "@gnu-taler/taler-wallet-core"; -import { + BankAccessApiClient, ExchangesListResponse, - URL, TalerErrorCode, + URL, j2s, } from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { defaultCoinConfig } from "../harness/denomStructures.js"; import { FaultInjectedExchangeService, FaultInjectionResponseContext, } from "../harness/faultInjection.js"; -import { defaultCoinConfig } from "../harness/denomStructures.js"; +import { + BankService, + ExchangeService, + GlobalTestState, + MerchantService, + WalletCli, + getPayto, + setupDb, +} from "../harness/harness.js"; /** * Test if the wallet handles outdated exchange versions correctly. @@ -105,13 +102,13 @@ export async function runExchangeManagementTest( await merchant.start(); await merchant.pingUntilAvailable(); - await merchant.addInstance({ + await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", paytoUris: [getPayto("merchant-default")], }); - await merchant.addInstance({ + await merchant.addInstanceWithWireAccount({ id: "minst1", name: "minst1", paytoUris: [getPayto("minst1")], @@ -266,10 +263,11 @@ export async function runExchangeManagementTest( // Create withdrawal operation - const user = await BankApi.createRandomBankUser(bank); - const wop = await BankAccessApi.createWithdrawalOperation( - bank, - user, + const bankClient = new BankAccessApiClient(bank.bankAccessApiBaseUrl); + + const user = await bankClient.createRandomBankUser(); + const wop = await bankClient.createWithdrawalOperation( + user.username, "TESTKUDOS:10", ); diff --git a/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts b/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts index 5ae97c3da..2ef7683b3 100644 --- a/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts +++ b/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts @@ -41,7 +41,11 @@ import { setupDb, WalletCli, } from "../harness/harness.js"; -import { withdrawViaBank } from "../harness/helpers.js"; +import { + applyTimeTravelV2, + createWalletDaemonWithClient, + withdrawViaBankV2, +} from "../harness/helpers.js"; const logger = new Logger("test-exchange-timetravel.ts"); @@ -89,36 +93,7 @@ function getDenomInfoFromKeys(ek: ExchangeKeysJson): DenomInfo[] { return denomInfos; } -async function applyTimeTravel( - timetravelDuration: Duration, - s: { - exchange?: ExchangeService; - merchant?: MerchantService; - wallet?: WalletCli; - }, -): Promise<void> { - if (s.exchange) { - await s.exchange.stop(); - s.exchange.setTimetravel(timetravelDuration); - await s.exchange.start(); - await s.exchange.pingUntilAvailable(); - } - - if (s.merchant) { - await s.merchant.stop(); - s.merchant.setTimetravel(timetravelDuration); - await s.merchant.start(); - await s.merchant.pingUntilAvailable(); - } - - if (s.wallet) { - console.log("setting wallet time travel to", timetravelDuration); - s.wallet.setTimetravel(timetravelDuration); - } -} - const http = createPlatformHttpLib({ - allowHttp: true, enableThrottling: false, }); @@ -173,13 +148,13 @@ export async function runExchangeTimetravelTest(t: GlobalTestState) { await merchant.start(); await merchant.pingUntilAvailable(); - await merchant.addInstance({ + await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", paytoUris: [getPayto("merchant-default")], }); - await merchant.addInstance({ + await merchant.addInstanceWithWireAccount({ id: "minst1", name: "minst1", paytoUris: [getPayto("minst1")], @@ -187,11 +162,19 @@ export async function runExchangeTimetravelTest(t: GlobalTestState) { console.log("setup done!"); - const wallet = new WalletCli(t); + const { walletClient } = await createWalletDaemonWithClient(t, { + name: "default", + }); // Withdraw digital cash into the wallet. - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:15" }); + const wres = await withdrawViaBankV2(t, { + walletClient, + bank, + exchange, + amount: "TESTKUDOS:15", + }); + await wres.withdrawalFinishedCond; const keysResp1 = await http.fetch(exchange.baseUrl + "keys"); const keys1 = await readSuccessResponseJsonOrThrow( @@ -206,11 +189,14 @@ export async function runExchangeTimetravelTest(t: GlobalTestState) { // Travel into the future, the deposit expiration is two years // into the future. console.log("applying first time travel"); - await applyTimeTravel(durationFromSpec({ days: 400 }), { - wallet, - exchange, - merchant, - }); + await applyTimeTravelV2( + Duration.toMilliseconds(durationFromSpec({ days: 400 })), + { + walletClient, + exchange, + merchant, + }, + ); const keysResp2 = await http.fetch(exchange.baseUrl + "keys"); const keys2 = await readSuccessResponseJsonOrThrow( diff --git a/packages/taler-harness/src/integrationtests/test-fee-regression.ts b/packages/taler-harness/src/integrationtests/test-fee-regression.ts index e0dc4bc3b..2d84b3a7c 100644 --- a/packages/taler-harness/src/integrationtests/test-fee-regression.ts +++ b/packages/taler-harness/src/integrationtests/test-fee-regression.ts @@ -139,7 +139,7 @@ export async function createMyTestkudosEnvironment( await merchant.pingUntilAvailable(); await merchant.addDefaultInstance(); - await merchant.addInstance({ + await merchant.addInstanceWithWireAccount({ id: "minst1", name: "minst1", paytoUris: [getPayto("minst1")], diff --git a/packages/taler-harness/src/integrationtests/test-kyc.ts b/packages/taler-harness/src/integrationtests/test-kyc.ts index 0fe24d708..1f7358b66 100644 --- a/packages/taler-harness/src/integrationtests/test-kyc.ts +++ b/packages/taler-harness/src/integrationtests/test-kyc.ts @@ -18,6 +18,7 @@ * Imports. */ import { + BankAccessApiClient, Duration, j2s, Logger, @@ -26,11 +27,9 @@ import { TransactionMinorState, TransactionType, } from "@gnu-taler/taler-util"; -import { - BankAccessApi, - BankApi, - WalletApiOperation, -} from "@gnu-taler/taler-wallet-core"; +import { createPlatformHttpLib } from "@gnu-taler/taler-util/http"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import * as http from "node:http"; import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; import { BankService, @@ -43,8 +42,6 @@ import { WalletService, } from "../harness/harness.js"; import { EnvOptions, SimpleTestEnvironmentNg } from "../harness/helpers.js"; -import * as http from "node:http"; -import { createPlatformHttpLib } from "@gnu-taler/taler-util/http"; const logger = new Logger("test-kyc.ts"); @@ -162,7 +159,7 @@ export async function createKycTestkudosEnvironment( await merchant.start(); await merchant.pingUntilAvailable(); - await merchant.addInstance({ + await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", paytoUris: [getPayto("merchant-default")], @@ -171,7 +168,7 @@ export async function createKycTestkudosEnvironment( ), }); - await merchant.addInstance({ + await merchant.addInstanceWithWireAccount({ id: "minst1", name: "minst1", paytoUris: [getPayto("minst1")], @@ -305,9 +302,11 @@ export async function runKycTest(t: GlobalTestState) { // Withdraw digital cash into the wallet. + const bankClient = new BankAccessApiClient(bank.bankAccessApiBaseUrl); + const amount = "TESTKUDOS:20"; - const user = await BankApi.createRandomBankUser(bank); - const wop = await BankAccessApi.createWithdrawalOperation(bank, user, amount); + const user = await bankClient.createRandomBankUser(); + const wop = await bankClient.createWithdrawalOperation(user.username, amount); // Hand it to the wallet @@ -332,7 +331,7 @@ export async function runKycTest(t: GlobalTestState) { // Confirm it - await BankApi.confirmWithdrawalOperation(bank, user, wop); + await bankClient.confirmWithdrawalOperation(user.username, wop); const kycNotificationCond = walletClient.waitForNotificationCond((x) => { if ( @@ -376,7 +375,6 @@ export async function runKycTest(t: GlobalTestState) { // which would usually done in the browser. const httpLib = createPlatformHttpLib({ - allowHttp: true, enableThrottling: false, }); const kycServerResp = await httpLib.fetch(kycUrl); diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-api-sandbox-camt.ts b/packages/taler-harness/src/integrationtests/test-libeufin-api-sandbox-camt.ts index 239bab334..22b411dc2 100644 --- a/packages/taler-harness/src/integrationtests/test-libeufin-api-sandbox-camt.ts +++ b/packages/taler-harness/src/integrationtests/test-libeufin-api-sandbox-camt.ts @@ -72,5 +72,5 @@ export async function runLibeufinApiSandboxCamtTest(t: GlobalTestState) { let ret = await LibeufinSandboxApi.getCamt053(sandbox, "mock-account-1"); console.log(ret); } -runLibeufinApiSandboxCamtTest.excludeByDefault = true; +runLibeufinApiSandboxCamtTest.experimental = true; runLibeufinApiSandboxCamtTest.suites = ["libeufin"]; diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-basic.ts b/packages/taler-harness/src/integrationtests/test-libeufin-basic.ts index f03b63f50..d87278197 100644 --- a/packages/taler-harness/src/integrationtests/test-libeufin-basic.ts +++ b/packages/taler-harness/src/integrationtests/test-libeufin-basic.ts @@ -19,23 +19,24 @@ */ import { AbsoluteTime, - MerchantContractTerms, Duration, + MerchantContractTerms, } from "@gnu-taler/taler-util"; -import { - WalletApiOperation, - HarnessExchangeBankAccount, -} from "@gnu-taler/taler-wallet-core"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; import { DbInfo, ExchangeService, GlobalTestState, + HarnessExchangeBankAccount, MerchantService, + WalletClient, setupDb, - WalletCli, } from "../harness/harness.js"; -import { makeTestPayment } from "../harness/helpers.js"; +import { + createWalletDaemonWithClient, + makeTestPaymentV2, +} from "../harness/helpers.js"; import { LibeufinNexusApi, LibeufinNexusService, @@ -53,7 +54,7 @@ export interface LibeufinTestEnvironment { exchange: ExchangeService; exchangeBankAccount: HarnessExchangeBankAccount; merchant: MerchantService; - wallet: WalletCli; + walletClient: WalletClient; libeufinSandbox: LibeufinSandboxService; libeufinNexus: LibeufinNexusService; } @@ -69,7 +70,7 @@ export async function createLibeufinTestEnvironment( const libeufinSandbox = await LibeufinSandboxService.create(t, { httpPort: 5010, - databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-sandbox.sqlite3`, + databaseJdbcUri: db.connStr, }); await libeufinSandbox.start(); @@ -77,7 +78,7 @@ export async function createLibeufinTestEnvironment( const libeufinNexus = await LibeufinNexusService.create(t, { httpPort: 5011, - databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-nexus.sqlite3`, + databaseJdbcUri: db.connStr, }); await libeufinNexus.start(); @@ -202,7 +203,7 @@ export async function createLibeufinTestEnvironment( await merchant.start(); await merchant.pingUntilAvailable(); - await merchant.addInstance({ + await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", paytoUris: [`payto://iban/${merchantIban}?receiver-name=Merchant`], @@ -213,13 +214,15 @@ export async function createLibeufinTestEnvironment( console.log("setup done!"); - const wallet = new WalletCli(t); + const { walletClient } = await createWalletDaemonWithClient(t, { + name: "default", + }); return { commonDb: db, exchange, merchant, - wallet, + walletClient, exchangeBankAccount, libeufinNexus, libeufinSandbox, @@ -232,14 +235,14 @@ export async function createLibeufinTestEnvironment( export async function runLibeufinBasicTest(t: GlobalTestState) { // Set up test environment - const { wallet, exchange, merchant, libeufinSandbox, libeufinNexus } = + const { walletClient, exchange, merchant, libeufinSandbox, libeufinNexus } = await createLibeufinTestEnvironment(t); - await wallet.client.call(WalletApiOperation.AddExchange, { + await walletClient.call(WalletApiOperation.AddExchange, { exchangeBaseUrl: exchange.baseUrl, }); - const wr = await wallet.client.call( + const wr = await walletClient.call( WalletApiOperation.AcceptManualWithdrawal, { exchangeBaseUrl: exchange.baseUrl, @@ -265,9 +268,9 @@ export async function runLibeufinBasicTest(t: GlobalTestState) { await exchange.runWirewatchOnce(); - await wallet.runUntilDone(); + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); - const bal = await wallet.client.call(WalletApiOperation.GetBalances, {}); + const bal = await walletClient.call(WalletApiOperation.GetBalances, {}); console.log("balances", JSON.stringify(bal, undefined, 2)); t.assertAmountEquals(bal.balances[0].available, "EUR:14.7"); @@ -275,10 +278,12 @@ export async function runLibeufinBasicTest(t: GlobalTestState) { summary: "Buy me!", amount: "EUR:5", fulfillment_url: "taler://fulfillment-success/thx", - wire_transfer_deadline: AbsoluteTime.toProtocolTimestamp(AbsoluteTime.now()), + wire_transfer_deadline: AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.now(), + ), }; - await makeTestPayment(t, { wallet, merchant, order }); + await makeTestPaymentV2(t, { walletClient, merchant, order }); await exchange.runAggregatorOnce(); await exchange.runTransferOnce(); diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-nexus-balance.ts b/packages/taler-harness/src/integrationtests/test-libeufin-nexus-balance.ts index 68b0174cc..868f93759 100644 --- a/packages/taler-harness/src/integrationtests/test-libeufin-nexus-balance.ts +++ b/packages/taler-harness/src/integrationtests/test-libeufin-nexus-balance.ts @@ -114,4 +114,4 @@ export async function runLibeufinNexusBalanceTest(t: GlobalTestState) { } runLibeufinNexusBalanceTest.suites = ["libeufin"]; -runLibeufinNexusBalanceTest.excludeByDefault = true; +runLibeufinNexusBalanceTest.experimental = true; diff --git a/packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts b/packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts index e18cd7a0f..2f79041d6 100644 --- a/packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts +++ b/packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts @@ -20,6 +20,7 @@ import { codecForMerchantOrderStatusUnpaid, ConfirmPayResultType, + MerchantApiClient, PreparePayResultType, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; @@ -35,16 +36,14 @@ import { getPayto, GlobalTestState, harnessHttpLib, - MerchantPrivateApi, MerchantService, setupDb, - WalletCli, } from "../harness/harness.js"; import { + createWalletDaemonWithClient, FaultyMerchantTestEnvironment, - withdrawViaBank, + withdrawViaBankV2, } from "../harness/helpers.js"; -import { createPlatformHttpLib } from "@gnu-taler/taler-util/http"; /** * Run a test case with a simple TESTKUDOS Taler environment, consisting @@ -110,13 +109,13 @@ export async function createConfusedMerchantTestkudosEnvironment( await merchant.start(); await merchant.pingUntilAvailable(); - await merchant.addInstance({ + await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", paytoUris: [getPayto("merchant-default")], }); - await merchant.addInstance({ + await merchant.addInstanceWithWireAccount({ id: "minst1", name: "minst1", paytoUris: [getPayto("minst1")], @@ -124,13 +123,15 @@ export async function createConfusedMerchantTestkudosEnvironment( console.log("setup done!"); - const wallet = new WalletCli(t); + const { walletClient } = await createWalletDaemonWithClient(t, { + name: "default", + }); return { commonDb: db, exchange, merchant, - wallet, + walletClient, bank, exchangeBankAccount, faultyMerchant, @@ -145,18 +146,20 @@ export async function createConfusedMerchantTestkudosEnvironment( export async function runMerchantExchangeConfusionTest(t: GlobalTestState) { // Set up test environment - const { wallet, bank, faultyExchange, faultyMerchant } = + const { walletClient, bank, faultyExchange, faultyMerchant } = await createConfusedMerchantTestkudosEnvironment(t); // Withdraw digital cash into the wallet. - await withdrawViaBank(t, { - wallet, + const wres = await withdrawViaBankV2(t, { + walletClient, bank, exchange: faultyExchange, amount: "TESTKUDOS:20", }); + await wres.withdrawalFinishedCond; + /** * ========================================================================= * Create an order and let the wallet pay under a session ID @@ -168,7 +171,9 @@ export async function runMerchantExchangeConfusionTest(t: GlobalTestState) { const merchant = faultyMerchant; - let orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); + + let orderResp = await merchantClient.createOrder({ order: { summary: "Buy me!", amount: "TESTKUDOS:5", @@ -176,7 +181,7 @@ export async function runMerchantExchangeConfusionTest(t: GlobalTestState) { }, }); - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + let orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, sessionId: "mysession-one", }); @@ -200,7 +205,7 @@ export async function runMerchantExchangeConfusionTest(t: GlobalTestState) { console.log(pubUnpaidStatus); - let preparePayResp = await wallet.client.call( + let preparePayResp = await walletClient.call( WalletApiOperation.PreparePayForUri, { talerPayUri: pubUnpaidStatus.taler_pay_uri, @@ -231,12 +236,9 @@ export async function runMerchantExchangeConfusionTest(t: GlobalTestState) { await publicOrderStatusResp.json(), ); - const confirmPayRes = await wallet.client.call( - WalletApiOperation.ConfirmPay, - { - proposalId: proposalId, - }, - ); + const confirmPayRes = await walletClient.call(WalletApiOperation.ConfirmPay, { + proposalId: proposalId, + }); t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done); } diff --git a/packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts b/packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts index e6e5bff76..ff567d33d 100644 --- a/packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts +++ b/packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts @@ -17,17 +17,15 @@ /** * Imports. */ -import { TalerError, URL } from "@gnu-taler/taler-util"; +import { MerchantApiClient, TalerError, URL } from "@gnu-taler/taler-util"; import { ExchangeService, GlobalTestState, - MerchantApiClient, MerchantService, - setupDb, getPayto, harnessHttpLib, + setupDb, } from "../harness/harness.js"; -import { createPlatformHttpLib } from "@gnu-taler/taler-util/http"; /** * Test instance deletion and authentication for it @@ -69,13 +67,15 @@ export async function runMerchantInstancesDeleteTest(t: GlobalTestState) { // Instances should initially be empty { - const r = await harnessHttpLib.fetch(new URL("management/instances", baseUrl).href); + const r = await harnessHttpLib.fetch( + new URL("management/instances", baseUrl).href, + ); const data = await r.json(); t.assertDeepEqual(data.instances, []); } // Add an instance, no auth! - await merchant.addInstance({ + await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", paytoUris: [getPayto("merchant-default")], @@ -85,7 +85,7 @@ export async function runMerchantInstancesDeleteTest(t: GlobalTestState) { }); // Add an instance, no auth! - await merchant.addInstance({ + await merchant.addInstanceWithWireAccount({ id: "myinst", name: "Second Instance", paytoUris: [getPayto("merchant-default")], diff --git a/packages/taler-harness/src/integrationtests/test-merchant-instances-urls.ts b/packages/taler-harness/src/integrationtests/test-merchant-instances-urls.ts index 18a09c76b..071288b0f 100644 --- a/packages/taler-harness/src/integrationtests/test-merchant-instances-urls.ts +++ b/packages/taler-harness/src/integrationtests/test-merchant-instances-urls.ts @@ -17,17 +17,15 @@ /** * Imports. */ -import { Duration } from "@gnu-taler/taler-util"; +import { Duration, MerchantApiClient } from "@gnu-taler/taler-util"; import { ExchangeService, GlobalTestState, - MerchantApiClient, MerchantService, - setupDb, getPayto, harnessHttpLib, + setupDb, } from "../harness/harness.js"; -import { createPlatformHttpLib } from "@gnu-taler/taler-util/http"; /** * Do basic checks on instance management and authentication. diff --git a/packages/taler-harness/src/integrationtests/test-merchant-instances.ts b/packages/taler-harness/src/integrationtests/test-merchant-instances.ts index f7d89c543..fd7a8ca3a 100644 --- a/packages/taler-harness/src/integrationtests/test-merchant-instances.ts +++ b/packages/taler-harness/src/integrationtests/test-merchant-instances.ts @@ -17,17 +17,15 @@ /** * Imports. */ -import { URL } from "@gnu-taler/taler-util"; +import { MerchantApiClient, URL } from "@gnu-taler/taler-util"; import { ExchangeService, GlobalTestState, - MerchantApiClient, MerchantService, setupDb, getPayto, harnessHttpLib, } from "../harness/harness.js"; -import { createPlatformHttpLib } from "@gnu-taler/taler-util/http"; /** * Do basic checks on instance management and authentication. @@ -77,7 +75,17 @@ export async function runMerchantInstancesTest(t: GlobalTestState) { } // Add an instance, no auth! - await merchant.addInstance({ + await merchant.addInstanceWithWireAccount({ + id: "default", + name: "Default Instance", + paytoUris: [getPayto("merchant-default")], + auth: { + method: "external", + }, + }); + + // Add it again, should be idempotent + await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", paytoUris: [getPayto("merchant-default")], @@ -87,7 +95,7 @@ export async function runMerchantInstancesTest(t: GlobalTestState) { }); // Add an instance, no auth! - await merchant.addInstance({ + await merchant.addInstanceWithWireAccount({ id: "myinst", name: "Second Instance", paytoUris: [getPayto("merchant-default")], diff --git a/packages/taler-harness/src/integrationtests/test-merchant-longpolling.ts b/packages/taler-harness/src/integrationtests/test-merchant-longpolling.ts index 8d271c5d1..bd63a8445 100644 --- a/packages/taler-harness/src/integrationtests/test-merchant-longpolling.ts +++ b/packages/taler-harness/src/integrationtests/test-merchant-longpolling.ts @@ -19,17 +19,17 @@ */ import { ConfirmPayResultType, + MerchantApiClient, PreparePayResultType, URL, codecForMerchantOrderStatusUnpaid, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { GlobalTestState, MerchantPrivateApi, harnessHttpLib } from "../harness/harness.js"; +import { GlobalTestState, harnessHttpLib } from "../harness/harness.js"; import { createSimpleTestkudosEnvironmentV2, withdrawViaBankV2, } from "../harness/helpers.js"; -import { createPlatformHttpLib } from "@gnu-taler/taler-util/http"; /** * Run test for basic, bank-integrated withdrawal. @@ -50,6 +50,8 @@ export async function runMerchantLongpollingTest(t: GlobalTestState) { await wres.withdrawalFinishedCond; + const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); + /** * ========================================================================= * Create an order and let the wallet pay under a session ID @@ -59,7 +61,7 @@ export async function runMerchantLongpollingTest(t: GlobalTestState) { * ========================================================================= */ - let orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + let orderResp = await merchantClient.createOrder({ order: { summary: "Buy me!", amount: "TESTKUDOS:5", @@ -68,7 +70,7 @@ export async function runMerchantLongpollingTest(t: GlobalTestState) { create_token: false, }); - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + let orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, sessionId: "mysession-one", }); @@ -81,7 +83,9 @@ export async function runMerchantLongpollingTest(t: GlobalTestState) { // First, request order status without longpolling { console.log("requesting", publicOrderStatusUrl.href); - let publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl.href); + let publicOrderStatusResp = await harnessHttpLib.fetch( + publicOrderStatusUrl.href, + ); if (publicOrderStatusResp.status != 402) { throw Error( @@ -94,7 +98,9 @@ export async function runMerchantLongpollingTest(t: GlobalTestState) { publicOrderStatusUrl.searchParams.set("timeout_ms", "500"); console.log("requesting", publicOrderStatusUrl.href); - let publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl.href); + let publicOrderStatusResp = await harnessHttpLib.fetch( + publicOrderStatusUrl.href, + ); if (publicOrderStatusResp.status != 402) { throw Error( @@ -129,7 +135,9 @@ export async function runMerchantLongpollingTest(t: GlobalTestState) { preparePayResp.contractTermsHash, ); - let publicOrderStatusPromise = harnessHttpLib.fetch(publicOrderStatusUrl.href); + let publicOrderStatusPromise = harnessHttpLib.fetch( + publicOrderStatusUrl.href, + ); t.assertTrue(preparePayResp.status === PreparePayResultType.PaymentPossible); diff --git a/packages/taler-harness/src/integrationtests/test-merchant-refund-api.ts b/packages/taler-harness/src/integrationtests/test-merchant-refund-api.ts index 8efac1fc1..1b69b9de6 100644 --- a/packages/taler-harness/src/integrationtests/test-merchant-refund-api.ts +++ b/packages/taler-harness/src/integrationtests/test-merchant-refund-api.ts @@ -17,43 +17,42 @@ /** * Imports. */ -import { createPlatformHttpLib } from "@gnu-taler/taler-util/http"; import { + Duration, + MerchantApiClient, + PreparePayResultType, + URL, + durationFromSpec, +} from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { + BankServiceHandle, + ExchangeServiceInterface, GlobalTestState, - MerchantPrivateApi, MerchantServiceInterface, - WalletCli, - ExchangeServiceInterface, + WalletClient, harnessHttpLib, } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironment, - withdrawViaBank, + createSimpleTestkudosEnvironmentV2, + withdrawViaBankV2, } from "../harness/helpers.js"; -import { - URL, - durationFromSpec, - PreparePayResultType, - Duration, -} from "@gnu-taler/taler-util"; -import { - WalletApiOperation, - BankServiceHandle, -} from "@gnu-taler/taler-wallet-core"; async function testRefundApiWithFulfillmentUrl( t: GlobalTestState, env: { merchant: MerchantServiceInterface; bank: BankServiceHandle; - wallet: WalletCli; + walletClient: WalletClient; exchange: ExchangeServiceInterface; }, ): Promise<void> { - const { wallet, bank, exchange, merchant } = env; + const { walletClient, merchant } = env; + + const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); // Set up order. - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + const orderResp = await merchantClient.createOrder({ order: { summary: "Buy me!", amount: "TESTKUDOS:5", @@ -64,7 +63,7 @@ async function testRefundApiWithFulfillmentUrl( ), }); - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + let orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, }); @@ -75,7 +74,7 @@ async function testRefundApiWithFulfillmentUrl( // Make wallet pay for the order - let preparePayResult = await wallet.client.call( + let preparePayResult = await walletClient.call( WalletApiOperation.PreparePayForUri, { talerPayUri, @@ -86,19 +85,19 @@ async function testRefundApiWithFulfillmentUrl( preparePayResult.status === PreparePayResultType.PaymentPossible, ); - await wallet.client.call(WalletApiOperation.ConfirmPay, { + await walletClient.call(WalletApiOperation.ConfirmPay, { proposalId: preparePayResult.proposalId, }); // Check if payment was successful. - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, }); t.assertTrue(orderStatus.order_status === "paid"); - preparePayResult = await wallet.client.call( + preparePayResult = await walletClient.call( WalletApiOperation.PreparePayForUri, { talerPayUri, @@ -109,14 +108,14 @@ async function testRefundApiWithFulfillmentUrl( preparePayResult.status === PreparePayResultType.AlreadyConfirmed, ); - await MerchantPrivateApi.giveRefund(merchant, { + await merchantClient.giveRefund({ amount: "TESTKUDOS:5", instance: "default", justification: "foo", orderId: orderResp.order_id, }); - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, }); @@ -136,7 +135,9 @@ async function testRefundApiWithFulfillmentUrl( preparePayResult.contractTermsHash, ); - let publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl.href); + let publicOrderStatusResp = await harnessHttpLib.fetch( + publicOrderStatusUrl.href, + ); const respData = await publicOrderStatusResp.json(); t.assertTrue(publicOrderStatusResp.status === 200); t.assertAmountEquals(respData.refund_amount, "TESTKUDOS:5"); @@ -158,14 +159,16 @@ async function testRefundApiWithFulfillmentMessage( env: { merchant: MerchantServiceInterface; bank: BankServiceHandle; - wallet: WalletCli; + walletClient: WalletClient; exchange: ExchangeServiceInterface; }, ): Promise<void> { - const { wallet, bank, exchange, merchant } = env; + const { walletClient, merchant } = env; + + const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); // Set up order. - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + const orderResp = await merchantClient.createOrder({ order: { summary: "Buy me!", amount: "TESTKUDOS:5", @@ -176,7 +179,7 @@ async function testRefundApiWithFulfillmentMessage( ), }); - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + let orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, }); @@ -187,7 +190,7 @@ async function testRefundApiWithFulfillmentMessage( // Make wallet pay for the order - let preparePayResult = await wallet.client.call( + let preparePayResult = await walletClient.call( WalletApiOperation.PreparePayForUri, { talerPayUri, @@ -198,19 +201,19 @@ async function testRefundApiWithFulfillmentMessage( preparePayResult.status === PreparePayResultType.PaymentPossible, ); - await wallet.client.call(WalletApiOperation.ConfirmPay, { + await walletClient.call(WalletApiOperation.ConfirmPay, { proposalId: preparePayResult.proposalId, }); // Check if payment was successful. - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, }); t.assertTrue(orderStatus.order_status === "paid"); - preparePayResult = await wallet.client.call( + preparePayResult = await walletClient.call( WalletApiOperation.PreparePayForUri, { talerPayUri, @@ -221,14 +224,14 @@ async function testRefundApiWithFulfillmentMessage( preparePayResult.status === PreparePayResultType.AlreadyConfirmed, ); - await MerchantPrivateApi.giveRefund(merchant, { + await merchantClient.giveRefund({ amount: "TESTKUDOS:5", instance: "default", justification: "foo", orderId: orderResp.order_id, }); - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, }); @@ -248,7 +251,9 @@ async function testRefundApiWithFulfillmentMessage( preparePayResult.contractTermsHash, ); - let publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl.href); + let publicOrderStatusResp = await harnessHttpLib.fetch( + publicOrderStatusUrl.href, + ); let respData = await publicOrderStatusResp.json(); console.log(respData); t.assertTrue(publicOrderStatusResp.status === 200); @@ -272,22 +277,28 @@ async function testRefundApiWithFulfillmentMessage( export async function runMerchantRefundApiTest(t: GlobalTestState) { // Set up test environment - const { wallet, bank, exchange, merchant } = - await createSimpleTestkudosEnvironment(t); + const { walletClient, bank, exchange, merchant } = + await createSimpleTestkudosEnvironmentV2(t); // Withdraw digital cash into the wallet. - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); + const wres = await withdrawViaBankV2(t, { + walletClient, + bank, + exchange, + amount: "TESTKUDOS:20", + }); + await wres.withdrawalFinishedCond; await testRefundApiWithFulfillmentUrl(t, { - wallet, + walletClient, bank, exchange, merchant, }); await testRefundApiWithFulfillmentMessage(t, { - wallet, + walletClient, bank, exchange, merchant, diff --git a/packages/taler-harness/src/integrationtests/test-merchant-spec-public-orders.ts b/packages/taler-harness/src/integrationtests/test-merchant-spec-public-orders.ts index e959e813b..afae8a899 100644 --- a/packages/taler-harness/src/integrationtests/test-merchant-spec-public-orders.ts +++ b/packages/taler-harness/src/integrationtests/test-merchant-spec-public-orders.ts @@ -19,6 +19,7 @@ */ import { ConfirmPayResultType, + MerchantApiClient, PreparePayResultType, URL, encodeCrock, @@ -29,14 +30,13 @@ import { BankService, ExchangeService, GlobalTestState, - MerchantPrivateApi, MerchantService, - WalletCli, harnessHttpLib, } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironment, - withdrawViaBank, + createSimpleTestkudosEnvironmentV2, + createWalletDaemonWithClient, + withdrawViaBankV2, } from "../harness/helpers.js"; interface Context { @@ -52,12 +52,21 @@ async function testWithClaimToken( t: GlobalTestState, c: Context, ): Promise<void> { - const wallet = new WalletCli(t, "withclaimtoken"); + const { walletClient } = await createWalletDaemonWithClient(t, { + name: "wct", + }); const { bank, exchange } = c; const { merchant, merchantBaseUrl } = c; - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); + const wres = await withdrawViaBankV2(t, { + walletClient, + bank, + exchange, + amount: "TESTKUDOS:20", + }); + await wres.withdrawalFinishedCond; const sessionId = "mysession"; - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); + const orderResp = await merchantClient.createOrder({ order: { summary: "Buy me!", amount: "TESTKUDOS:5", @@ -104,7 +113,7 @@ async function testWithClaimToken( console.log(r); } - const preparePayResp = await wallet.client.call( + const preparePayResp = await walletClient.call( WalletApiOperation.PreparePayForUri, { talerPayUri, @@ -166,12 +175,9 @@ async function testWithClaimToken( t.assertDeepEqual(httpResp.status, 202); } - const confirmPayRes = await wallet.client.call( - WalletApiOperation.ConfirmPay, - { - proposalId: proposalId, - }, - ); + const confirmPayRes = await walletClient.call(WalletApiOperation.ConfirmPay, { + proposalId: proposalId, + }); t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done); @@ -238,7 +244,7 @@ async function testWithClaimToken( t.assertDeepEqual(httpResp.status, 200); } - const confirmPayRes2 = await wallet.client.call( + const confirmPayRes2 = await walletClient.call( WalletApiOperation.ConfirmPay, { proposalId: proposalId, @@ -249,18 +255,14 @@ async function testWithClaimToken( t.assertTrue(confirmPayRes2.type === ConfirmPayResultType.Done); // Create another order with identical fulfillment URL to test the "already paid" flow - const alreadyPaidOrderResp = await MerchantPrivateApi.createOrder( - merchant, - "default", - { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "https://example.com/article42", - public_reorder_url: "https://example.com/article42-share", - }, + const alreadyPaidOrderResp = await merchantClient.createOrder({ + order: { + summary: "Buy me!", + amount: "TESTKUDOS:5", + fulfillment_url: "https://example.com/article42", + public_reorder_url: "https://example.com/article42-share", }, - ); + }); const apOrderId = alreadyPaidOrderResp.order_id; const apToken = alreadyPaidOrderResp.token; @@ -307,12 +309,21 @@ async function testWithoutClaimToken( t: GlobalTestState, c: Context, ): Promise<void> { - const wallet = new WalletCli(t, "withoutct"); + const { walletClient } = await createWalletDaemonWithClient(t, { + name: "wnoct", + }); const sessionId = "mysession2"; const { bank, exchange } = c; const { merchant, merchantBaseUrl } = c; - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); + const wres = await withdrawViaBankV2(t, { + walletClient, + bank, + exchange, + amount: "TESTKUDOS:20", + }); + await wres.withdrawalFinishedCond; + const orderResp = await merchantClient.createOrder({ order: { summary: "Buy me!", amount: "TESTKUDOS:5", @@ -356,7 +367,7 @@ async function testWithoutClaimToken( console.log(r); } - const preparePayResp = await wallet.client.call( + const preparePayResp = await walletClient.call( WalletApiOperation.PreparePayForUri, { talerPayUri, @@ -422,12 +433,9 @@ async function testWithoutClaimToken( t.assertDeepEqual(httpResp.status, 402); } - const confirmPayRes = await wallet.client.call( - WalletApiOperation.ConfirmPay, - { - proposalId: proposalId, - }, - ); + const confirmPayRes = await walletClient.call(WalletApiOperation.ConfirmPay, { + proposalId: proposalId, + }); t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done); @@ -492,7 +500,7 @@ async function testWithoutClaimToken( t.assertDeepEqual(httpResp.status, 200); } - const confirmPayRes2 = await wallet.client.call( + const confirmPayRes2 = await walletClient.call( WalletApiOperation.ConfirmPay, { proposalId: proposalId, @@ -503,18 +511,14 @@ async function testWithoutClaimToken( t.assertTrue(confirmPayRes2.type === ConfirmPayResultType.Done); // Create another order with identical fulfillment URL to test the "already paid" flow - const alreadyPaidOrderResp = await MerchantPrivateApi.createOrder( - merchant, - "default", - { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "https://example.com/article42", - public_reorder_url: "https://example.com/article42-share", - }, + const alreadyPaidOrderResp = await merchantClient.createOrder({ + order: { + summary: "Buy me!", + amount: "TESTKUDOS:5", + fulfillment_url: "https://example.com/article42", + public_reorder_url: "https://example.com/article42-share", }, - ); + }); const apOrderId = alreadyPaidOrderResp.order_id; const apToken = alreadyPaidOrderResp.token; @@ -564,7 +568,7 @@ async function testWithoutClaimToken( * specification of the endpoint. */ export async function runMerchantSpecPublicOrdersTest(t: GlobalTestState) { - const { bank, exchange, merchant } = await createSimpleTestkudosEnvironment( + const { bank, exchange, merchant } = await createSimpleTestkudosEnvironmentV2( t, ); @@ -572,7 +576,9 @@ export async function runMerchantSpecPublicOrdersTest(t: GlobalTestState) { const merchantBaseUrl = merchant.makeInstanceBaseUrl(); { - const httpResp = await httpLib.fetch(new URL("config", merchantBaseUrl).href); + const httpResp = await httpLib.fetch( + new URL("config", merchantBaseUrl).href, + ); const r = await httpResp.json(); console.log(r); t.assertDeepEqual(r.currency, "TESTKUDOS"); diff --git a/packages/taler-harness/src/integrationtests/test-pay-paid.ts b/packages/taler-harness/src/integrationtests/test-pay-paid.ts index a377b7237..3d93f6e29 100644 --- a/packages/taler-harness/src/integrationtests/test-pay-paid.ts +++ b/packages/taler-harness/src/integrationtests/test-pay-paid.ts @@ -17,19 +17,20 @@ /** * Imports. */ -import { GlobalTestState, MerchantPrivateApi, harnessHttpLib } from "../harness/harness.js"; import { - withdrawViaBank, - createFaultInjectedMerchantTestkudosEnvironment, -} from "../harness/helpers.js"; -import { - PreparePayResultType, - codecForMerchantOrderStatusUnpaid, ConfirmPayResultType, + MerchantApiClient, + PreparePayResultType, URL, + codecForMerchantOrderStatusUnpaid, } from "@gnu-taler/taler-util"; -import { FaultInjectionRequestContext } from "../harness/faultInjection.js"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { FaultInjectionRequestContext } from "../harness/faultInjection.js"; +import { GlobalTestState, harnessHttpLib } from "../harness/harness.js"; +import { + createFaultInjectedMerchantTestkudosEnvironment, + withdrawViaBankV2, +} from "../harness/helpers.js"; /** * Run test for the wallets repurchase detection mechanism @@ -42,18 +43,20 @@ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; export async function runPayPaidTest(t: GlobalTestState) { // Set up test environment - const { wallet, bank, faultyExchange, faultyMerchant } = + const { walletClient, bank, faultyExchange, faultyMerchant } = await createFaultInjectedMerchantTestkudosEnvironment(t); // Withdraw digital cash into the wallet. - await withdrawViaBank(t, { - wallet, + const wres = await withdrawViaBankV2(t, { + walletClient, bank, exchange: faultyExchange, amount: "TESTKUDOS:20", }); + await wres.withdrawalFinishedCond; + /** * ========================================================================= * Create an order and let the wallet pay under a session ID @@ -65,7 +68,9 @@ export async function runPayPaidTest(t: GlobalTestState) { const merchant = faultyMerchant; - let orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); + + let orderResp = await merchantClient.createOrder({ order: { summary: "Buy me!", amount: "TESTKUDOS:5", @@ -74,7 +79,7 @@ export async function runPayPaidTest(t: GlobalTestState) { }, }); - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + let orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, sessionId: "mysession-one", }); @@ -98,7 +103,7 @@ export async function runPayPaidTest(t: GlobalTestState) { console.log(pubUnpaidStatus); - let preparePayResp = await wallet.client.call( + let preparePayResp = await walletClient.call( WalletApiOperation.PreparePayForUri, { talerPayUri: pubUnpaidStatus.taler_pay_uri, @@ -121,12 +126,9 @@ export async function runPayPaidTest(t: GlobalTestState) { publicOrderStatusResp.json(), ); - const confirmPayRes = await wallet.client.call( - WalletApiOperation.ConfirmPay, - { - proposalId: proposalId, - }, - ); + const confirmPayRes = await walletClient.call(WalletApiOperation.ConfirmPay, { + proposalId: proposalId, + }); t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done); @@ -147,7 +149,7 @@ export async function runPayPaidTest(t: GlobalTestState) { * ========================================================================= */ - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, sessionId: "mysession-two", }); @@ -174,7 +176,7 @@ export async function runPayPaidTest(t: GlobalTestState) { }, }); - let orderRespTwo = await MerchantPrivateApi.createOrder(merchant, "default", { + let orderRespTwo = await merchantClient.createOrder({ order: { summary: "Buy me!", amount: "TESTKUDOS:5", @@ -183,20 +185,17 @@ export async function runPayPaidTest(t: GlobalTestState) { }, }); - let orderStatusTwo = await MerchantPrivateApi.queryPrivateOrderStatus( - merchant, - { - orderId: orderRespTwo.order_id, - sessionId: "mysession-two", - }, - ); + let orderStatusTwo = await merchantClient.queryPrivateOrderStatus({ + orderId: orderRespTwo.order_id, + sessionId: "mysession-two", + }); t.assertTrue(orderStatusTwo.order_status === "unpaid"); // Pay with new taler://pay URI, which should // have the new session ID! // Wallet should now automatically re-play payment. - preparePayResp = await wallet.client.call( + preparePayResp = await walletClient.call( WalletApiOperation.PreparePayForUri, { talerPayUri: orderStatusTwo.taler_pay_uri, diff --git a/packages/taler-harness/src/integrationtests/test-payment-abort.ts b/packages/taler-harness/src/integrationtests/test-payment-abort.ts index 05ca7a543..fe7ecbcd7 100644 --- a/packages/taler-harness/src/integrationtests/test-payment-abort.ts +++ b/packages/taler-harness/src/integrationtests/test-payment-abort.ts @@ -17,41 +17,46 @@ /** * Imports. */ -import { GlobalTestState, MerchantPrivateApi, harnessHttpLib } from "../harness/harness.js"; import { - withdrawViaBank, - createFaultInjectedMerchantTestkudosEnvironment, -} from "../harness/helpers.js"; -import { FaultInjectionRequestContext } from "../harness/faultInjection.js"; -import { - codecForMerchantOrderStatusUnpaid, ConfirmPayResultType, - j2s, + MerchantApiClient, PreparePayResultType, TalerErrorCode, TalerErrorDetail, URL, + codecForMerchantOrderStatusUnpaid, + j2s, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { FaultInjectionRequestContext } from "../harness/faultInjection.js"; +import { GlobalTestState, harnessHttpLib } from "../harness/harness.js"; +import { + createFaultInjectedMerchantTestkudosEnvironment, + withdrawViaBankV2, +} from "../harness/helpers.js"; export async function runPaymentAbortTest(t: GlobalTestState) { // Set up test environment - const { wallet, bank, exchange, faultyMerchant, faultyExchange } = + const { walletClient, bank, exchange, faultyMerchant, faultyExchange } = await createFaultInjectedMerchantTestkudosEnvironment(t); // Withdraw digital cash into the wallet. - await withdrawViaBank(t, { - wallet, + const wres = await withdrawViaBankV2(t, { + walletClient, bank, exchange: faultyExchange, amount: "TESTKUDOS:20", }); - const merchant = faultyMerchant; + await wres.withdrawalFinishedCond; - let orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + const merchantClient = new MerchantApiClient( + faultyMerchant.makeInstanceBaseUrl(), + ); + + let orderResp = await merchantClient.createOrder({ order: { summary: "Buy me!", amount: "TESTKUDOS:5", @@ -60,7 +65,7 @@ export async function runPaymentAbortTest(t: GlobalTestState) { }, }); - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + let orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, sessionId: "mysession-one", }); @@ -84,7 +89,7 @@ export async function runPaymentAbortTest(t: GlobalTestState) { console.log(pubUnpaidStatus); - let preparePayResp = await wallet.client.call( + let preparePayResp = await walletClient.call( WalletApiOperation.PreparePayForUri, { talerPayUri: pubUnpaidStatus.taler_pay_uri, @@ -124,7 +129,7 @@ export async function runPaymentAbortTest(t: GlobalTestState) { }, }); - const confirmPayResp = await wallet.client.call( + const confirmPayResp = await walletClient.call( WalletApiOperation.ConfirmPay, { proposalId, @@ -134,19 +139,16 @@ export async function runPaymentAbortTest(t: GlobalTestState) { // Can't have succeeded yet, but network error results in "pending" state. t.assertDeepEqual(confirmPayResp.type, ConfirmPayResultType.Pending); - const txns = await wallet.client.call(WalletApiOperation.GetTransactions, {}); + const txns = await walletClient.call(WalletApiOperation.GetTransactions, {}); console.log(j2s(txns)); - await wallet.client.call(WalletApiOperation.AbortTransaction, { + await walletClient.call(WalletApiOperation.AbortTransaction, { transactionId: txns.transactions[1].transactionId, }); - await wallet.runUntilDone(); + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); - const txns2 = await wallet.client.call( - WalletApiOperation.GetTransactions, - {}, - ); + const txns2 = await walletClient.call(WalletApiOperation.GetTransactions, {}); console.log(j2s(txns2)); const txTypes = txns2.transactions.map((x) => x.type); diff --git a/packages/taler-harness/src/integrationtests/test-payment-claim.ts b/packages/taler-harness/src/integrationtests/test-payment-claim.ts index 3e52cb5dd..b5ed89ec3 100644 --- a/packages/taler-harness/src/integrationtests/test-payment-claim.ts +++ b/packages/taler-harness/src/integrationtests/test-payment-claim.ts @@ -17,13 +17,13 @@ /** * Imports. */ -import { PreparePayResultType, TalerErrorCode } from "@gnu-taler/taler-util"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { - GlobalTestState, - MerchantPrivateApi, - WalletCli, -} from "../harness/harness.js"; + MerchantApiClient, + PreparePayResultType, + TalerErrorCode, +} from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { GlobalTestState, WalletCli } from "../harness/harness.js"; import { createSimpleTestkudosEnvironmentV2, withdrawViaBankV2, @@ -38,6 +38,8 @@ export async function runPaymentClaimTest(t: GlobalTestState) { const { walletClient, bank, exchange, merchant } = await createSimpleTestkudosEnvironmentV2(t); + const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); + const walletTwo = new WalletCli(t, "two"); // Withdraw digital cash into the wallet. @@ -53,7 +55,7 @@ export async function runPaymentClaimTest(t: GlobalTestState) { // Set up order. - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + const orderResp = await merchantClient.createOrder({ order: { summary: "Buy me!", amount: "TESTKUDOS:5", @@ -61,7 +63,7 @@ export async function runPaymentClaimTest(t: GlobalTestState) { }, }); - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + let orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, }); @@ -94,7 +96,7 @@ export async function runPaymentClaimTest(t: GlobalTestState) { // Check if payment was successful. - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, }); diff --git a/packages/taler-harness/src/integrationtests/test-payment-expired.ts b/packages/taler-harness/src/integrationtests/test-payment-expired.ts new file mode 100644 index 000000000..176fc74f7 --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-payment-expired.ts @@ -0,0 +1,131 @@ +/* + 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 { + AbsoluteTime, + ConfirmPayResultType, + Duration, + MerchantApiClient, + MerchantContractTerms, + PreparePayResultType, + j2s, +} from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { GlobalTestState } from "../harness/harness.js"; +import { + applyTimeTravelV2, + createSimpleTestkudosEnvironmentV2, + withdrawViaBankV2, +} from "../harness/helpers.js"; + +/** + * Run a test for the following scenario: + * + * - Wallet claims an order + * - Merchant goes down + * - Wallet tried to pay, but it fails as the merchant is unavailable + * - The order expires + * - The merchant goes back up again + * - Instead of trying to get an abort-refund, the wallet notices that + * the order is expired, puts the transaction into "failed", + * refreshes allocated coins and thus raises the balance again. + */ +export async function runPaymentExpiredTest(t: GlobalTestState) { + // Set up test environment + + const { walletClient, bank, exchange, merchant } = + await createSimpleTestkudosEnvironmentV2(t); + + // Withdraw digital cash into the wallet. + + await withdrawViaBankV2(t, { + walletClient, + bank, + exchange, + amount: "TESTKUDOS:20", + }); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); + + // Order that can only be paid within five minutes. + const order: Partial<MerchantContractTerms> = { + summary: "Buy me!", + amount: "TESTKUDOS:5", + fulfillment_url: "taler://fulfillment-success/thx", + pay_deadline: AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromSpec({ minutes: 5 }), + ), + ), + }; + + const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); + + const orderResp = await merchantClient.createOrder({ + order, + }); + + let orderStatus = await merchantClient.queryPrivateOrderStatus({ + orderId: orderResp.order_id, + }); + + t.assertTrue(orderStatus.order_status === "unpaid"); + + const preparePayResult = await walletClient.call( + WalletApiOperation.PreparePayForUri, + { + talerPayUri: orderStatus.taler_pay_uri, + }, + ); + + t.assertDeepEqual( + preparePayResult.status, + PreparePayResultType.PaymentPossible, + ); + + await applyTimeTravelV2( + Duration.toMilliseconds(Duration.fromSpec({ hours: 1 })), + { walletClient, exchange, merchant }, + ); + + const confirmPayResult = await walletClient.call( + WalletApiOperation.ConfirmPay, + { transactionId: preparePayResult.transactionId }, + ); + console.log("confirm pay result:"); + console.log(j2s(confirmPayResult)); + t.assertDeepEqual(confirmPayResult.type, ConfirmPayResultType.Pending); + await walletClient.call(WalletApiOperation.AbortTransaction, { + transactionId: preparePayResult.transactionId, + }); + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); + + const bal = await walletClient.call(WalletApiOperation.GetBalances, {}); + console.log(bal); + + t.assertAmountEquals(bal.balances[0].available, "TESTKUDOS:18.93"); + + const txns = await walletClient.call(WalletApiOperation.GetTransactions, { + includeRefreshes: true, + }); + console.log(j2s(txns)); +} + +runPaymentExpiredTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-payment-fault.ts b/packages/taler-harness/src/integrationtests/test-payment-fault.ts index c1438a419..e57427fac 100644 --- a/packages/taler-harness/src/integrationtests/test-payment-fault.ts +++ b/packages/taler-harness/src/integrationtests/test-payment-fault.ts @@ -22,27 +22,26 @@ * Imports. */ import { - GlobalTestState, - MerchantService, - ExchangeService, - setupDb, - BankService, - WalletCli, - MerchantPrivateApi, - getPayto, -} from "../harness/harness.js"; + BankAccessApiClient, + CoreApiResponse, + MerchantApiClient, +} from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { defaultCoinConfig } from "../harness/denomStructures.js"; import { FaultInjectedExchangeService, FaultInjectionRequestContext, FaultInjectionResponseContext, } from "../harness/faultInjection.js"; -import { CoreApiResponse } from "@gnu-taler/taler-util"; -import { defaultCoinConfig } from "../harness/denomStructures.js"; import { - WalletApiOperation, - BankApi, - BankAccessApi, -} from "@gnu-taler/taler-wallet-core"; + BankService, + ExchangeService, + GlobalTestState, + MerchantService, + WalletCli, + getPayto, + setupDb, +} from "../harness/harness.js"; /** * Run test for basic, bank-integrated withdrawal. @@ -114,22 +113,25 @@ export async function runPaymentFaultTest(t: GlobalTestState) { await merchant.start(); await merchant.pingUntilAvailable(); - await merchant.addInstance({ + await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", paytoUris: [getPayto("merchant-default")], }); + const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); + console.log("setup done!"); const wallet = new WalletCli(t); // Create withdrawal operation - const user = await BankApi.createRandomBankUser(bank); - const wop = await BankAccessApi.createWithdrawalOperation( - bank, - user, + const bankClient = new BankAccessApiClient(bank.bankAccessApiBaseUrl); + + const user = await bankClient.createRandomBankUser(); + const wop = await bankClient.createWithdrawalOperation( + user.username, "TESTKUDOS:20", ); @@ -151,7 +153,7 @@ export async function runPaymentFaultTest(t: GlobalTestState) { // Confirm it - await BankApi.confirmWithdrawalOperation(bank, user, wop); + await bankClient.confirmWithdrawalOperation(user.username, wop); await wallet.runUntilDone(); @@ -161,7 +163,7 @@ export async function runPaymentFaultTest(t: GlobalTestState) { // Set up order. - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + const orderResp = await merchantClient.createOrder({ order: { summary: "Buy me!", amount: "TESTKUDOS:5", @@ -169,7 +171,7 @@ export async function runPaymentFaultTest(t: GlobalTestState) { }, }); - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + let orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, }); @@ -218,7 +220,7 @@ export async function runPaymentFaultTest(t: GlobalTestState) { // Check if payment was successful. - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, }); diff --git a/packages/taler-harness/src/integrationtests/test-payment-idempotency.ts b/packages/taler-harness/src/integrationtests/test-payment-idempotency.ts index 6373c2393..65fd3a562 100644 --- a/packages/taler-harness/src/integrationtests/test-payment-idempotency.ts +++ b/packages/taler-harness/src/integrationtests/test-payment-idempotency.ts @@ -17,9 +17,9 @@ /** * Imports. */ -import { PreparePayResultType } from "@gnu-taler/taler-util"; +import { MerchantApiClient, PreparePayResultType } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; +import { GlobalTestState } from "../harness/harness.js"; import { createSimpleTestkudosEnvironmentV2, withdrawViaBankV2, @@ -46,9 +46,11 @@ export async function runPaymentIdempotencyTest(t: GlobalTestState) { await wres.withdrawalFinishedCond; + const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); + // Set up order. - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + const orderResp = await merchantClient.createOrder({ order: { summary: "Buy me!", amount: "TESTKUDOS:5", @@ -56,7 +58,7 @@ export async function runPaymentIdempotencyTest(t: GlobalTestState) { }, }); - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + let orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, }); @@ -102,7 +104,7 @@ export async function runPaymentIdempotencyTest(t: GlobalTestState) { // Check if payment was successful. - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, }); diff --git a/packages/taler-harness/src/integrationtests/test-payment-multiple.ts b/packages/taler-harness/src/integrationtests/test-payment-multiple.ts index 25e19ae8e..4ef5e3bff 100644 --- a/packages/taler-harness/src/integrationtests/test-payment-multiple.ts +++ b/packages/taler-harness/src/integrationtests/test-payment-multiple.ts @@ -17,19 +17,21 @@ /** * Imports. */ +import { MerchantApiClient } from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { coin_ct10, coin_u1 } from "../harness/denomStructures.js"; import { - GlobalTestState, - setupDb, BankService, ExchangeService, + GlobalTestState, MerchantService, - WalletCli, - MerchantPrivateApi, getPayto, + setupDb, } from "../harness/harness.js"; -import { withdrawViaBank } from "../harness/helpers.js"; -import { coin_ct10, coin_u1 } from "../harness/denomStructures.js"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { + createWalletDaemonWithClient, + withdrawViaBankV2, +} from "../harness/helpers.js"; async function setupTest(t: GlobalTestState): Promise<{ merchant: MerchantService; @@ -82,13 +84,13 @@ async function setupTest(t: GlobalTestState): Promise<{ await merchant.start(); await merchant.pingUntilAvailable(); - await merchant.addInstance({ + await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", paytoUris: [getPayto("merchant-default")], }); - await merchant.addInstance({ + await merchant.addInstanceWithWireAccount({ id: "minst1", name: "minst1", paytoUris: [getPayto("minst1")], @@ -113,15 +115,26 @@ export async function runPaymentMultipleTest(t: GlobalTestState) { const { merchant, bank, exchange } = await setupTest(t); - const wallet = new WalletCli(t); + const { walletClient } = await createWalletDaemonWithClient(t, { + name: "default", + }); + + const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); // Withdraw digital cash into the wallet. - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:100" }); + const wres = await withdrawViaBankV2(t, { + walletClient, + bank, + exchange, + amount: "TESTKUDOS:100", + }); + + await wres.withdrawalFinishedCond; // Set up order. - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + const orderResp = await merchantClient.createOrder({ order: { summary: "Buy me!", amount: "TESTKUDOS:80", @@ -129,7 +142,7 @@ export async function runPaymentMultipleTest(t: GlobalTestState) { }, }); - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + let orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, }); @@ -137,18 +150,17 @@ export async function runPaymentMultipleTest(t: GlobalTestState) { // Make wallet pay for the order - const r1 = await wallet.client.call(WalletApiOperation.PreparePayForUri, { + const r1 = await walletClient.call(WalletApiOperation.PreparePayForUri, { talerPayUri: orderStatus.taler_pay_uri, }); - await wallet.client.call(WalletApiOperation.ConfirmPay, { - // FIXME: should be validated, don't cast! - proposalId: r1.proposalId, + await walletClient.call(WalletApiOperation.ConfirmPay, { + transactionId: r1.transactionId, }); // Check if payment was successful. - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, }); diff --git a/packages/taler-harness/src/integrationtests/test-payment-share.ts b/packages/taler-harness/src/integrationtests/test-payment-share.ts index 156571372..c4a82c917 100644 --- a/packages/taler-harness/src/integrationtests/test-payment-share.ts +++ b/packages/taler-harness/src/integrationtests/test-payment-share.ts @@ -19,10 +19,11 @@ */ import { ConfirmPayResultType, + MerchantApiClient, PreparePayResultType, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; +import { GlobalTestState } from "../harness/harness.js"; import { createSimpleTestkudosEnvironmentV2, createWalletDaemonWithClient, @@ -41,6 +42,8 @@ export async function runPaymentShareTest(t: GlobalTestState) { merchant, } = await createSimpleTestkudosEnvironmentV2(t); + const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); + // Withdraw digital cash into the wallet. await withdrawViaBankV2(t, { walletClient: firstWallet, @@ -62,7 +65,7 @@ export async function runPaymentShareTest(t: GlobalTestState) { }); await secondWallet.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); - //create two orders to pay + // create two orders to pay async function createOrder(amount: string) { const order = { summary: "Buy me!", @@ -70,26 +73,16 @@ export async function runPaymentShareTest(t: GlobalTestState) { fulfillment_url: "taler://fulfillment-success/thx", }; - const instance = "default"; const args = { order }; const auth = {}; - const orderResp = await MerchantPrivateApi.createOrder( - merchant, - instance, - { - order: args.order, - }, - auth, - ); + const orderResp = await merchantClient.createOrder({ + order: args.order, + }); - const orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus( - merchant, - { - orderId: orderResp.order_id, - }, - auth, - ); + const orderStatus = await merchantClient.queryPrivateOrderStatus({ + orderId: orderResp.order_id, + }); t.assertTrue(orderStatus.order_status === "unpaid"); return { id: orderResp.order_id, uri: orderStatus.taler_pay_uri }; diff --git a/packages/taler-harness/src/integrationtests/test-payment-template.ts b/packages/taler-harness/src/integrationtests/test-payment-template.ts index 707be52e1..e77236a9a 100644 --- a/packages/taler-harness/src/integrationtests/test-payment-template.ts +++ b/packages/taler-harness/src/integrationtests/test-payment-template.ts @@ -17,12 +17,17 @@ /** * Imports. */ -import { ConfirmPayResultType, Duration, PreparePayResultType } from "@gnu-taler/taler-util"; +import { + ConfirmPayResultType, + Duration, + MerchantApiClient, + PreparePayResultType, +} from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; +import { GlobalTestState } from "../harness/harness.js"; import { createSimpleTestkudosEnvironmentV2, - withdrawViaBankV2 + withdrawViaBankV2, } from "../harness/helpers.js"; /** @@ -34,7 +39,9 @@ export async function runPaymentTemplateTest(t: GlobalTestState) { const { walletClient, bank, exchange, merchant } = await createSimpleTestkudosEnvironmentV2(t); - await MerchantPrivateApi.createTemplate(merchant, "default", { + const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); + + await merchantClient.createTemplate({ template_id: "template1", template_description: "my test template", template_contract: { @@ -50,7 +57,12 @@ export async function runPaymentTemplateTest(t: GlobalTestState) { // Withdraw digital cash into the wallet. - const wres = await withdrawViaBankV2(t, { walletClient, bank, exchange, amount: "TESTKUDOS:20" }); + const wres = await withdrawViaBankV2(t, { + walletClient, + bank, + exchange, + amount: "TESTKUDOS:20", + }); await wres.withdrawalFinishedCond; // Request a template payment @@ -79,13 +91,10 @@ export async function runPaymentTemplateTest(t: GlobalTestState) { // Check if payment was successful. - const orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus( - merchant, - { - orderId: preparePayResult.contractTerms.order_id, - instance: "default", - }, - ); + const orderStatus = await merchantClient.queryPrivateOrderStatus({ + orderId: preparePayResult.contractTerms.order_id, + instance: "default", + }); t.assertTrue(orderStatus.order_status === "paid"); await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); diff --git a/packages/taler-harness/src/integrationtests/test-payment-transient.ts b/packages/taler-harness/src/integrationtests/test-payment-transient.ts index c2a8e37c5..1911b5e92 100644 --- a/packages/taler-harness/src/integrationtests/test-payment-transient.ts +++ b/packages/taler-harness/src/integrationtests/test-payment-transient.ts @@ -19,6 +19,7 @@ */ import { ConfirmPayResultType, + MerchantApiClient, PreparePayResultType, TalerErrorCode, TalerErrorDetail, @@ -27,13 +28,12 @@ import { } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { FaultInjectionResponseContext } from "../harness/faultInjection.js"; -import { GlobalTestState, MerchantPrivateApi, harnessHttpLib } from "../harness/harness.js"; +import { GlobalTestState, harnessHttpLib } from "../harness/harness.js"; import { createFaultInjectedMerchantTestkudosEnvironment, - withdrawViaBank, + withdrawViaBankV2, } from "../harness/helpers.js"; - /** * Run test for a payment where the merchant has a transient * failure in /pay @@ -41,21 +41,25 @@ import { export async function runPaymentTransientTest(t: GlobalTestState) { // Set up test environment - const { wallet, bank, exchange, faultyMerchant, faultyExchange } = + const { walletClient, bank, faultyMerchant, faultyExchange } = await createFaultInjectedMerchantTestkudosEnvironment(t); + const merchantClient = new MerchantApiClient( + faultyMerchant.makeInstanceBaseUrl(), + ); + // Withdraw digital cash into the wallet. - await withdrawViaBank(t, { - wallet, + const wres = await withdrawViaBankV2(t, { + walletClient, bank, exchange: faultyExchange, amount: "TESTKUDOS:20", }); - const merchant = faultyMerchant; + await wres.withdrawalFinishedCond; - let orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + let orderResp = await merchantClient.createOrder({ order: { summary: "Buy me!", amount: "TESTKUDOS:5", @@ -64,7 +68,7 @@ export async function runPaymentTransientTest(t: GlobalTestState) { }, }); - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + let orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, sessionId: "mysession-one", }); @@ -88,7 +92,7 @@ export async function runPaymentTransientTest(t: GlobalTestState) { console.log(pubUnpaidStatus); - let preparePayResp = await wallet.client.call( + let preparePayResp = await walletClient.call( WalletApiOperation.PreparePayForUri, { talerPayUri: pubUnpaidStatus.taler_pay_uri, @@ -136,7 +140,7 @@ export async function runPaymentTransientTest(t: GlobalTestState) { }, }); - const confirmPayResp = await wallet.client.call( + const confirmPayResp = await walletClient.call( WalletApiOperation.ConfirmPay, { proposalId, @@ -148,7 +152,7 @@ export async function runPaymentTransientTest(t: GlobalTestState) { t.assertTrue(confirmPayResp.type === ConfirmPayResultType.Pending); t.assertTrue(faultInjected); - const confirmPayRespTwo = await wallet.client.call( + const confirmPayRespTwo = await walletClient.call( WalletApiOperation.ConfirmPay, { proposalId, diff --git a/packages/taler-harness/src/integrationtests/test-paywall-flow.ts b/packages/taler-harness/src/integrationtests/test-paywall-flow.ts index 5f63d4fac..247ec9cad 100644 --- a/packages/taler-harness/src/integrationtests/test-paywall-flow.ts +++ b/packages/taler-harness/src/integrationtests/test-paywall-flow.ts @@ -17,14 +17,15 @@ /** * Imports. */ -import { GlobalTestState, MerchantPrivateApi, harnessHttpLib } from "../harness/harness.js"; import { - PreparePayResultType, - codecForMerchantOrderStatusUnpaid, ConfirmPayResultType, + MerchantApiClient, + PreparePayResultType, URL, + codecForMerchantOrderStatusUnpaid, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { GlobalTestState, harnessHttpLib } from "../harness/harness.js"; import { createSimpleTestkudosEnvironmentV2, withdrawViaBankV2, @@ -39,6 +40,8 @@ export async function runPaywallFlowTest(t: GlobalTestState) { const { walletClient, bank, exchange, merchant } = await createSimpleTestkudosEnvironmentV2(t); + const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); + // Withdraw digital cash into the wallet. const wres = await withdrawViaBankV2(t, { @@ -59,7 +62,7 @@ export async function runPaywallFlowTest(t: GlobalTestState) { * ========================================================================= */ - let orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + let orderResp = await merchantClient.createOrder({ order: { summary: "Buy me!", amount: "TESTKUDOS:5", @@ -70,7 +73,7 @@ export async function runPaywallFlowTest(t: GlobalTestState) { const firstOrderId = orderResp.order_id; - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + let orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, sessionId: "mysession-one", }); @@ -82,7 +85,9 @@ export async function runPaywallFlowTest(t: GlobalTestState) { t.assertTrue(orderStatus.already_paid_order_id === undefined); let publicOrderStatusUrl = new URL(orderStatus.order_status_url); - let publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl.href); + let publicOrderStatusResp = await harnessHttpLib.fetch( + publicOrderStatusUrl.href, + ); if (publicOrderStatusResp.status != 402) { throw Error( @@ -142,7 +147,7 @@ export async function runPaywallFlowTest(t: GlobalTestState) { * ========================================================================= */ - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, sessionId: "mysession-two", }); @@ -169,7 +174,7 @@ export async function runPaywallFlowTest(t: GlobalTestState) { * ========================================================================= */ - orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + orderResp = await merchantClient.createOrder({ order: { summary: "Buy me!", amount: "TESTKUDOS:5", @@ -181,7 +186,7 @@ export async function runPaywallFlowTest(t: GlobalTestState) { const secondOrderId = orderResp.order_id; - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: secondOrderId, sessionId: "mysession-three", }); @@ -206,7 +211,7 @@ export async function runPaywallFlowTest(t: GlobalTestState) { // The first order should now be paid under "mysession-three", // as the wallet did re-purchase detection - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: firstOrderId, sessionId: "mysession-three", }); @@ -215,7 +220,7 @@ export async function runPaywallFlowTest(t: GlobalTestState) { // Check that with a completely new session ID, the status would NOT // be paid. - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: firstOrderId, sessionId: "mysession-four", }); diff --git a/packages/taler-harness/src/integrationtests/test-peer-repair.ts b/packages/taler-harness/src/integrationtests/test-peer-repair.ts index b09bff2dc..d457ce1e5 100644 --- a/packages/taler-harness/src/integrationtests/test-peer-repair.ts +++ b/packages/taler-harness/src/integrationtests/test-peer-repair.ts @@ -130,7 +130,7 @@ export async function runPeerRepairTest(t: GlobalTestState) { ); await wallet2.client.call(WalletApiOperation.ConfirmPeerPushCredit, { - peerPushPaymentIncomingId: resp2.peerPushPaymentIncomingId, + peerPushCreditId: resp2.peerPushCreditId, }); await peerPushCreditDone1Cond; diff --git a/packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts b/packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts index 5b55b1de1..25c000808 100644 --- a/packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts +++ b/packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts @@ -123,7 +123,7 @@ export async function runPeerToPeerPullTest(t: GlobalTestState) { ); await wallet2.client.call(WalletApiOperation.ConfirmPeerPullDebit, { - peerPullPaymentIncomingId: checkResp.peerPullPaymentIncomingId, + peerPullDebitId: checkResp.peerPullDebitId, }); await peerPullCreditDoneCond; diff --git a/packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts b/packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts index 26f70a5cc..018fa2020 100644 --- a/packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts +++ b/packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts @@ -126,7 +126,7 @@ export async function runPeerToPeerPushTest(t: GlobalTestState) { const acceptResp = await w2.walletClient.call( WalletApiOperation.ConfirmPeerPushCredit, { - peerPushPaymentIncomingId: checkResp.peerPushPaymentIncomingId, + peerPushCreditId: checkResp.peerPushCreditId, }, ); diff --git a/packages/taler-harness/src/integrationtests/test-refund-auto.ts b/packages/taler-harness/src/integrationtests/test-refund-auto.ts index 5648835d5..e8bfecefa 100644 --- a/packages/taler-harness/src/integrationtests/test-refund-auto.ts +++ b/packages/taler-harness/src/integrationtests/test-refund-auto.ts @@ -17,9 +17,13 @@ /** * Imports. */ -import { Duration, durationFromSpec } from "@gnu-taler/taler-util"; +import { + Duration, + MerchantApiClient, + durationFromSpec, +} from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; +import { GlobalTestState } from "../harness/harness.js"; import { createSimpleTestkudosEnvironmentV2, withdrawViaBankV2, @@ -34,6 +38,8 @@ export async function runRefundAutoTest(t: GlobalTestState) { const { walletClient, bank, exchange, merchant } = await createSimpleTestkudosEnvironmentV2(t); + const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); + // Withdraw digital cash into the wallet. const wres = await withdrawViaBankV2(t, { @@ -46,7 +52,7 @@ export async function runRefundAutoTest(t: GlobalTestState) { await wres.withdrawalFinishedCond; // Set up order. - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + const orderResp = await merchantClient.createOrder({ order: { summary: "Buy me!", amount: "TESTKUDOS:5", @@ -60,7 +66,7 @@ export async function runRefundAutoTest(t: GlobalTestState) { ), }); - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + let orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, }); @@ -78,13 +84,13 @@ export async function runRefundAutoTest(t: GlobalTestState) { // Check if payment was successful. - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, }); t.assertTrue(orderStatus.order_status === "paid"); - const ref = await MerchantPrivateApi.giveRefund(merchant, { + const ref = await merchantClient.giveRefund({ amount: "TESTKUDOS:5", instance: "default", justification: "foo", diff --git a/packages/taler-harness/src/integrationtests/test-refund-gone.ts b/packages/taler-harness/src/integrationtests/test-refund-gone.ts index 5f226c7dd..d50919934 100644 --- a/packages/taler-harness/src/integrationtests/test-refund-gone.ts +++ b/packages/taler-harness/src/integrationtests/test-refund-gone.ts @@ -17,18 +17,19 @@ /** * Imports. */ -import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; -import { - createSimpleTestkudosEnvironment, - withdrawViaBank, - applyTimeTravel, -} from "../harness/helpers.js"; import { AbsoluteTime, Duration, + MerchantApiClient, durationFromSpec, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { GlobalTestState } from "../harness/harness.js"; +import { + applyTimeTravelV2, + createSimpleTestkudosEnvironmentV2, + withdrawViaBankV2, +} from "../harness/helpers.js"; /** * Run test for basic, bank-integrated withdrawal. @@ -36,16 +37,25 @@ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; export async function runRefundGoneTest(t: GlobalTestState) { // Set up test environment - const { wallet, bank, exchange, merchant } = - await createSimpleTestkudosEnvironment(t); + const { walletClient, bank, exchange, merchant } = + await createSimpleTestkudosEnvironmentV2(t); + + const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); // Withdraw digital cash into the wallet. - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); + const wres = await withdrawViaBankV2(t, { + walletClient, + bank, + exchange, + amount: "TESTKUDOS:20", + }); + + await wres.withdrawalFinishedCond; // Set up order. - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + const orderResp = await merchantClient.createOrder({ order: { summary: "Buy me!", amount: "TESTKUDOS:5", @@ -64,7 +74,7 @@ export async function runRefundGoneTest(t: GlobalTestState) { ), }); - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + let orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, }); @@ -72,17 +82,17 @@ export async function runRefundGoneTest(t: GlobalTestState) { // Make wallet pay for the order - const r1 = await wallet.client.call(WalletApiOperation.PreparePayForUri, { + const r1 = await walletClient.call(WalletApiOperation.PreparePayForUri, { talerPayUri: orderStatus.taler_pay_uri, }); - const r2 = await wallet.client.call(WalletApiOperation.ConfirmPay, { - proposalId: r1.proposalId, + const r2 = await walletClient.call(WalletApiOperation.ConfirmPay, { + transactionId: r1.transactionId, }); // Check if payment was successful. - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, }); @@ -90,11 +100,14 @@ export async function runRefundGoneTest(t: GlobalTestState) { console.log(orderStatus); - await applyTimeTravel(durationFromSpec({ hours: 1 }), { exchange, wallet }); + await applyTimeTravelV2( + Duration.toMilliseconds(Duration.fromSpec({ hours: 1 })), + { exchange, walletClient: walletClient }, + ); await exchange.runAggregatorOnce(); - const ref = await MerchantPrivateApi.giveRefund(merchant, { + const ref = await merchantClient.giveRefund({ amount: "TESTKUDOS:5", instance: "default", justification: "foo", @@ -103,16 +116,16 @@ export async function runRefundGoneTest(t: GlobalTestState) { console.log(ref); - await wallet.client.call(WalletApiOperation.StartRefundQuery, { + await walletClient.call(WalletApiOperation.StartRefundQuery, { transactionId: r1.transactionId, }); - await wallet.runUntilDone(); + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); - let r = await wallet.client.call(WalletApiOperation.GetBalances, {}); + let r = await walletClient.call(WalletApiOperation.GetBalances, {}); console.log(JSON.stringify(r, undefined, 2)); - const r3 = await wallet.client.call(WalletApiOperation.GetTransactions, {}); + const r3 = await walletClient.call(WalletApiOperation.GetTransactions, {}); console.log(JSON.stringify(r3, undefined, 2)); await t.shutdown(); diff --git a/packages/taler-harness/src/integrationtests/test-refund-incremental.ts b/packages/taler-harness/src/integrationtests/test-refund-incremental.ts index 8ac0948f2..e7e041ce6 100644 --- a/packages/taler-harness/src/integrationtests/test-refund-incremental.ts +++ b/packages/taler-harness/src/integrationtests/test-refund-incremental.ts @@ -20,15 +20,12 @@ import { Amounts, Duration, + MerchantApiClient, TransactionType, durationFromSpec, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { - GlobalTestState, - MerchantPrivateApi, - delayMs, -} from "../harness/harness.js"; +import { GlobalTestState, delayMs } from "../harness/harness.js"; import { createSimpleTestkudosEnvironmentV2, withdrawViaBankV2, @@ -43,6 +40,8 @@ export async function runRefundIncrementalTest(t: GlobalTestState) { const { walletClient, bank, exchange, merchant } = await createSimpleTestkudosEnvironmentV2(t); + const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); + // Withdraw digital cash into the wallet. const wres = await withdrawViaBankV2(t, { @@ -56,7 +55,7 @@ export async function runRefundIncrementalTest(t: GlobalTestState) { // Set up order. - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + const orderResp = await merchantClient.createOrder({ order: { summary: "Buy me!", amount: "TESTKUDOS:10", @@ -67,7 +66,7 @@ export async function runRefundIncrementalTest(t: GlobalTestState) { ), }); - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + let orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, }); @@ -85,13 +84,13 @@ export async function runRefundIncrementalTest(t: GlobalTestState) { // Check if payment was successful. - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, }); t.assertTrue(orderStatus.order_status === "paid"); - let ref = await MerchantPrivateApi.giveRefund(merchant, { + let ref = await merchantClient.giveRefund({ amount: "TESTKUDOS:2.5", instance: "default", justification: "foo", @@ -120,7 +119,7 @@ export async function runRefundIncrementalTest(t: GlobalTestState) { // refund will be grouped with the previous one. await delayMs(1200); - ref = await MerchantPrivateApi.giveRefund(merchant, { + ref = await merchantClient.giveRefund({ amount: "TESTKUDOS:5", instance: "default", justification: "bar", @@ -133,7 +132,7 @@ export async function runRefundIncrementalTest(t: GlobalTestState) { // refund will be grouped with the previous one. await delayMs(1200); - ref = await MerchantPrivateApi.giveRefund(merchant, { + ref = await merchantClient.giveRefund({ amount: "TESTKUDOS:10", instance: "default", justification: "bar", @@ -153,7 +152,7 @@ export async function runRefundIncrementalTest(t: GlobalTestState) { console.log(wr); } - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, }); diff --git a/packages/taler-harness/src/integrationtests/test-refund.ts b/packages/taler-harness/src/integrationtests/test-refund.ts index 44848d88a..aeeb91f38 100644 --- a/packages/taler-harness/src/integrationtests/test-refund.ts +++ b/packages/taler-harness/src/integrationtests/test-refund.ts @@ -20,11 +20,12 @@ import { Duration, durationFromSpec, + MerchantApiClient, NotificationType, TransactionMajorState, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; +import { GlobalTestState } from "../harness/harness.js"; import { createSimpleTestkudosEnvironmentV2, withdrawViaBankV2, @@ -43,6 +44,8 @@ export async function runRefundTest(t: GlobalTestState) { merchant, } = await createSimpleTestkudosEnvironmentV2(t); + const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); + // Withdraw digital cash into the wallet. const withdrawalRes = await withdrawViaBankV2(t, { @@ -56,7 +59,7 @@ export async function runRefundTest(t: GlobalTestState) { // Set up order. - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + const orderResp = await merchantClient.createOrder({ order: { summary: "Buy me!", amount: "TESTKUDOS:5", @@ -67,7 +70,7 @@ export async function runRefundTest(t: GlobalTestState) { ), }); - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + let orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, }); @@ -85,13 +88,13 @@ export async function runRefundTest(t: GlobalTestState) { // Check if payment was successful. - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, }); t.assertTrue(orderStatus.order_status === "paid"); - const ref = await MerchantPrivateApi.giveRefund(merchant, { + const ref = await merchantClient.giveRefund({ amount: "TESTKUDOS:5", instance: "default", justification: "foo", diff --git a/packages/taler-harness/src/integrationtests/test-revocation.ts b/packages/taler-harness/src/integrationtests/test-revocation.ts index 04707e51a..0cb6987ad 100644 --- a/packages/taler-harness/src/integrationtests/test-revocation.ts +++ b/packages/taler-harness/src/integrationtests/test-revocation.ts @@ -28,20 +28,22 @@ import { BankService, delayMs, getPayto, + WalletClient, } from "../harness/harness.js"; import { - withdrawViaBank, - makeTestPayment, - SimpleTestEnvironment, + SimpleTestEnvironmentNg, + createWalletDaemonWithClient, + makeTestPaymentV2, + withdrawViaBankV2, } from "../harness/helpers.js"; async function revokeAllWalletCoins(req: { - wallet: WalletCli; + walletClient: WalletClient; exchange: ExchangeService; merchant: MerchantService; }): Promise<void> { - const { wallet, exchange, merchant } = req; - const coinDump = await wallet.client.call(WalletApiOperation.DumpCoins, {}); + const { walletClient, exchange, merchant } = req; + const coinDump = await walletClient.call(WalletApiOperation.DumpCoins, {}); console.log(coinDump); const usedDenomHashes = new Set<string>(); for (const coin of coinDump.coins) { @@ -60,7 +62,7 @@ async function revokeAllWalletCoins(req: { async function createTestEnvironment( t: GlobalTestState, -): Promise<SimpleTestEnvironment> { +): Promise<SimpleTestEnvironmentNg> { const db = await setupDb(t); const bank = await BankService.create(t, { @@ -120,13 +122,13 @@ async function createTestEnvironment( await merchant.start(); await merchant.pingUntilAvailable(); - await merchant.addInstance({ + await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", paytoUris: [getPayto("merchant-default")], }); - await merchant.addInstance({ + await merchant.addInstanceWithWireAccount({ id: "minst1", name: "minst1", paytoUris: [getPayto("minst1")], @@ -136,11 +138,19 @@ async function createTestEnvironment( const wallet = new WalletCli(t); + const { walletService, walletClient } = await createWalletDaemonWithClient( + t, + { + name: "default", + }, + ); + return { commonDb: db, exchange, merchant, - wallet, + walletClient, + walletService, bank, exchangeBankAccount, }; @@ -152,24 +162,30 @@ async function createTestEnvironment( export async function runRevocationTest(t: GlobalTestState) { // Set up test environment - const { wallet, bank, exchange, merchant } = await createTestEnvironment(t); + const { walletClient, bank, exchange, merchant } = + await createTestEnvironment(t); // Withdraw digital cash into the wallet. - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:15" }); + const wres = await withdrawViaBankV2(t, { + walletClient, + bank, + exchange, + amount: "TESTKUDOS:15", + }); + await wres.withdrawalFinishedCond; console.log("revoking first time"); - await revokeAllWalletCoins({ wallet, exchange, merchant }); + await revokeAllWalletCoins({ walletClient, exchange, merchant }); // FIXME: this shouldn't be necessary once https://bugs.taler.net/n/6565 // is implemented. - await wallet.client.call(WalletApiOperation.AddExchange, { + await walletClient.call(WalletApiOperation.AddExchange, { exchangeBaseUrl: exchange.baseUrl, forceUpdate: true, }); - await wallet.runUntilDone(); - await wallet.runUntilDone(); - const bal = await wallet.client.call(WalletApiOperation.GetBalances, {}); + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); + const bal = await walletClient.call(WalletApiOperation.GetBalances, {}); console.log("wallet balance", bal); const order = { @@ -178,39 +194,43 @@ export async function runRevocationTest(t: GlobalTestState) { fulfillment_url: "taler://fulfillment-success/thx", }; - await makeTestPayment(t, { wallet, merchant, order }); + await makeTestPaymentV2(t, { walletClient, merchant, order }); - wallet.deleteDatabase(); + await walletClient.call(WalletApiOperation.ClearDb, {}); - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:15" }); + await withdrawViaBankV2(t, { + walletClient, + bank, + exchange, + amount: "TESTKUDOS:15", + }); - const coinDump = await wallet.client.call(WalletApiOperation.DumpCoins, {}); + const coinDump = await walletClient.call(WalletApiOperation.DumpCoins, {}); console.log(coinDump); const coinPubList = coinDump.coins.map((x) => x.coin_pub); - await wallet.client.call(WalletApiOperation.ForceRefresh, { + await walletClient.call(WalletApiOperation.ForceRefresh, { coinPubList, }); - await wallet.runUntilDone(); + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); console.log("revoking second time"); - await revokeAllWalletCoins({ wallet, exchange, merchant }); + await revokeAllWalletCoins({ walletClient, exchange, merchant }); // FIXME: this shouldn't be necessary once https://bugs.taler.net/n/6565 // is implemented. - await wallet.client.call(WalletApiOperation.AddExchange, { + await walletClient.call(WalletApiOperation.AddExchange, { exchangeBaseUrl: exchange.baseUrl, forceUpdate: true, }); - await wallet.runUntilDone(); - await wallet.runUntilDone(); + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); { - const bal = await wallet.client.call(WalletApiOperation.GetBalances, {}); + const bal = await walletClient.call(WalletApiOperation.GetBalances, {}); console.log("wallet balance", bal); } - await makeTestPayment(t, { wallet, merchant, order }); + await makeTestPaymentV2(t, { walletClient, merchant, order }); } runRevocationTest.timeoutMs = 120000; runRevocationTest.suites = ["wallet"]; -runRevocationTest.excludeByDefault = true;
\ No newline at end of file +runRevocationTest.experimental = true; diff --git a/packages/taler-harness/src/integrationtests/test-stored-backups.ts b/packages/taler-harness/src/integrationtests/test-stored-backups.ts new file mode 100644 index 000000000..a3a5e6ca3 --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-stored-backups.ts @@ -0,0 +1,112 @@ +/* + This file is part of GNU Taler + (C) 2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * Imports. + */ +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { GlobalTestState } from "../harness/harness.js"; +import { + withdrawViaBankV2, + makeTestPaymentV2, + useSharedTestkudosEnvironment, +} from "../harness/helpers.js"; + +/** + * Test stored backup wallet-core API. + */ +export async function runStoredBackupsTest(t: GlobalTestState) { + // Set up test environment + + const { walletClient, bank, exchange, merchant } = + await useSharedTestkudosEnvironment(t); + + // Withdraw digital cash into the wallet. + + const wres = await withdrawViaBankV2(t, { + walletClient, + bank, + exchange, + amount: "TESTKUDOS:20", + }); + + await wres; + + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); + + const sb1Resp = await walletClient.call( + WalletApiOperation.CreateStoredBackup, + {}, + ); + const sbList = await walletClient.call( + WalletApiOperation.ListStoredBackups, + {}, + ); + t.assertTrue(sbList.storedBackups.length === 1); + t.assertTrue(sbList.storedBackups[0].name === sb1Resp.name); + + const order = { + summary: "Buy me!", + amount: "TESTKUDOS:5", + fulfillment_url: "taler://fulfillment-success/thx", + }; + + await makeTestPaymentV2(t, { walletClient, merchant, order }); + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); + + const txn1 = await walletClient.call(WalletApiOperation.GetTransactions, {}); + t.assertDeepEqual(txn1.transactions.length, 2); + + // Recover from the stored backup now. + + const sb2Resp = await walletClient.call( + WalletApiOperation.CreateStoredBackup, + {}, + ); + + console.log("recovering backup"); + + await walletClient.call(WalletApiOperation.RecoverStoredBackup, { + name: sb1Resp.name, + }); + + console.log("first recovery done"); + + // Recovery went well, now we can delete the backup + // of the old database we stored before importing. + { + const sbl1 = await walletClient.call( + WalletApiOperation.ListStoredBackups, + {}, + ); + t.assertTrue(sbl1.storedBackups.length === 2); + + await walletClient.call(WalletApiOperation.DeleteStoredBackup, { + name: sb1Resp.name, + }); + const sbl2 = await walletClient.call( + WalletApiOperation.ListStoredBackups, + {}, + ); + t.assertTrue(sbl2.storedBackups.length === 1); + } + + const txn2 = await walletClient.call(WalletApiOperation.GetTransactions, {}); + // We only have the withdrawal after restoring + t.assertDeepEqual(txn2.transactions.length, 1); +} + +runStoredBackupsTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts b/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts index 50f9cbcd3..b94f7757c 100644 --- a/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts +++ b/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts @@ -21,6 +21,7 @@ import { ConfirmPayResultType, Duration, durationFromSpec, + MerchantApiClient, PreparePayResultType, } from "@gnu-taler/taler-util"; import { @@ -31,42 +32,16 @@ import { makeNoFeeCoinConfig } from "../harness/denomStructures.js"; import { BankService, ExchangeService, + getPayto, GlobalTestState, - MerchantPrivateApi, MerchantService, setupDb, - WalletCli, - getPayto, } from "../harness/harness.js"; -import { startWithdrawViaBank, withdrawViaBank } from "../harness/helpers.js"; - -async function applyTimeTravel( - timetravelDuration: Duration, - s: { - exchange?: ExchangeService; - merchant?: MerchantService; - wallet?: WalletCli; - }, -): Promise<void> { - if (s.exchange) { - await s.exchange.stop(); - s.exchange.setTimetravel(timetravelDuration); - await s.exchange.start(); - await s.exchange.pingUntilAvailable(); - } - - if (s.merchant) { - await s.merchant.stop(); - s.merchant.setTimetravel(timetravelDuration); - await s.merchant.start(); - await s.merchant.pingUntilAvailable(); - } - - if (s.wallet) { - console.log("setting wallet time travel to", timetravelDuration); - s.wallet.setTimetravel(timetravelDuration); - } -} +import { + applyTimeTravelV2, + createWalletDaemonWithClient, + withdrawViaBankV2, +} from "../harness/helpers.js"; /** * Basic time travel test. @@ -119,13 +94,13 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) { await merchant.start(); await merchant.pingUntilAvailable(); - await merchant.addInstance({ + await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", paytoUris: [getPayto("merchant-default")], }); - await merchant.addInstance({ + await merchant.addInstanceWithWireAccount({ id: "minst1", name: "minst1", paytoUris: [getPayto("minst1")], @@ -133,54 +108,70 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) { console.log("setup done!"); - const wallet = new WalletCli(t); + const { walletClient } = await createWalletDaemonWithClient(t, { + name: "w1", + }); + + const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); // Withdraw digital cash into the wallet. - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:15" }); + const wres = await withdrawViaBankV2(t, { + walletClient, + bank, + exchange, + amount: "TESTKUDOS:15", + }); + await wres.withdrawalFinishedCond; // Travel into the future, the deposit expiration is two years // into the future. console.log("applying first time travel"); - await applyTimeTravel(durationFromSpec({ days: 400 }), { - wallet, - exchange, - merchant, - }); + await applyTimeTravelV2( + Duration.toMilliseconds(durationFromSpec({ days: 400 })), + { + walletClient, + exchange, + merchant, + }, + ); - await wallet.runUntilDone(); + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); let p: PendingOperationsResponse; - p = await wallet.client.call(WalletApiOperation.GetPendingOperations, {}); + p = await walletClient.call(WalletApiOperation.GetPendingOperations, {}); console.log("pending operations after first time travel"); console.log(JSON.stringify(p, undefined, 2)); - await startWithdrawViaBank(t, { - wallet, + await withdrawViaBankV2(t, { + walletClient, bank, exchange, amount: "TESTKUDOS:20", }); - await wallet.runUntilDone(); + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); // Travel into the future, the deposit expiration is two years // into the future. console.log("applying second time travel"); - await applyTimeTravel(durationFromSpec({ years: 2, months: 6 }), { - wallet, - exchange, - merchant, - }); + await applyTimeTravelV2( + Duration.toMilliseconds(durationFromSpec({ years: 2, months: 6 })), + { + walletClient, + exchange, + merchant, + }, + ); // At this point, the original coins should've been refreshed. // It would be too late to refresh them now, as we're past // the two year deposit expiration. - await wallet.runUntilDone(); + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + const orderResp = await merchantClient.createOrder({ order: { fulfillment_url: "http://example.com", summary: "foo", @@ -188,17 +179,14 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) { }, }); - const orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus( - merchant, - { - orderId: orderResp.order_id, - instance: "default", - }, - ); + const orderStatus = await merchantClient.queryPrivateOrderStatus({ + orderId: orderResp.order_id, + instance: "default", + }); t.assertTrue(orderStatus.order_status === "unpaid"); - const r = await wallet.client.call(WalletApiOperation.PreparePayForUri, { + const r = await walletClient.call(WalletApiOperation.PreparePayForUri, { talerPayUri: orderStatus.taler_pay_uri, }); @@ -206,7 +194,7 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) { t.assertTrue(r.status === PreparePayResultType.PaymentPossible); - const cpr = await wallet.client.call(WalletApiOperation.ConfirmPay, { + const cpr = await walletClient.call(WalletApiOperation.ConfirmPay, { proposalId: r.proposalId, }); diff --git a/packages/taler-harness/src/integrationtests/test-timetravel-withdraw.ts b/packages/taler-harness/src/integrationtests/test-timetravel-withdraw.ts index ca3e67647..e594d2d72 100644 --- a/packages/taler-harness/src/integrationtests/test-timetravel-withdraw.ts +++ b/packages/taler-harness/src/integrationtests/test-timetravel-withdraw.ts @@ -17,13 +17,16 @@ /** * Imports. */ -import { Duration, TransactionMajorState, TransactionType } from "@gnu-taler/taler-util"; +import { + Duration, + TransactionMajorState, + TransactionType, +} from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironment, - startWithdrawViaBank, - withdrawViaBank, + createSimpleTestkudosEnvironmentV2, + withdrawViaBankV2, } from "../harness/helpers.js"; /** @@ -32,12 +35,18 @@ import { export async function runTimetravelWithdrawTest(t: GlobalTestState) { // Set up test environment - const { wallet, bank, exchange, merchant } = - await createSimpleTestkudosEnvironment(t); + const { walletClient, bank, exchange, merchant } = + await createSimpleTestkudosEnvironmentV2(t); // Withdraw digital cash into the wallet. - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:15" }); + const wres1 = await withdrawViaBankV2(t, { + walletClient, + bank, + exchange, + amount: "TESTKUDOS:15", + }); + await wres1.withdrawalFinishedCond; // Travel 400 days into the future, // as the deposit expiration is two years @@ -47,21 +56,21 @@ export async function runTimetravelWithdrawTest(t: GlobalTestState) { }; await exchange.stop(); - exchange.setTimetravel(timetravelDuration); + exchange.setTimetravel(Duration.toMilliseconds(timetravelDuration)); await exchange.start(); await exchange.pingUntilAvailable(); await exchange.keyup(); await merchant.stop(); - merchant.setTimetravel(timetravelDuration); + merchant.setTimetravel(Duration.toMilliseconds(timetravelDuration)); await merchant.start(); await merchant.pingUntilAvailable(); console.log("starting withdrawal via bank"); // This should fail, as the wallet didn't time travel yet. - await startWithdrawViaBank(t, { - wallet, + await withdrawViaBankV2(t, { + walletClient, bank, exchange, amount: "TESTKUDOS:20", @@ -71,10 +80,7 @@ export async function runTimetravelWithdrawTest(t: GlobalTestState) { // Check that transactions are correct for the failed withdrawal { - console.log("running until done (should run into maxRetries limit)"); - await wallet.runUntilDone({ maxRetries: 5 }); - console.log("wallet done running"); - const transactions = await wallet.client.call( + const transactions = await walletClient.call( WalletApiOperation.GetTransactions, {}, ); @@ -88,7 +94,9 @@ export async function runTimetravelWithdrawTest(t: GlobalTestState) { // Now we also let the wallet time travel - wallet.setTimetravel(timetravelDuration); + walletClient.call(WalletApiOperation.TestingSetTimetravel, { + offsetMs: Duration.toMilliseconds(timetravelDuration), + }); // This doesn't work yet, see https://bugs.taler.net/n/6585 diff --git a/packages/taler-harness/src/integrationtests/test-tipping.ts b/packages/taler-harness/src/integrationtests/test-tipping.ts index 53d7f08c8..4140311ab 100644 --- a/packages/taler-harness/src/integrationtests/test-tipping.ts +++ b/packages/taler-harness/src/integrationtests/test-tipping.ts @@ -19,17 +19,15 @@ */ import { BankAccessApiClient, - WalletApiOperation, + MerchantApiClient, + TransactionMajorState, WireGatewayApiClient, -} from "@gnu-taler/taler-wallet-core"; +} from "@gnu-taler/taler-util"; import { - GlobalTestState, - MerchantApiClient, - MerchantPrivateApi, - getWireMethodForTest, -} from "../harness/harness.js"; + WalletApiOperation, +} from "@gnu-taler/taler-wallet-core"; +import { GlobalTestState, getWireMethodForTest } from "../harness/harness.js"; import { createSimpleTestkudosEnvironmentV2 } from "../harness/helpers.js"; -import { TransactionMajorState } from "@gnu-taler/taler-util"; /** * Run test for basic, bank-integrated withdrawal. @@ -40,18 +38,12 @@ export async function runTippingTest(t: GlobalTestState) { const { walletClient, bank, exchange, merchant, exchangeBankAccount } = await createSimpleTestkudosEnvironmentV2(t); - const bankAccessApiClient = new BankAccessApiClient({ - allowHttp: true, - baseUrl: bank.bankAccessApiBaseUrl, - }); + const bankAccessApiClient = new BankAccessApiClient( + bank.bankAccessApiBaseUrl, + ); const mbu = await bankAccessApiClient.createRandomBankUser(); - const merchantClient = new MerchantApiClient( - merchant.makeInstanceBaseUrl("default"), - { - method: "external", - }, - ); + const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); const tipReserveResp = await merchantClient.createTippingReserve({ exchange_url: exchange.baseUrl, @@ -66,12 +58,15 @@ export async function runTippingTest(t: GlobalTestState) { exchangeBankAccount.accountPaytoUri, ); - const wireGatewayApiClient = new WireGatewayApiClient({ - wireGatewayApiBaseUrl: exchangeBankAccount.wireGatewayApiBaseUrl, - accountName: exchangeBankAccount.accountName, - accountPassword: exchangeBankAccount.accountPassword, - allowHttp: true, - }); + const wireGatewayApiClient = new WireGatewayApiClient( + exchangeBankAccount.wireGatewayApiBaseUrl, + { + auth: { + username: exchangeBankAccount.accountName, + password: exchangeBankAccount.accountPassword, + }, + }, + ); await wireGatewayApiClient.adminAddIncoming({ amount: "TESTKUDOS:10", @@ -85,7 +80,7 @@ export async function runTippingTest(t: GlobalTestState) { await merchant.start(); await merchant.pingUntilAvailable(); - const r = await MerchantPrivateApi.queryTippingReserves(merchant, "default"); + const r = await merchantClient.queryTippingReserves(); console.log("tipping reserves:", JSON.stringify(r, undefined, 2)); t.assertTrue(r.reserves.length === 1); @@ -94,7 +89,7 @@ export async function runTippingTest(t: GlobalTestState) { r.reserves[0].merchant_initial_amount, ); - const tip = await MerchantPrivateApi.giveTip(merchant, "default", { + const tip = await merchantClient.giveTip({ amount: "TESTKUDOS:5", justification: "why not?", next_url: "https://example.com/after-tip", diff --git a/packages/taler-harness/src/integrationtests/test-wallet-backup-basic.ts b/packages/taler-harness/src/integrationtests/test-wallet-backup-basic.ts index 5321cf5c7..21ac662c2 100644 --- a/packages/taler-harness/src/integrationtests/test-wallet-backup-basic.ts +++ b/packages/taler-harness/src/integrationtests/test-wallet-backup-basic.ts @@ -21,9 +21,8 @@ import { j2s } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState, WalletCli } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironment, createSimpleTestkudosEnvironmentV2, - withdrawViaBank, + createWalletDaemonWithClient, withdrawViaBankV2, } from "../harness/helpers.js"; import { SyncService } from "../harness/sync.js"; @@ -112,40 +111,42 @@ export async function runWalletBackupBasicTest(t: GlobalTestState) { const txs = await walletClient.call(WalletApiOperation.GetTransactions, {}); console.log(`backed up transactions ${j2s(txs)}`); - const wallet2 = new WalletCli(t, "wallet2"); + const { walletClient: walletClient2 } = await createWalletDaemonWithClient(t, { + name: "w2" + }); // Check that the second wallet is a fresh wallet. { - const bal = await wallet2.client.call(WalletApiOperation.GetBalances, {}); + const bal = await walletClient2.call(WalletApiOperation.GetBalances, {}); t.assertTrue(bal.balances.length === 0); } - await wallet2.client.call(WalletApiOperation.ImportBackupRecovery, { + await walletClient2.call(WalletApiOperation.ImportBackupRecovery, { recovery: backupRecovery, }); - await wallet2.client.call(WalletApiOperation.RunBackupCycle, {}); + await walletClient2.call(WalletApiOperation.RunBackupCycle, {}); // Check that now the old balance is available! { - const bal = await wallet2.client.call(WalletApiOperation.GetBalances, {}); + const bal = await walletClient2.call(WalletApiOperation.GetBalances, {}); t.assertTrue(bal.balances.length === 1); console.log(bal); } // Now do some basic checks that the restored wallet is still functional { - const txs = await wallet2.client.call( + const txs = await walletClient2.call( WalletApiOperation.GetTransactions, {}, ); console.log(`restored transactions ${j2s(txs)}`); - const bal1 = await wallet2.client.call(WalletApiOperation.GetBalances, {}); + const bal1 = await walletClient2.call(WalletApiOperation.GetBalances, {}); t.assertAmountEquals(bal1.balances[0].available, "TESTKUDOS:14.1"); - await withdrawViaBank(t, { - wallet: wallet2, + await withdrawViaBankV2(t, { + walletClient: walletClient2, bank, exchange, amount: "TESTKUDOS:10", @@ -153,15 +154,15 @@ export async function runWalletBackupBasicTest(t: GlobalTestState) { await exchange.runWirewatchOnce(); - await wallet2.runUntilDone(); + await walletClient2.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); - const txs2 = await wallet2.client.call( + const txs2 = await walletClient2.call( WalletApiOperation.GetTransactions, {}, ); console.log(`tx after withdraw after restore ${j2s(txs2)}`); - const bal2 = await wallet2.client.call(WalletApiOperation.GetBalances, {}); + const bal2 = await walletClient2.call(WalletApiOperation.GetBalances, {}); t.assertAmountEquals(bal2.balances[0].available, "TESTKUDOS:23.82"); } diff --git a/packages/taler-harness/src/integrationtests/test-wallet-backup-doublespend.ts b/packages/taler-harness/src/integrationtests/test-wallet-backup-doublespend.ts index e3e18d5a4..c761c4fb0 100644 --- a/packages/taler-harness/src/integrationtests/test-wallet-backup-doublespend.ts +++ b/packages/taler-harness/src/integrationtests/test-wallet-backup-doublespend.ts @@ -17,18 +17,14 @@ /** * Imports. */ -import { PreparePayResultType } from "@gnu-taler/taler-util"; +import { MerchantApiClient, PreparePayResultType } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { - GlobalTestState, - MerchantPrivateApi, - WalletCli, -} from "../harness/harness.js"; +import { GlobalTestState } from "../harness/harness.js"; import { createSimpleTestkudosEnvironmentV2, + createWalletDaemonWithClient, makeTestPaymentV2, - withdrawViaBank, - withdrawViaBankV2 + withdrawViaBankV2, } from "../harness/helpers.js"; import { SyncService } from "../harness/sync.js"; @@ -38,6 +34,8 @@ export async function runWalletBackupDoublespendTest(t: GlobalTestState) { const { commonDb, merchant, walletClient, bank, exchange } = await createSimpleTestkudosEnvironmentV2(t); + const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); + const sync = await SyncService.create(t, { currency: "TESTKUDOS", annualFee: "TESTKUDOS:0.5", @@ -58,7 +56,12 @@ export async function runWalletBackupDoublespendTest(t: GlobalTestState) { name: sync.baseUrl, }); - await withdrawViaBankV2(t, { walletClient, bank, exchange, amount: "TESTKUDOS:10" }); + await withdrawViaBankV2(t, { + walletClient, + bank, + exchange, + amount: "TESTKUDOS:10", + }); await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); @@ -71,13 +74,16 @@ export async function runWalletBackupDoublespendTest(t: GlobalTestState) { {}, ); - const wallet2 = new WalletCli(t, "wallet2"); + const { walletClient: walletClientTwo } = await createWalletDaemonWithClient( + t, + { name: "default" }, + ); - await wallet2.client.call(WalletApiOperation.ImportBackupRecovery, { + await walletClientTwo.call(WalletApiOperation.ImportBackupRecovery, { recovery: backupRecovery, }); - await wallet2.client.call(WalletApiOperation.RunBackupCycle, {}); + await walletClientTwo.call(WalletApiOperation.RunBackupCycle, {}); console.log( "wallet1 balance before spend:", @@ -103,7 +109,7 @@ export async function runWalletBackupDoublespendTest(t: GlobalTestState) { { console.log( "wallet2 balance:", - await wallet2.client.call(WalletApiOperation.GetBalances, {}), + await walletClientTwo.call(WalletApiOperation.GetBalances, {}), ); } @@ -112,7 +118,7 @@ export async function runWalletBackupDoublespendTest(t: GlobalTestState) { { const instance = "default"; - const orderResp = await MerchantPrivateApi.createOrder(merchant, instance, { + const orderResp = await merchantClient.createOrder({ order: { amount: "TESTKUDOS:8", summary: "bla", @@ -120,12 +126,9 @@ export async function runWalletBackupDoublespendTest(t: GlobalTestState) { }, }); - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus( - merchant, - { - orderId: orderResp.order_id, - }, - ); + let orderStatus = await merchantClient.queryPrivateOrderStatus({ + orderId: orderResp.order_id, + }); t.assertTrue(orderStatus.order_status === "unpaid"); @@ -134,11 +137,11 @@ export async function runWalletBackupDoublespendTest(t: GlobalTestState) { { console.log( "wallet2 balance before preparePay:", - await wallet2.client.call(WalletApiOperation.GetBalances, {}), + await walletClientTwo.call(WalletApiOperation.GetBalances, {}), ); } - const preparePayResult = await wallet2.client.call( + const preparePayResult = await walletClientTwo.call( WalletApiOperation.PreparePayForUri, { talerPayUri: orderStatus.taler_pay_uri, @@ -150,25 +153,28 @@ export async function runWalletBackupDoublespendTest(t: GlobalTestState) { PreparePayResultType.PaymentPossible, ); - const res = await wallet2.client.call(WalletApiOperation.ConfirmPay, { - proposalId: preparePayResult.proposalId, + const res = await walletClientTwo.call(WalletApiOperation.ConfirmPay, { + transactionId: preparePayResult.transactionId, }); console.log(res); // FIXME: wait for a notification that indicates insufficient funds! - await withdrawViaBank(t, { - wallet: wallet2, + await withdrawViaBankV2(t, { + walletClient: walletClientTwo, bank, exchange, amount: "TESTKUDOS:50", }); - const bal = await wallet2.client.call(WalletApiOperation.GetBalances, {}); + const bal = await walletClientTwo.call(WalletApiOperation.GetBalances, {}); console.log("bal", bal); - await wallet2.runUntilDone(); + await walletClientTwo.call( + WalletApiOperation.TestingWaitTransactionsFinal, + {}, + ); } } diff --git a/packages/taler-harness/src/integrationtests/test-wallet-balance.ts b/packages/taler-harness/src/integrationtests/test-wallet-balance.ts index 15b0fd427..fc2774adf 100644 --- a/packages/taler-harness/src/integrationtests/test-wallet-balance.ts +++ b/packages/taler-harness/src/integrationtests/test-wallet-balance.ts @@ -17,9 +17,15 @@ /** * Imports. */ -import { Amounts, PreparePayResultType } from "@gnu-taler/taler-util"; +import { + Amounts, + Duration, + MerchantApiClient, + MerchantContractTerms, + PreparePayResultType, +} from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; +import { GlobalTestState } from "../harness/harness.js"; import { createSimpleTestkudosEnvironmentV2, withdrawViaBankV2, @@ -37,6 +43,19 @@ export async function runWalletBalanceTest(t: GlobalTestState) { const { merchant, walletClient, exchange, bank } = await createSimpleTestkudosEnvironmentV2(t); + await merchant.addInstanceWithWireAccount({ + id: "myinst", + name: "My Instance", + paytoUris: ["payto://void/foo"], + defaultWireTransferDelay: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 1 }), + ), + }); + + const merchantClient = new MerchantApiClient( + merchant.makeInstanceBaseUrl("myinst"), + ); + // Withdraw digital cash into the wallet. const wres = await withdrawViaBankV2(t, { @@ -48,17 +67,17 @@ export async function runWalletBalanceTest(t: GlobalTestState) { await wres.withdrawalFinishedCond; - const order = { + const order: Partial<MerchantContractTerms> = { summary: "Buy me!", amount: "TESTKUDOS:5", fulfillment_url: "taler://fulfillment-success/thx", }; - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + const orderResp = await merchantClient.createOrder({ order, }); - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + let orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, }); diff --git a/packages/taler-harness/src/integrationtests/test-wallet-notifications.ts b/packages/taler-harness/src/integrationtests/test-wallet-notifications.ts index c70fd51d4..9a0eb77ae 100644 --- a/packages/taler-harness/src/integrationtests/test-wallet-notifications.ts +++ b/packages/taler-harness/src/integrationtests/test-wallet-notifications.ts @@ -18,14 +18,12 @@ * Imports. */ import { + BankAccessApiClient, Duration, NotificationType, TransactionMajorState, } from "@gnu-taler/taler-util"; -import { - BankAccessApiClient, - WalletApiOperation, -} from "@gnu-taler/taler-wallet-core"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; import { ExchangeService, @@ -92,7 +90,7 @@ export async function runWalletNotificationsTest(t: GlobalTestState) { // Fakebank uses x-taler-bank, but merchant is configured to only accept sepa! const label = "mymerchant"; - await merchant.addInstance({ + await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", paytoUris: [ @@ -123,10 +121,9 @@ export async function runWalletNotificationsTest(t: GlobalTestState) { skipDefaults: true, }); - const bankAccessApiClient = new BankAccessApiClient({ - allowHttp: true, - baseUrl: bank.bankAccessApiBaseUrl, - }); + const bankAccessApiClient = new BankAccessApiClient( + bank.bankAccessApiBaseUrl, + ); const user = await bankAccessApiClient.createRandomBankUser(); bankAccessApiClient.setAuth(user); const wop = await bankAccessApiClient.createWithdrawalOperation( diff --git a/packages/taler-harness/src/integrationtests/test-wallettesting.ts b/packages/taler-harness/src/integrationtests/test-wallettesting.ts index a856df79f..4fa870f1c 100644 --- a/packages/taler-harness/src/integrationtests/test-wallettesting.ts +++ b/packages/taler-harness/src/integrationtests/test-wallettesting.ts @@ -91,7 +91,7 @@ export async function createMyEnvironment( await merchant.start(); await merchant.pingUntilAvailable(); - await merchant.addInstance({ + await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", paytoUris: [getPayto("merchant-default")], diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-abort-bank.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-abort-bank.ts index c3069c317..aa5e2b770 100644 --- a/packages/taler-harness/src/integrationtests/test-withdrawal-abort-bank.ts +++ b/packages/taler-harness/src/integrationtests/test-withdrawal-abort-bank.ts @@ -17,13 +17,10 @@ /** * Imports. */ -import { TalerErrorCode } from "@gnu-taler/taler-util"; -import { - WalletApiOperation, - BankAccessApiClient, -} from "@gnu-taler/taler-wallet-core"; +import { BankAccessApiClient, TalerErrorCode } from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState } from "../harness/harness.js"; -import { createSimpleTestkudosEnvironment } from "../harness/helpers.js"; +import { createSimpleTestkudosEnvironmentV2 } from "../harness/helpers.js"; /** * Run test for basic, bank-integrated withdrawal. @@ -31,14 +28,14 @@ import { createSimpleTestkudosEnvironment } from "../harness/helpers.js"; export async function runWithdrawalAbortBankTest(t: GlobalTestState) { // Set up test environment - const { wallet, bank, exchange } = await createSimpleTestkudosEnvironment(t); + const { walletClient, bank, exchange } = + await createSimpleTestkudosEnvironmentV2(t); // Create a withdrawal operation - const bankAccessApiClient = new BankAccessApiClient({ - allowHttp: true, - baseUrl: bank.bankAccessApiBaseUrl, - }); + const bankAccessApiClient = new BankAccessApiClient( + bank.bankAccessApiBaseUrl, + ); const user = await bankAccessApiClient.createRandomBankUser(); bankAccessApiClient.setAuth(user); const wop = await bankAccessApiClient.createWithdrawalOperation( @@ -48,11 +45,11 @@ export async function runWithdrawalAbortBankTest(t: GlobalTestState) { // Hand it to the wallet - await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, { + await walletClient.call(WalletApiOperation.GetWithdrawalDetailsForUri, { talerWithdrawUri: wop.taler_withdraw_uri, }); - await wallet.runPending(); + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); // Abort it @@ -67,13 +64,10 @@ export async function runWithdrawalAbortBankTest(t: GlobalTestState) { // WHY ?! // const e = await t.assertThrowsTalerErrorAsync(async () => { - await wallet.client.call( - WalletApiOperation.AcceptBankIntegratedWithdrawal, - { - exchangeBaseUrl: exchange.baseUrl, - talerWithdrawUri: wop.taler_withdraw_uri, - }, - ); + await walletClient.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, { + exchangeBaseUrl: exchange.baseUrl, + talerWithdrawUri: wop.taler_withdraw_uri, + }); }); t.assertDeepEqual( e.errorDetail.code, diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts index 61687ec02..232b6d7c2 100644 --- a/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts +++ b/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts @@ -17,10 +17,8 @@ /** * Imports. */ -import { GlobalTestState } from "../harness/harness.js"; -import { createSimpleTestkudosEnvironmentV2 } from "../harness/helpers.js"; -import { BankAccessApiClient, WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { + BankAccessApiClient, j2s, NotificationType, TransactionMajorState, @@ -28,6 +26,9 @@ import { TransactionType, WithdrawalType, } from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { GlobalTestState } from "../harness/harness.js"; +import { createSimpleTestkudosEnvironmentV2 } from "../harness/helpers.js"; /** * Run test for basic, bank-integrated withdrawal. @@ -40,10 +41,9 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) { // Create a withdrawal operation - const bankAccessApiClient = new BankAccessApiClient({ - allowHttp: true, - baseUrl: bank.bankAccessApiBaseUrl, - }); + const bankAccessApiClient = new BankAccessApiClient( + bank.bankAccessApiBaseUrl, + ); const user = await bankAccessApiClient.createRandomBankUser(); bankAccessApiClient.setAuth(user); const wop = await bankAccessApiClient.createWithdrawalOperation( diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts index 06355b964..bc2946a18 100644 --- a/packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts +++ b/packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts @@ -17,22 +17,16 @@ /** * Imports. */ +import { BankAccessApiClient, j2s } from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { CoinConfig } from "../harness/denomStructures.js"; import { + BankService, + ExchangeService, GlobalTestState, WalletCli, setupDb, - ExchangeService, - BankService, } from "../harness/harness.js"; -import { - BankAccessApi, - BankAccessApiClient, - BankApi, - WalletApiOperation, -} from "@gnu-taler/taler-wallet-core"; -import { CoinConfig } from "../harness/denomStructures.js"; -import { j2s, URL } from "@gnu-taler/taler-util"; -import { withdrawViaBank } from "../harness/helpers.js"; const coinRsaCommon = { cipher: "RSA" as const, @@ -113,13 +107,15 @@ export async function runWithdrawalFeesTest(t: GlobalTestState) { const amount = "TESTKUDOS:7.5"; - const bankAccessApiClient = new BankAccessApiClient({ - allowHttp: true, - baseUrl: bank.bankAccessApiBaseUrl, - }); + const bankAccessApiClient = new BankAccessApiClient( + bank.bankAccessApiBaseUrl, + ); const user = await bankAccessApiClient.createRandomBankUser(); bankAccessApiClient.setAuth(user); - const wop = await bankAccessApiClient.createWithdrawalOperation(user.username, amount); + const wop = await bankAccessApiClient.createWithdrawalOperation( + user.username, + amount, + ); // Hand it to the wallet diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-huge.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-huge.ts index a9d544ee0..8777b19e2 100644 --- a/packages/taler-harness/src/integrationtests/test-withdrawal-huge.ts +++ b/packages/taler-harness/src/integrationtests/test-withdrawal-huge.ts @@ -114,4 +114,5 @@ export async function runWithdrawalHugeTest(t: GlobalTestState) { } runWithdrawalHugeTest.suites = ["wallet-perf"]; -runWithdrawalHugeTest.excludeByDefault = true; +// FIXME: Should not be "experimental" but "slow" or something similar. +runWithdrawalHugeTest.experimental = true; diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-manual.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-manual.ts index d49235f89..1d98cd46e 100644 --- a/packages/taler-harness/src/integrationtests/test-withdrawal-manual.ts +++ b/packages/taler-harness/src/integrationtests/test-withdrawal-manual.ts @@ -17,12 +17,14 @@ /** * Imports. */ -import { AbsoluteTime, Logger, j2s } from "@gnu-taler/taler-util"; import { + AbsoluteTime, BankAccessApiClient, - WalletApiOperation, + Logger, WireGatewayApiClient, -} from "@gnu-taler/taler-wallet-core"; + j2s, +} from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState } from "../harness/harness.js"; import { createSimpleTestkudosEnvironmentV2 } from "../harness/helpers.js"; @@ -39,10 +41,9 @@ export async function runWithdrawalManualTest(t: GlobalTestState) { // Create a withdrawal operation - const bankAccessApiClient = new BankAccessApiClient({ - baseUrl: bank.bankAccessApiBaseUrl, - allowHttp: true, - }); + const bankAccessApiClient = new BankAccessApiClient( + bank.bankAccessApiBaseUrl, + ); const user = await bankAccessApiClient.createRandomBankUser(); @@ -74,12 +75,15 @@ export async function runWithdrawalManualTest(t: GlobalTestState) { const reservePub: string = wres.reservePub; - const wireGatewayApiClient = new WireGatewayApiClient({ - wireGatewayApiBaseUrl: exchangeBankAccount.wireGatewayApiBaseUrl, - accountName: exchangeBankAccount.accountName, - accountPassword: exchangeBankAccount.accountPassword, - allowHttp: true, - }); + const wireGatewayApiClient = new WireGatewayApiClient( + exchangeBankAccount.wireGatewayApiBaseUrl, + { + auth: { + username: exchangeBankAccount.accountName, + password: exchangeBankAccount.accountPassword, + }, + }, + ); await wireGatewayApiClient.adminAddIncoming({ amount: "TESTKUDOS:10", diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts index 501af98a4..66bd87a59 100644 --- a/packages/taler-harness/src/integrationtests/testrunner.ts +++ b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -14,39 +14,39 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { - CancellationToken, - Logger, - minimatch, - setGlobalLogLevelFromString, -} from "@gnu-taler/taler-util"; +import { CancellationToken, Logger, minimatch } from "@gnu-taler/taler-util"; import * as child_process from "child_process"; +import { spawnSync } from "child_process"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; import url from "url"; import { GlobalTestState, - runCommand, + TestRunResult, runTestWithState, shouldLingerInTest, - TestRunResult, } from "../harness/harness.js"; -import { spawnSync } from "child_process"; +import { getSharedTestDir } from "../harness/helpers.js"; import { runAgeRestrictionsMerchantTest } from "./test-age-restrictions-merchant.js"; +import { runAgeRestrictionsMixedMerchantTest } from "./test-age-restrictions-mixed-merchant.js"; +import { runAgeRestrictionsPeerTest } from "./test-age-restrictions-peer.js"; import { runBankApiTest } from "./test-bank-api.js"; import { runClaimLoopTest } from "./test-claim-loop.js"; import { runClauseSchnorrTest } from "./test-clause-schnorr.js"; import { runDenomUnofferedTest } from "./test-denom-unoffered.js"; import { runDepositTest } from "./test-deposit.js"; +import { runExchangeDepositTest } from "./test-exchange-deposit.js"; import { runExchangeManagementTest } from "./test-exchange-management.js"; +import { runExchangePurseTest } from "./test-exchange-purse.js"; import { runExchangeTimetravelTest } from "./test-exchange-timetravel.js"; import { runFeeRegressionTest } from "./test-fee-regression.js"; import { runForcedSelectionTest } from "./test-forced-selection.js"; +import { runKycTest } from "./test-kyc.js"; import { runLibeufinApiBankaccountTest } from "./test-libeufin-api-bankaccount.js"; import { runLibeufinApiBankconnectionTest } from "./test-libeufin-api-bankconnection.js"; -import { runLibeufinApiFacadeTest } from "./test-libeufin-api-facade.js"; import { runLibeufinApiFacadeBadRequestTest } from "./test-libeufin-api-facade-bad-request.js"; +import { runLibeufinApiFacadeTest } from "./test-libeufin-api-facade.js"; import { runLibeufinApiPermissionsTest } from "./test-libeufin-api-permissions.js"; import { runLibeufinApiSandboxCamtTest } from "./test-libeufin-api-sandbox-camt.js"; import { runLibeufinApiSandboxTransactionsTest } from "./test-libeufin-api-sandbox-transactions.js"; @@ -58,62 +58,58 @@ import { runLibeufinC5xTest } from "./test-libeufin-c5x.js"; import { runLibeufinAnastasisFacadeTest } from "./test-libeufin-facade-anastasis.js"; import { runLibeufinKeyrotationTest } from "./test-libeufin-keyrotation.js"; import { runLibeufinNexusBalanceTest } from "./test-libeufin-nexus-balance.js"; -import { runLibeufinRefundTest } from "./test-libeufin-refund.js"; import { runLibeufinRefundMultipleUsersTest } from "./test-libeufin-refund-multiple-users.js"; +import { runLibeufinRefundTest } from "./test-libeufin-refund.js"; import { runLibeufinSandboxWireTransferCliTest } from "./test-libeufin-sandbox-wire-transfer-cli.js"; import { runLibeufinTutorialTest } from "./test-libeufin-tutorial.js"; import { runMerchantExchangeConfusionTest } from "./test-merchant-exchange-confusion.js"; -import { runMerchantInstancesTest } from "./test-merchant-instances.js"; import { runMerchantInstancesDeleteTest } from "./test-merchant-instances-delete.js"; import { runMerchantInstancesUrlsTest } from "./test-merchant-instances-urls.js"; +import { runMerchantInstancesTest } from "./test-merchant-instances.js"; import { runMerchantLongpollingTest } from "./test-merchant-longpolling.js"; import { runMerchantRefundApiTest } from "./test-merchant-refund-api.js"; import { runMerchantSpecPublicOrdersTest } from "./test-merchant-spec-public-orders.js"; import { runPayPaidTest } from "./test-pay-paid.js"; -import { runPaymentTest } from "./test-payment.js"; +import { runPaymentAbortTest } from "./test-payment-abort.js"; import { runPaymentClaimTest } from "./test-payment-claim.js"; +import { runPaymentExpiredTest } from "./test-payment-expired.js"; import { runPaymentFaultTest } from "./test-payment-fault.js"; import { runPaymentForgettableTest } from "./test-payment-forgettable.js"; import { runPaymentIdempotencyTest } from "./test-payment-idempotency.js"; import { runPaymentMultipleTest } from "./test-payment-multiple.js"; +import { runPaymentShareTest } from "./test-payment-share.js"; +import { runPaymentTemplateTest } from "./test-payment-template.js"; import { runPaymentTransientTest } from "./test-payment-transient.js"; import { runPaymentZeroTest } from "./test-payment-zero.js"; +import { runPaymentTest } from "./test-payment.js"; import { runPaywallFlowTest } from "./test-paywall-flow.js"; +import { runPeerRepairTest } from "./test-peer-repair.js"; import { runPeerToPeerPullTest } from "./test-peer-to-peer-pull.js"; import { runPeerToPeerPushTest } from "./test-peer-to-peer-push.js"; -import { runRefundTest } from "./test-refund.js"; import { runRefundAutoTest } from "./test-refund-auto.js"; import { runRefundGoneTest } from "./test-refund-gone.js"; import { runRefundIncrementalTest } from "./test-refund-incremental.js"; +import { runRefundTest } from "./test-refund.js"; import { runRevocationTest } from "./test-revocation.js"; +import { runSimplePaymentTest } from "./test-simple-payment.js"; +import { runStoredBackupsTest } from "./test-stored-backups.js"; import { runTimetravelAutorefreshTest } from "./test-timetravel-autorefresh.js"; import { runTimetravelWithdrawTest } from "./test-timetravel-withdraw.js"; import { runTippingTest } from "./test-tipping.js"; +import { runTermOfServiceFormatTest } from "./test-tos-format.js"; import { runWalletBackupBasicTest } from "./test-wallet-backup-basic.js"; import { runWalletBackupDoublespendTest } from "./test-wallet-backup-doublespend.js"; +import { runWalletBalanceTest } from "./test-wallet-balance.js"; +import { runWalletCryptoWorkerTest } from "./test-wallet-cryptoworker.js"; import { runWalletDblessTest } from "./test-wallet-dbless.js"; +import { runWalletNotificationsTest } from "./test-wallet-notifications.js"; import { runWallettestingTest } from "./test-wallettesting.js"; import { runWithdrawalAbortBankTest } from "./test-withdrawal-abort-bank.js"; import { runWithdrawalBankIntegratedTest } from "./test-withdrawal-bank-integrated.js"; import { runWithdrawalFakebankTest } from "./test-withdrawal-fakebank.js"; -import { runWithdrawalManualTest } from "./test-withdrawal-manual.js"; -import { runAgeRestrictionsPeerTest } from "./test-age-restrictions-peer.js"; -import { runWalletNotificationsTest } from "./test-wallet-notifications.js"; -import { runAgeRestrictionsMixedMerchantTest } from "./test-age-restrictions-mixed-merchant.js"; -import { runWalletCryptoWorkerTest } from "./test-wallet-cryptoworker.js"; -import { runWithdrawalHugeTest } from "./test-withdrawal-huge.js"; -import { runKycTest } from "./test-kyc.js"; -import { runPaymentAbortTest } from "./test-payment-abort.js"; import { runWithdrawalFeesTest } from "./test-withdrawal-fees.js"; -import { runWalletBalanceTest } from "./test-wallet-balance.js"; -import { runPaymentTemplateTest } from "./test-payment-template.js"; -import { runExchangeDepositTest } from "./test-exchange-deposit.js"; -import { runPeerRepairTest } from "./test-peer-repair.js"; -import { runPaymentShareTest } from "./test-payment-share.js"; -import { runSimplePaymentTest } from "./test-simple-payment.js"; -import { runTermOfServiceFormatTest } from "./test-tos-format.js"; -import { runExchangePurseTest } from "./test-exchange-purse.js"; -import { getSharedTestDir } from "../harness/helpers.js"; +import { runWithdrawalHugeTest } from "./test-withdrawal-huge.js"; +import { runWithdrawalManualTest } from "./test-withdrawal-manual.js"; /** * Test runner. @@ -126,7 +122,6 @@ const logger = new Logger("testrunner.ts"); interface TestMainFunction { (t: GlobalTestState): Promise<void>; timeoutMs?: number; - excludeByDefault?: boolean; experimental?: boolean; suites?: string[]; } @@ -212,6 +207,8 @@ const allTests: TestMainFunction[] = [ runWithdrawalFeesTest, runWithdrawalHugeTest, runTermOfServiceFormatTest, + runStoredBackupsTest, + runPaymentExpiredTest, ]; export interface TestRunSpec { @@ -228,7 +225,6 @@ export interface TestRunSpec { export interface TestInfo { name: string; suites: string[]; - excludeByDefault: boolean; experimental: boolean; } @@ -333,19 +329,16 @@ export async function runTests(spec: TestRunSpec) { continue; } + if (testCase.experimental && !spec.includeExperimental) { + continue; + } + if (suites) { const ts = new Set(testCase.suites ?? []); const intersection = new Set([...suites].filter((x) => ts.has(x))); if (intersection.size === 0) { continue; } - } else { - if (testCase.excludeByDefault) { - continue; - } - if (testCase.experimental && !spec.includeExperimental) { - continue; - } } if (spec.dryRun) { @@ -521,7 +514,6 @@ export function getTestInfo(): TestInfo[] { return allTests.map((x) => ({ name: getTestName(x), suites: x.suites ?? [], - excludeByDefault: x.excludeByDefault ?? false, experimental: x.experimental ?? false, })); } diff --git a/packages/taler-harness/src/lint.ts b/packages/taler-harness/src/lint.ts index 6d8e679db..a45e6db9d 100644 --- a/packages/taler-harness/src/lint.ts +++ b/packages/taler-harness/src/lint.ts @@ -55,7 +55,6 @@ interface PubkeyConf { const httpLib = createPlatformHttpLib({ enableThrottling: false, - allowHttp: true, }); interface ShellResult { diff --git a/packages/taler-harness/tsconfig.json b/packages/taler-harness/tsconfig.json index d022b16e8..ece83a85f 100644 --- a/packages/taler-harness/tsconfig.json +++ b/packages/taler-harness/tsconfig.json @@ -3,10 +3,10 @@ "compilerOptions": { "composite": true, "target": "ES2018", - "module": "ESNext", + "module": "Node16", "moduleResolution": "Node16", "sourceMap": true, - "lib": ["es6"], + "lib": ["ES2020"], "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "strict": true, diff --git a/packages/taler-util/package.json b/packages/taler-util/package.json index 6ac9a2689..0bb98767d 100644 --- a/packages/taler-util/package.json +++ b/packages/taler-util/package.json @@ -68,14 +68,14 @@ "esbuild": "^0.17.7", "prettier": "^2.8.8", "rimraf": "^3.0.2", - "typescript": "^5.1.3" + "typescript": "^5.2.2" }, "dependencies": { "big-integer": "^1.6.51", "fflate": "^0.7.4", + "hash-wasm": "^4.9.0", "jed": "^1.1.1", - "tslib": "^2.5.3", - "hash-wasm": "^4.9.0" + "tslib": "^2.5.3" }, "ava": { "files": [ diff --git a/packages/taler-util/src/MerchantApiClient.ts b/packages/taler-util/src/MerchantApiClient.ts new file mode 100644 index 000000000..cbdcb9fdf --- /dev/null +++ b/packages/taler-util/src/MerchantApiClient.ts @@ -0,0 +1,331 @@ +/* + This file is part of GNU Taler + (C) 2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { + createPlatformHttpLib, + expectSuccessResponseOrThrow, + readSuccessResponseJsonOrThrow, +} from "./http.js"; +import { FacadeCredentials } from "./libeufin-api-types.js"; +import { Logger } from "./logging.js"; +import { + MerchantReserveCreateConfirmation, + codecForMerchantReserveCreateConfirmation, + TippingReserveStatus, + MerchantInstancesResponse, + MerchantPostOrderRequest, + MerchantPostOrderResponse, + codecForMerchantPostOrderResponse, + MerchantOrderPrivateStatusResponse, + codecForMerchantOrderPrivateStatusResponse, + RewardCreateRequest, + RewardCreateConfirmation, + MerchantTemplateAddDetails, +} from "./merchant-api-types.js"; +import { AmountString } from "./taler-types.js"; +import { TalerProtocolDuration } from "./time.js"; + +const logger = new Logger("MerchantApiClient.ts"); + +export interface MerchantAuthConfiguration { + method: "external" | "token"; + token?: string; +} + +// FIXME: Why do we need this? Describe / fix! +export interface PartialMerchantInstanceConfig { + auth?: MerchantAuthConfiguration; + id: string; + name: string; + paytoUris: string[]; + address?: unknown; + jurisdiction?: unknown; + defaultWireTransferDelay?: TalerProtocolDuration; + defaultPayDelay?: TalerProtocolDuration; +} + +export interface CreateMerchantTippingReserveRequest { + // Amount that the merchant promises to put into the reserve + initial_balance: AmountString; + + // Exchange the merchant intends to use for tipping + exchange_url: string; + + // Desired wire method, for example "iban" or "x-taler-bank" + wire_method: string; +} + +export interface DeleteTippingReserveArgs { + reservePub: string; + purge?: boolean; +} + +export interface MerchantInstanceConfig { + accounts: MerchantBankAccount[]; + auth: MerchantAuthConfiguration; + id: string; + name: string; + address: unknown; + jurisdiction: unknown; + use_stefan: boolean; + default_wire_transfer_delay: TalerProtocolDuration; + default_pay_delay: TalerProtocolDuration; +} + +interface MerchantBankAccount { + // The payto:// URI where the wallet will send coins. + payto_uri: string; + + // Optional base URL for a facade where the + // merchant backend can see incoming wire + // transfers to reconcile its accounting + // with that of the exchange. Used by + // taler-merchant-wirewatch. + credit_facade_url?: string; + + // Credentials for accessing the credit facade. + credit_facade_credentials?: FacadeCredentials; +} + +export interface MerchantInstanceConfig { + accounts: MerchantBankAccount[]; + auth: MerchantAuthConfiguration; + id: string; + name: string; + address: unknown; + jurisdiction: unknown; + use_stefan: boolean; + default_wire_transfer_delay: TalerProtocolDuration; + default_pay_delay: TalerProtocolDuration; +} + +export interface PrivateOrderStatusQuery { + instance?: string; + orderId: string; + sessionId?: string; +} + +/** + * Client for the GNU Taler merchant backend. + */ +export class MerchantApiClient { + /** + * Base URL for the particular instance that this merchant API client + * is for. + */ + private baseUrl: string; + + readonly auth: MerchantAuthConfiguration; + + constructor(baseUrl: string, auth?: MerchantAuthConfiguration) { + this.baseUrl = baseUrl; + + this.auth = auth ?? { + method: "external", + }; + } + + httpClient = createPlatformHttpLib(); + + async changeAuth(auth: MerchantAuthConfiguration): Promise<void> { + const url = new URL("private/auth", this.baseUrl); + const res = await this.httpClient.fetch(url.href, { + method: "POST", + body: auth, + headers: this.makeAuthHeader(), + }); + await expectSuccessResponseOrThrow(res); + } + + async deleteTippingReserve(req: DeleteTippingReserveArgs): Promise<void> { + const url = new URL(`private/reserves/${req.reservePub}`, this.baseUrl); + if (req.purge) { + url.searchParams.set("purge", "YES"); + } + const resp = await this.httpClient.fetch(url.href, { + method: "DELETE", + headers: this.makeAuthHeader(), + }); + logger.info(`delete status: ${resp.status}`); + return; + } + + async createTippingReserve( + req: CreateMerchantTippingReserveRequest, + ): Promise<MerchantReserveCreateConfirmation> { + const url = new URL("private/reserves", this.baseUrl); + const resp = await this.httpClient.fetch(url.href, { + method: "POST", + body: req, + headers: this.makeAuthHeader(), + }); + const respData = readSuccessResponseJsonOrThrow( + resp, + codecForMerchantReserveCreateConfirmation(), + ); + return respData; + } + + async getPrivateInstanceInfo(): Promise<any> { + const url = new URL("private", this.baseUrl); + const resp = await this.httpClient.fetch(url.href, { + method: "GET", + headers: this.makeAuthHeader(), + }); + return await resp.json(); + } + + async getPrivateTipReserves(): Promise<TippingReserveStatus> { + const url = new URL("private/reserves", this.baseUrl); + const resp = await this.httpClient.fetch(url.href, { + method: "GET", + headers: this.makeAuthHeader(), + }); + // FIXME: Validate! + return await resp.json(); + } + + async deleteInstance(instanceId: string) { + const url = new URL(`management/instances/${instanceId}`, this.baseUrl); + const resp = await this.httpClient.fetch(url.href, { + method: "DELETE", + headers: this.makeAuthHeader(), + }); + await expectSuccessResponseOrThrow(resp); + } + + async createInstance(req: MerchantInstanceConfig): Promise<void> { + const url = new URL("management/instances", this.baseUrl); + await this.httpClient.fetch(url.href, { + method: "POST", + body: req, + headers: this.makeAuthHeader(), + }); + } + + async getInstances(): Promise<MerchantInstancesResponse> { + const url = new URL("management/instances", this.baseUrl); + const resp = await this.httpClient.fetch(url.href, { + headers: this.makeAuthHeader(), + }); + return resp.json(); + } + + async getInstanceFullDetails(instanceId: string): Promise<any> { + const url = new URL(`management/instances/${instanceId}`, this.baseUrl); + try { + const resp = await this.httpClient.fetch(url.href, { + headers: this.makeAuthHeader(), + }); + return resp.json(); + } catch (e) { + throw e; + } + } + + async createOrder( + req: MerchantPostOrderRequest, + ): Promise<MerchantPostOrderResponse> { + let url = new URL("private/orders", this.baseUrl); + const resp = await this.httpClient.fetch(url.href, { + method: "POST", + body: req, + headers: this.makeAuthHeader(), + }); + return readSuccessResponseJsonOrThrow( + resp, + codecForMerchantPostOrderResponse(), + ); + } + + async queryPrivateOrderStatus( + query: PrivateOrderStatusQuery, + ): Promise<MerchantOrderPrivateStatusResponse> { + const reqUrl = new URL(`private/orders/${query.orderId}`, this.baseUrl); + if (query.sessionId) { + reqUrl.searchParams.set("session_id", query.sessionId); + } + const resp = await this.httpClient.fetch(reqUrl.href, { + headers: this.makeAuthHeader(), + }); + return readSuccessResponseJsonOrThrow( + resp, + codecForMerchantOrderPrivateStatusResponse(), + ); + } + + async giveTip(req: RewardCreateRequest): Promise<RewardCreateConfirmation> { + const reqUrl = new URL(`private/tips`, this.baseUrl); + const resp = await this.httpClient.fetch(reqUrl.href, { + method: "POST", + body: req, + }); + // FIXME: validate + return resp.json(); + } + + async queryTippingReserves(): Promise<TippingReserveStatus> { + const reqUrl = new URL(`private/reserves`, this.baseUrl); + const resp = await this.httpClient.fetch(reqUrl.href, { + headers: this.makeAuthHeader(), + }); + // FIXME: validate + return resp.json(); + } + + async giveRefund(r: { + instance: string; + orderId: string; + amount: string; + justification: string; + }): Promise<{ talerRefundUri: string }> { + const reqUrl = new URL(`private/orders/${r.orderId}/refund`, this.baseUrl); + const resp = await this.httpClient.fetch(reqUrl.href, { + method: "POST", + body: { + refund: r.amount, + reason: r.justification, + }, + }); + const respBody = await resp.json(); + return { + talerRefundUri: respBody.taler_refund_uri, + }; + } + + async createTemplate(req: MerchantTemplateAddDetails) { + let url = new URL("private/templates", this.baseUrl); + const resp = await this.httpClient.fetch(url.href, { + method: "POST", + body: req, + headers: this.makeAuthHeader(), + }); + if (resp.status !== 204) { + throw Error("failed to create template"); + } + } + + private makeAuthHeader(): Record<string, string> { + switch (this.auth.method) { + case "external": + return {}; + case "token": + return { + Authorization: `Bearer ${this.auth.token}`, + }; + } + } +} diff --git a/packages/taler-util/src/backup-types.ts b/packages/taler-util/src/backup-types.ts index 2eba1e4ca..8c38b70a6 100644 --- a/packages/taler-util/src/backup-types.ts +++ b/packages/taler-util/src/backup-types.ts @@ -14,6 +14,8 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +import { AmountString } from "./taler-types.js"; + export interface BackupRecovery { walletRootPriv: string; providers: { @@ -21,3 +23,20 @@ export interface BackupRecovery { url: string; }[]; } + +export class BackupBackupProviderTerms { + /** + * Last known supported protocol version. + */ + supported_protocol_version: string; + + /** + * Last known annual fee. + */ + annual_fee: AmountString; + + /** + * Last known storage limit. + */ + storage_limit_in_megabytes: number; +} diff --git a/packages/taler-wallet-core/src/bank-api-client.ts b/packages/taler-util/src/bank-api-client.ts index 3174667f1..cc4123500 100644 --- a/packages/taler-wallet-core/src/bank-api-client.ts +++ b/packages/taler-util/src/bank-api-client.ts @@ -58,11 +58,6 @@ export interface BankAccountBalanceResponse { }; } -export interface BankServiceHandle { - readonly bankAccessApiBaseUrl: string; - readonly http: HttpRequestLibrary; -} - export interface BankUser { username: string; password: string; @@ -75,16 +70,6 @@ export interface WithdrawalOperationInfo { } /** - * FIXME: Rename, this is not part of the integration test harness anymore. - */ -export interface HarnessExchangeBankAccount { - accountName: string; - accountPassword: string; - accountPaytoUri: string; - wireGatewayApiBaseUrl: string; -} - -/** * Helper function to generate the "Authorization" HTTP header. */ function makeBasicAuthHeader(username: string, password: string): string { @@ -99,177 +84,9 @@ const codecForWithdrawalOperationInfo = (): Codec<WithdrawalOperationInfo> => .property("taler_withdraw_uri", codecForString()) .build("WithdrawalOperationInfo"); -/** - * @deprecated Use BankAccessApiClient or WireGatewayApi - */ -export namespace BankApi { - // FIXME: Move to BankAccessApi?! - export async function registerAccount( - bank: BankServiceHandle, - username: string, - password: string, - options: { - iban?: string; - }, - ): Promise<BankUser> { - const url = new URL("testing/register", bank.bankAccessApiBaseUrl); - const resp = await bank.http.postJson(url.href, { - username, - password, - iban: options?.iban, - }); - let paytoUri = `payto://x-taler-bank/localhost/${username}`; - if (resp.status !== 200 && resp.status !== 202 && resp.status !== 204) { - logger.error(`${j2s(await resp.json())}`); - throw TalerError.fromDetail( - TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR, - { - httpStatusCode: resp.status, - }, - ); - } - try { - // Pybank has no body, thus this might throw. - const respJson = await resp.json(); - // LibEuFin demobank returns payto URI in response - if (respJson.paytoUri) { - paytoUri = respJson.paytoUri; - } - } catch (e) { - // Do nothing - } - return { - password, - username, - accountPaytoUri: paytoUri, - }; - } - - // FIXME: Move to BankAccessApi?! - export async function createRandomBankUser( - bank: BankServiceHandle, - ): Promise<BankUser> { - const username = "user-" + encodeCrock(getRandomBytes(10)).toLowerCase(); - const password = "pw-" + encodeCrock(getRandomBytes(10)).toLowerCase(); - // FIXME: This is just a temporary workaround, because demobank is running out of short IBANs - const iban = generateIban("DE", 15); - return await registerAccount(bank, username, password, { - iban, - }); - } - - export async function confirmWithdrawalOperation( - bank: BankServiceHandle, - bankUser: BankUser, - wopi: WithdrawalOperationInfo, - ): Promise<void> { - const url = new URL( - `accounts/${bankUser.username}/withdrawals/${wopi.withdrawal_id}/confirm`, - bank.bankAccessApiBaseUrl, - ); - logger.info(`confirming withdrawal operation via ${url.href}`); - const resp = await bank.http.postJson( - url.href, - {}, - { - headers: { - Authorization: makeBasicAuthHeader( - bankUser.username, - bankUser.password, - ), - }, - }, - ); - - logger.info(`response status ${resp.status}`); - const respJson = await readSuccessResponseJsonOrThrow(resp, codecForAny()); - - // FIXME: We don't check the status here! - } - - export async function abortWithdrawalOperation( - bank: BankServiceHandle, - bankUser: BankUser, - wopi: WithdrawalOperationInfo, - ): Promise<void> { - const url = new URL( - `accounts/${bankUser.username}/withdrawals/${wopi.withdrawal_id}/abort`, - bank.bankAccessApiBaseUrl, - ); - const resp = await bank.http.postJson( - url.href, - {}, - { - headers: { - Authorization: makeBasicAuthHeader( - bankUser.username, - bankUser.password, - ), - }, - }, - ); - await readSuccessResponseJsonOrThrow(resp, codecForAny()); - } -} - -/** - * @deprecated use BankAccessApiClient - */ -export namespace BankAccessApi { - export async function getAccountBalance( - bank: BankServiceHandle, - bankUser: BankUser, - ): Promise<BankAccountBalanceResponse> { - const url = new URL( - `accounts/${bankUser.username}`, - bank.bankAccessApiBaseUrl, - ); - const resp = await bank.http.fetch(url.href, { - headers: { - Authorization: makeBasicAuthHeader( - bankUser.username, - bankUser.password, - ), - }, - }); - return await resp.json(); - } - - export async function createWithdrawalOperation( - bank: BankServiceHandle, - bankUser: BankUser, - amount: string, - ): Promise<WithdrawalOperationInfo> { - const url = new URL( - `accounts/${bankUser.username}/withdrawals`, - bank.bankAccessApiBaseUrl, - ); - const resp = await bank.http.postJson( - url.href, - { - amount, - }, - { - headers: { - Authorization: makeBasicAuthHeader( - bankUser.username, - bankUser.password, - ), - }, - }, - ); - return readSuccessResponseJsonOrThrow( - resp, - codecForWithdrawalOperationInfo(), - ); - } -} - export interface BankAccessApiClientArgs { - baseUrl: string; auth?: { username: string; password: string }; - enableThrottling?: boolean; - allowHttp?: boolean; + httpClient?: HttpRequestLibrary; } export interface BankAccessApiCreateTransactionRequest { @@ -278,11 +95,11 @@ export interface BankAccessApiCreateTransactionRequest { } export class WireGatewayApiClientArgs { - accountName: string; - accountPassword: string; - wireGatewayApiBaseUrl: string; - enableThrottling?: boolean; - allowHttp?: boolean; + auth?: { + username: string; + password: string; + }; + httpClient?: HttpRequestLibrary; } /** @@ -292,11 +109,21 @@ export class WireGatewayApiClientArgs { export class WireGatewayApiClient { httpLib; - constructor(private args: WireGatewayApiClientArgs) { - this.httpLib = createPlatformHttpLib({ - enableThrottling: !!args.enableThrottling, - allowHttp: !!args.allowHttp, - }); + constructor( + private baseUrl: string, + private args: WireGatewayApiClientArgs = {}, + ) { + this.httpLib = args.httpClient ?? createPlatformHttpLib(); + } + + private makeAuthHeader(): Record<string, string> { + const auth = this.args.auth; + if (auth) { + return { + Authorization: makeBasicAuthHeader(auth.username, auth.password), + }; + } + return {}; } async adminAddIncoming(params: { @@ -304,7 +131,7 @@ export class WireGatewayApiClient { reservePub: string; debitAccountPayto: string; }): Promise<void> { - let url = new URL(`admin/add-incoming`, this.args.wireGatewayApiBaseUrl); + let url = new URL(`admin/add-incoming`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "POST", body: { @@ -312,12 +139,7 @@ export class WireGatewayApiClient { reserve_pub: params.reservePub, debit_account: params.debitAccountPayto, }, - headers: { - Authorization: makeBasicAuthHeader( - this.args.accountName, - this.args.accountPassword, - ), - }, + headers: this.makeAuthHeader(), }); logger.info(`add-incoming response status: ${resp.status}`); await checkSuccessResponseOrThrow(resp); @@ -331,11 +153,11 @@ export class WireGatewayApiClient { export class BankAccessApiClient { httpLib: HttpRequestLibrary; - constructor(private args: BankAccessApiClientArgs) { - this.httpLib = createPlatformHttpLib({ - enableThrottling: !!args.enableThrottling, - allowHttp: !!args.allowHttp, - }); + constructor( + private baseUrl: string, + private args: BankAccessApiClientArgs = {}, + ) { + this.httpLib = args.httpClient ?? createPlatformHttpLib(); } setAuth(auth: { username: string; password: string }) { @@ -355,12 +177,18 @@ export class BankAccessApiClient { }; } + async getAccountBalance( + username: string, + ): Promise<BankAccountBalanceResponse> { + const url = new URL(`accounts/${username}`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + headers: this.makeAuthHeader(), + }); + return await resp.json(); + } + async getTransactions(username: string): Promise<void> { - const auth = this.args.auth; - const reqUrl = new URL( - `accounts/${username}/transactions`, - this.args.baseUrl, - ); + const reqUrl = new URL(`accounts/${username}/transactions`, this.baseUrl); const resp = await this.httpLib.fetch(reqUrl.href, { method: "GET", headers: { @@ -376,10 +204,7 @@ export class BankAccessApiClient { username: string, req: BankAccessApiCreateTransactionRequest, ): Promise<any> { - const reqUrl = new URL( - `accounts/${username}/transactions`, - this.args.baseUrl, - ); + const reqUrl = new URL(`accounts/${username}/transactions`, this.baseUrl); const resp = await this.httpLib.fetch(reqUrl.href, { method: "POST", @@ -395,9 +220,9 @@ export class BankAccessApiClient { password: string, options: { iban?: string; - }, + } = {}, ): Promise<BankUser> { - const url = new URL("testing/register", this.args.baseUrl); + const url = new URL("testing/register", this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "POST", body: { @@ -447,7 +272,7 @@ export class BankAccessApiClient { user: string, amount: string, ): Promise<WithdrawalOperationInfo> { - const url = new URL(`accounts/${user}/withdrawals`, this.args.baseUrl); + const url = new URL(`accounts/${user}/withdrawals`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "POST", body: { @@ -467,7 +292,7 @@ export class BankAccessApiClient { ): Promise<void> { const url = new URL( `accounts/${username}/withdrawals/${wopi.withdrawal_id}/confirm`, - this.args.baseUrl, + this.baseUrl, ); logger.info(`confirming withdrawal operation via ${url.href}`); const resp = await this.httpLib.fetch(url.href, { @@ -488,7 +313,7 @@ export class BankAccessApiClient { ): Promise<void> { const url = new URL( `accounts/${accountName}/withdrawals/${wopi.withdrawal_id}/abort`, - this.args.baseUrl, + this.baseUrl, ); const resp = await this.httpLib.fetch(url.href, { method: "POST", diff --git a/packages/taler-util/src/bitcoin.ts b/packages/taler-util/src/bitcoin.ts index 8c22ba522..37b7ae6b9 100644 --- a/packages/taler-util/src/bitcoin.ts +++ b/packages/taler-util/src/bitcoin.ts @@ -69,10 +69,10 @@ export function generateFakeSegwitAddress( addr[0] === "t" && addr[1] == "b" ? "tb" : addr[0] === "b" && addr[1] == "c" && addr[2] === "r" && addr[3] == "t" - ? "bcrt" - : addr[0] === "b" && addr[1] == "c" - ? "bc" - : undefined; + ? "bcrt" + : addr[0] === "b" && addr[1] == "c" + ? "bc" + : undefined; if (prefix === undefined) throw new Error("unknown bitcoin net"); const addr1 = segwit.default.encode(prefix, 0, first_part); diff --git a/packages/taler-util/src/http-common.ts b/packages/taler-util/src/http-common.ts index 93cf9bba0..f25705545 100644 --- a/packages/taler-util/src/http-common.ts +++ b/packages/taler-util/src/http-common.ts @@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL3.0-or-later */ -import { CancellationToken } from "./CancellationToken.js"; +import type { CancellationToken } from "./CancellationToken.js"; import { Codec } from "./codec.js"; import { j2s } from "./helpers.js"; import { @@ -436,7 +436,10 @@ export function getExpiry( export interface HttpLibArgs { enableThrottling?: boolean; - allowHttp?: boolean; + /** + * Only allow HTTPS connections, not plain http. + */ + requireTls?: boolean; } export function encodeBody(body: any): ArrayBuffer { diff --git a/packages/taler-util/src/http-impl.node.ts b/packages/taler-util/src/http-impl.node.ts index 07648a28d..528d303be 100644 --- a/packages/taler-util/src/http-impl.node.ts +++ b/packages/taler-util/src/http-impl.node.ts @@ -63,11 +63,11 @@ const textDecoder = new TextDecoder(); export class HttpLibImpl implements HttpRequestLibrary { private throttle = new RequestThrottler(); private throttlingEnabled = true; - private allowHttp = false; + private requireTls = false; constructor(args?: HttpLibArgs) { this.throttlingEnabled = args?.enableThrottling ?? false; - this.allowHttp = args?.allowHttp ?? false; + this.requireTls = args?.requireTls ?? false; } /** @@ -94,7 +94,7 @@ export class HttpLibImpl implements HttpRequestLibrary { `request to origin ${parsedUrl.origin} was throttled`, ); } - if (!this.allowHttp && parsedUrl.protocol !== "https:") { + if (this.requireTls && parsedUrl.protocol !== "https:") { throw TalerError.fromDetail( TalerErrorCode.WALLET_NETWORK_ERROR, { diff --git a/packages/taler-util/src/http-impl.qtart.ts b/packages/taler-util/src/http-impl.qtart.ts index 3e076e96d..fb642ac89 100644 --- a/packages/taler-util/src/http-impl.qtart.ts +++ b/packages/taler-util/src/http-impl.qtart.ts @@ -41,11 +41,11 @@ const textDecoder = new TextDecoder(); export class HttpLibImpl implements HttpRequestLibrary { private throttle = new RequestThrottler(); private throttlingEnabled = true; - private allowHttp = false; + private requireTls = false; constructor(args?: HttpLibArgs) { this.throttlingEnabled = args?.enableThrottling ?? false; - this.allowHttp = args?.allowHttp ?? false; + this.requireTls = args?.requireTls ?? false; } /** @@ -72,7 +72,7 @@ export class HttpLibImpl implements HttpRequestLibrary { `request to origin ${parsedUrl.origin} was throttled`, ); } - if (!this.allowHttp && parsedUrl.protocol !== "https:") { + if (this.requireTls && parsedUrl.protocol !== "https:") { throw TalerError.fromDetail( TalerErrorCode.WALLET_NETWORK_ERROR, { diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts index cfd0f7c47..568e2f438 100644 --- a/packages/taler-util/src/index.ts +++ b/packages/taler-util/src/index.ts @@ -39,3 +39,6 @@ export * from "./merchant-api-types.js"; export * from "./errors.js"; export * from "./iban.js"; export * from "./transaction-test-data.js"; +export * from "./libeufin-api-types.js"; +export * from "./MerchantApiClient.js"; +export * from "./bank-api-client.js"; diff --git a/packages/taler-util/src/libeufin-api-types.ts b/packages/taler-util/src/libeufin-api-types.ts new file mode 100644 index 000000000..aa3d0cb7a --- /dev/null +++ b/packages/taler-util/src/libeufin-api-types.ts @@ -0,0 +1,31 @@ +/* + This file is part of GNU Taler + (C) 2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +export type FacadeCredentials = + | NoFacadeCredentials + | BasicAuthFacadeCredentials; +export interface NoFacadeCredentials { + type: "none"; +} +export interface BasicAuthFacadeCredentials { + type: "basic"; + + // Username to use to authenticate + username: string; + + // Password to use to authenticate + password: string; +} diff --git a/packages/taler-util/src/merchant-api-types.ts b/packages/taler-util/src/merchant-api-types.ts index 9f00173f2..9933b93dc 100644 --- a/packages/taler-util/src/merchant-api-types.ts +++ b/packages/taler-util/src/merchant-api-types.ts @@ -47,6 +47,7 @@ import { WireAccount, codecForWireAccount, codecForList, + FacadeCredentials, } from "@gnu-taler/taler-util"; export interface MerchantPostOrderRequest { @@ -384,3 +385,19 @@ export const codecForMerchantReserveCreateConfirmation = .property("accounts", codecForList(codecForWireAccount())) .property("reserve_pub", codecForString()) .build("MerchantReserveCreateConfirmation"); + +export interface AccountAddDetails { + // payto:// URI of the account. + payto_uri: string; + + // URL from where the merchant can download information + // about incoming wire transfers to this account. + credit_facade_url?: string; + + // Credentials to use when accessing the credit facade. + // Never returned on a GET (as this may be somewhat + // sensitive data). Can be set in POST + // or PATCH requests to update (or delete) credentials. + // To really delete credentials, set them to the type: "none". + credit_facade_credentials?: FacadeCredentials; +} diff --git a/packages/taler-util/src/payto.ts b/packages/taler-util/src/payto.ts index 2b0af4cc2..60c4ba838 100644 --- a/packages/taler-util/src/payto.ts +++ b/packages/taler-util/src/payto.ts @@ -24,7 +24,7 @@ export type PaytoUri = | PaytoUriBitcoin; export interface PaytoUriGeneric { - targetType: string; + targetType: PaytoType | string; targetPath: string; params: { [name: string]: string }; } @@ -55,6 +55,8 @@ export interface PaytoUriBitcoin extends PaytoUriGeneric { const paytoPfx = "payto://"; +export type PaytoType = "iban" | "bitcoin" | "x-taler-bank" + export function buildPayto( type: "iban", iban: string, @@ -71,7 +73,7 @@ export function buildPayto( account: string, ): PaytoUriTalerBank; export function buildPayto( - type: "iban" | "bitcoin" | "x-taler-bank", + type: PaytoType, first: string, second?: string, ): PaytoUriGeneric { diff --git a/packages/taler-util/src/time.ts b/packages/taler-util/src/time.ts index 55cda08a5..46ed37637 100644 --- a/packages/taler-util/src/time.ts +++ b/packages/taler-util/src/time.ts @@ -154,13 +154,27 @@ export interface TalerProtocolDuration { readonly d_us: number | "forever"; } +/** + * Timeshift in milliseconds. + */ let timeshift = 0; +/** + * Set timetravel offset in milliseconds. + * + * Use carefully and only for testing. + */ export function setDangerousTimetravel(dt: number): void { timeshift = dt; } export namespace Duration { + export function toMilliseconds(d: Duration): number { + if (d.d_ms === "forever") { + return Number.MAX_VALUE; + } + return d.d_ms; + } export function getRemaining( deadline: AbsoluteTime, now = AbsoluteTime.now(), diff --git a/packages/taler-util/src/transactions-types.ts b/packages/taler-util/src/transactions-types.ts index 6331bc731..304183ceb 100644 --- a/packages/taler-util/src/transactions-types.ts +++ b/packages/taler-util/src/transactions-types.ts @@ -528,22 +528,6 @@ export interface OrderShortInfo { summary_i18n?: InternationalizedString; /** - * List of products that are part of the order - */ - products: Product[] | undefined; - - /** - * Time indicating when the order should be delivered. - * May be overwritten by individual products. - */ - delivery_date?: TalerProtocolTimestamp; - - /** - * Delivery location for (all!) products. - */ - delivery_location?: Location; - - /** * URL of the fulfillment, given by the merchant */ fulfillmentUrl?: string; @@ -724,7 +708,6 @@ export const codecForOrderShortInfo = (): Codec<OrderShortInfo> => .property("fulfillmentUrl", codecOptional(codecForString())) .property("merchant", codecForMerchantInfo()) .property("orderId", codecForString()) - .property("products", codecOptional(codecForList(codecForProduct()))) .property("summary", codecForString()) .property("summary_i18n", codecOptional(codecForInternationalizedString())) .build("OrderShortInfo"); diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts index accab746f..c6f19c73f 100644 --- a/packages/taler-util/src/wallet-types.ts +++ b/packages/taler-util/src/wallet-types.ts @@ -1076,13 +1076,6 @@ export interface KnownBankAccounts { accounts: KnownBankAccountsInfo[]; } -export interface ExchangeTosStatusDetails { - acceptedVersion?: string; - currentVersion?: string; - contentType?: string; - content?: string; -} - /** * Wire fee for one wire method */ @@ -1253,7 +1246,6 @@ export interface ExchangeFullDetails { exchangeBaseUrl: string; currency: string; paytoUris: string[]; - tos: ExchangeTosStatusDetails; auditors: ExchangeAuditor[]; wireInfo: WireInfo; denomFees: DenomOperationMap<FeeDescription[]>; @@ -1317,14 +1309,6 @@ const codecForExchangeAuditor = (): Codec<ExchangeAuditor> => .property("denomination_keys", codecForList(codecForAuditorDenomSig())) .build("codecForExchangeAuditor"); -const codecForExchangeTos = (): Codec<ExchangeTosStatusDetails> => - buildCodecForObject<ExchangeTosStatusDetails>() - .property("acceptedVersion", codecOptional(codecForString())) - .property("currentVersion", codecOptional(codecForString())) - .property("contentType", codecOptional(codecForString())) - .property("content", codecOptional(codecForString())) - .build("ExchangeTos"); - export const codecForFeeDescriptionPair = (): Codec<FeeDescriptionPair> => buildCodecForObject<FeeDescriptionPair>() .property("group", codecForString()) @@ -1357,7 +1341,6 @@ export const codecForExchangeFullDetails = (): Codec<ExchangeFullDetails> => .property("currency", codecForString()) .property("exchangeBaseUrl", codecForString()) .property("paytoUris", codecForList(codecForString())) - .property("tos", codecForExchangeTos()) .property("auditors", codecForList(codecForExchangeAuditor())) .property("wireInfo", codecForWireInfo()) .property("denomFees", codecForFeesByOperations()) @@ -1465,22 +1448,11 @@ export interface ExchangeWithdrawalDetails { selectedDenoms: DenomSelectionState; /** - * Does the wallet know about an auditor for - * the exchange that the reserve. - */ - isAudited: boolean; - - /** * Did the user already accept the current terms of service for the exchange? */ termsOfServiceAccepted: boolean; /** - * The exchange is trusted directly. - */ - isTrusted: boolean; - - /** * The earliest deposit expiration of the selected coins. */ earliestDepositExpiration: TalerProtocolTimestamp; @@ -2466,7 +2438,7 @@ export interface PreparePeerPushCreditResponse { amount: AmountString; amountRaw: AmountString; amountEffective: AmountString; - peerPushPaymentIncomingId: string; + peerPushCreditId: string; transactionId: string; } @@ -2481,7 +2453,7 @@ export interface PreparePeerPullDebitResponse { amountRaw: AmountString; amountEffective: AmountString; - peerPullPaymentIncomingId: string; + peerPullDebitId: string; transactionId: string; } @@ -2504,7 +2476,7 @@ export interface ConfirmPeerPushCreditRequest { * * @deprecated specify transactionId instead! */ - peerPushPaymentIncomingId?: string; + peerPushCreditId?: string; transactionId?: string; } @@ -2519,7 +2491,7 @@ export interface AcceptPeerPullPaymentResponse { export const codecForConfirmPeerPushPaymentRequest = (): Codec<ConfirmPeerPushCreditRequest> => buildCodecForObject<ConfirmPeerPushCreditRequest>() - .property("peerPushPaymentIncomingId", codecOptional(codecForString())) + .property("peerPushCreditId", codecOptional(codecForString())) .property("transactionId", codecOptional(codecForString())) .build("ConfirmPeerPushCreditRequest"); @@ -2529,7 +2501,7 @@ export interface ConfirmPeerPullDebitRequest { * * @deprecated use transactionId instead */ - peerPullPaymentIncomingId?: string; + peerPullDebitId?: string; transactionId?: string; } @@ -2547,7 +2519,7 @@ export const codecForApplyDevExperiment = export const codecForAcceptPeerPullPaymentRequest = (): Codec<ConfirmPeerPullDebitRequest> => buildCodecForObject<ConfirmPeerPullDebitRequest>() - .property("peerPullPaymentIncomingId", codecOptional(codecForString())) + .property("peerPullDebitId", codecOptional(codecForString())) .property("transactionId", codecOptional(codecForString())) .build("ConfirmPeerPullDebitRequest"); @@ -2673,3 +2645,25 @@ export interface RecoverStoredBackupRequest { export interface DeleteStoredBackupRequest { name: string; } + +export const codecForDeleteStoredBackupRequest = + (): Codec<DeleteStoredBackupRequest> => + buildCodecForObject<DeleteStoredBackupRequest>() + .property("name", codecForString()) + .build("DeleteStoredBackupRequest"); + +export const codecForRecoverStoredBackupRequest = + (): Codec<RecoverStoredBackupRequest> => + buildCodecForObject<RecoverStoredBackupRequest>() + .property("name", codecForString()) + .build("RecoverStoredBackupRequest"); + +export interface TestingSetTimetravelRequest { + offsetMs: number; +} + +export const codecForTestingSetTimetravelRequest = + (): Codec<TestingSetTimetravelRequest> => + buildCodecForObject<TestingSetTimetravelRequest>() + .property("offsetMs", codecForNumber()) + .build("TestingSetTimetravelRequest"); diff --git a/packages/taler-util/tsconfig.json b/packages/taler-util/tsconfig.json index 34f35d253..2e97142ce 100644 --- a/packages/taler-util/tsconfig.json +++ b/packages/taler-util/tsconfig.json @@ -5,8 +5,8 @@ "declaration": true, "declarationMap": false, "target": "ES2020", - "module": "ES2020", - "moduleResolution": "node16", + "module": "Node16", + "moduleResolution": "Node16", "sourceMap": true, "lib": ["ES2020"], "types": ["node"], diff --git a/packages/taler-wallet-cli/package.json b/packages/taler-wallet-cli/package.json index 06df1da76..6196c8971 100644 --- a/packages/taler-wallet-cli/package.json +++ b/packages/taler-wallet-cli/package.json @@ -33,8 +33,8 @@ "@types/node": "^18.11.17", "prettier": "^2.8.8", "rimraf": "^3.0.2", - "typedoc": "^0.24.8", - "typescript": "^5.1.3" + "typedoc": "^0.25.1", + "typescript": "^5.2.2" }, "dependencies": { "@gnu-taler/taler-util": "workspace:*", diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts index 36e7f7768..3fc86d0b5 100644 --- a/packages/taler-wallet-cli/src/index.ts +++ b/packages/taler-wallet-cli/src/index.ts @@ -244,7 +244,7 @@ async function createLocalWallet( const dbPath = walletCliArgs.wallet.walletDbFile ?? defaultWalletDbPath; const myHttpLib = createPlatformHttpLib({ enableThrottling: walletCliArgs.wallet.noThrottle ? false : true, - allowHttp: walletCliArgs.wallet.noHttp ? false : true, + requireTls: walletCliArgs.wallet.noHttp, }); const wallet = await createNativeWalletHost({ persistentStoragePath: dbPath !== ":memory:" ? dbPath : undefined, @@ -883,7 +883,7 @@ backupCli.subcommand("exportDb", "export-db").action(async (args) => { }); }); -backupCli.subcommand("storeBackup", "store-backup").action(async (args) => { +backupCli.subcommand("storeBackup", "store").action(async (args) => { await withWallet(args, async (wallet) => { const resp = await wallet.client.call( WalletApiOperation.CreateStoredBackup, @@ -893,6 +893,46 @@ backupCli.subcommand("storeBackup", "store-backup").action(async (args) => { }); }); +backupCli.subcommand("storeBackup", "list-stored").action(async (args) => { + await withWallet(args, async (wallet) => { + const resp = await wallet.client.call( + WalletApiOperation.ListStoredBackups, + {}, + ); + console.log(JSON.stringify(resp, undefined, 2)); + }); +}); + +backupCli + .subcommand("storeBackup", "delete-stored") + .requiredArgument("name", clk.STRING) + .action(async (args) => { + await withWallet(args, async (wallet) => { + const resp = await wallet.client.call( + WalletApiOperation.DeleteStoredBackup, + { + name: args.storeBackup.name, + }, + ); + console.log(JSON.stringify(resp, undefined, 2)); + }); + }); + +backupCli + .subcommand("recoverBackup", "recover-stored") + .requiredArgument("name", clk.STRING) + .action(async (args) => { + await withWallet(args, async (wallet) => { + const resp = await wallet.client.call( + WalletApiOperation.RecoverStoredBackup, + { + name: args.recoverBackup.name, + }, + ); + console.log(JSON.stringify(resp, undefined, 2)); + }); + }); + backupCli.subcommand("importDb", "import-db").action(async (args) => { await withWallet(args, async (wallet) => { const dumpRaw = await read(process.stdin); @@ -984,14 +1024,14 @@ peerCli peerCli .subcommand("confirmIncomingPayPull", "confirm-pull-debit") - .requiredArgument("peerPullPaymentIncomingId", clk.STRING) + .requiredArgument("peerPullDebitId", clk.STRING) .action(async (args) => { await withWallet(args, async (wallet) => { const resp = await wallet.client.call( WalletApiOperation.ConfirmPeerPullDebit, { - peerPullPaymentIncomingId: - args.confirmIncomingPayPull.peerPullPaymentIncomingId, + peerPullDebitId: + args.confirmIncomingPayPull.peerPullDebitId, }, ); console.log(JSON.stringify(resp, undefined, 2)); @@ -1000,14 +1040,14 @@ peerCli peerCli .subcommand("confirmIncomingPayPush", "confirm-push-credit") - .requiredArgument("peerPushPaymentIncomingId", clk.STRING) + .requiredArgument("peerPushCreditId", clk.STRING) .action(async (args) => { await withWallet(args, async (wallet) => { const resp = await wallet.client.call( WalletApiOperation.ConfirmPeerPushCredit, { - peerPushPaymentIncomingId: - args.confirmIncomingPayPush.peerPushPaymentIncomingId, + peerPushCreditId: + args.confirmIncomingPayPush.peerPushCreditId, }, ); console.log(JSON.stringify(resp, undefined, 2)); @@ -1219,10 +1259,7 @@ advancedCli help: "Run the 'bench-internal' benchmark", }) .action(async (args) => { - const myHttpLib = createPlatformHttpLib({ - enableThrottling: false, - allowHttp: true, - }); + const myHttpLib = createPlatformHttpLib(); const res = await createNativeWalletHost2({ // No persistent DB storage. persistentStoragePath: undefined, diff --git a/packages/taler-wallet-cli/tsconfig.json b/packages/taler-wallet-cli/tsconfig.json index 100339e43..42f0d88a8 100644 --- a/packages/taler-wallet-cli/tsconfig.json +++ b/packages/taler-wallet-cli/tsconfig.json @@ -3,10 +3,10 @@ "compilerOptions": { "composite": true, "target": "ES2018", - "module": "ESNext", + "module": "Node16", "moduleResolution": "Node16", "sourceMap": true, - "lib": ["es6"], + "lib": ["ES2020"], "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "strict": true, diff --git a/packages/taler-wallet-core/package.json b/packages/taler-wallet-core/package.json index 9cd6f6466..fda7b6081 100644 --- a/packages/taler-wallet-core/package.json +++ b/packages/taler-wallet-core/package.json @@ -65,8 +65,8 @@ "po2json": "^0.4.5", "prettier": "^2.8.8", "rimraf": "^3.0.2", - "typedoc": "^0.24.8", - "typescript": "^5.1.3" + "typedoc": "^0.25.1", + "typescript": "^5.2.2" }, "dependencies": { "@gnu-taler/idb-bridge": "workspace:*", diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts index c1a761fb6..35777e714 100644 --- a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts +++ b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts @@ -959,11 +959,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { req: DenominationValidationRequest, ): Promise<ValidationResult> { const { masterPub, denom } = req; - const value: AmountJson = { - currency: denom.currency, - fraction: denom.amountFrac, - value: denom.amountVal, - }; + const value: AmountJson = Amounts.parseOrThrow(denom.value); const p = buildSigPS(TalerSignaturePurpose.MASTER_DENOMINATION_KEY_VALIDITY) .put(decodeCrock(masterPub)) .put(timestampRoundedToBuffer(denom.stampStart)) diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index b52a503bc..25757ef25 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -22,59 +22,59 @@ import { IDBDatabase, IDBFactory, IDBObjectStore, + IDBRequest, IDBTransaction, structuredEncapsulate, } from "@gnu-taler/idb-bridge"; import { AgeCommitmentProof, - AmountJson, AmountString, + Amounts, + AttentionInfo, + Codec, CoinEnvelope, + CoinPublicKeyString, CoinRefreshRequest, CoinStatus, - MerchantContractTerms, + DenomSelectionState, DenominationInfo, DenominationPubKey, - DenomSelectionState, EddsaPublicKeyString, EddsaSignatureString, ExchangeAuditor, ExchangeGlobalFees, + HashCodeString, InternationalizedString, - Location, + Logger, + MerchantContractTerms, MerchantInfo, PayCoinSelection, PeerContractTerms, - Product, RefreshReason, TalerErrorDetail, + TalerPreciseTimestamp, TalerProtocolDuration, TalerProtocolTimestamp, TransactionIdStr, UnblindedSignature, WireInfo, - HashCodeString, - Amounts, - AttentionInfo, - Logger, - CoinPublicKeyString, - TalerPreciseTimestamp, + codecForAny, } from "@gnu-taler/taler-util"; +import { RetryInfo, TaskIdentifiers } from "./operations/common.js"; import { DbAccess, DbReadOnlyTransaction, DbReadWriteTransaction, - describeContents, - describeIndex, - describeStore, GetReadWriteAccess, IndexDescriptor, - openDatabase, StoreDescriptor, StoreNames, StoreWithIndexes, + describeContents, + describeIndex, + describeStore, + openDatabase, } from "./util/query.js"; -import { RetryInfo, TaskIdentifiers } from "./operations/common.js"; /** * This file contains the database schema of the Taler wallet together @@ -99,12 +99,25 @@ import { RetryInfo, TaskIdentifiers } from "./operations/common.js"; */ /** + FIXMEs: + - Contract terms can be quite large. We currently tend to read the + full contract terms from the DB quite often. + Instead, we should probably extract what we need into a separate object + store. + - More object stores should have an "id" primary key, + as this makes referencing less expensive. + - Coin selections should probably go into a separate object store. + - Some records should be split up into an extra "details" record + that we don't always need to iterate over. + */ + +/** * Name of the Taler database. This is effectively the major * version of the DB schema. Whenever it changes, custom import logic * for all previous versions must be written, which should be * avoided. */ -export const TALER_WALLET_MAIN_DB_NAME = "taler-wallet-main-v9"; +export const TALER_WALLET_MAIN_DB_NAME = "taler-wallet-main-v10"; /** * Name of the metadata database. This database is used @@ -115,10 +128,17 @@ export const TALER_WALLET_MAIN_DB_NAME = "taler-wallet-main-v9"; export const TALER_WALLET_META_DB_NAME = "taler-wallet-meta"; /** - * Stored backups, mainly created when manually importing a backup. + * Name of the "stored backups" database. + * Stored backups are created before manually importing a backup. + * We use IndexedDB for this purpose, since we don't have file system + * access on some platforms. */ -export const TALER_WALLET_STORED_BACKUPS_DB_NAME = "taler-wallet-stored-backups"; +export const TALER_WALLET_STORED_BACKUPS_DB_NAME = + "taler-wallet-stored-backups"; +/** + * Name of the "meta config" database. + */ export const CURRENT_DB_CONFIG_KEY = "currentMainDbName"; /** @@ -128,26 +148,26 @@ export const CURRENT_DB_CONFIG_KEY = "currentMainDbName"; * backwards-compatible way or object stores and indices * are added. */ -export const WALLET_DB_MINOR_VERSION = 10; +export const WALLET_DB_MINOR_VERSION = 1; /** - * Ranges for operation status fields. - * - * All individual enums should make sure that the values they - * defined are in the right range. + * Format of the operation status code: 0x0abc_nnnn + + * a=1: active + * 0x0100_nnnn: pending + * 0x0101_nnnn: dialog + * 0x0102_nnnn: (reserved) + * 0x0103_nnnn: aborting + * 0x0110_nnnn: suspended + * 0x0113_nnnn: suspended-aborting + * a=5: final + * 0x0500_nnnn: done + * 0x0501_nnnn: failed + * 0x0502_nnnn: expired + * 0x0503_nnnn: aborted + * + * nnnn=0000 should always be the most generic minor state for the major state */ -export enum OperationStatusRange { - // Operations that need to be actively processed. - ACTIVE_START = 10, - ACTIVE_END = 29, - // Operations that are suspended and might - // expire, but nothing else can be done. - SUSPENDED_START = 30, - SUSPENDED_END = 49, - // Operations that don't need any attention or processing. - DORMANT_START = 50, - DORMANT_END = 69, -} /** * Status of a withdrawal. @@ -156,71 +176,70 @@ export enum WithdrawalGroupStatus { /** * Reserve must be registered with the bank. */ - PendingRegisteringBank = 10, + PendingRegisteringBank = 0x0100_0001, + SuspendedRegisteringBank = 0x0110_0001, /** * We've registered reserve's information with the bank * and are now waiting for the user to confirm the withdraw * with the bank (typically 2nd factor auth). */ - PendingWaitConfirmBank = 11, + PendingWaitConfirmBank = 0x0100_0002, + SuspendedWaitConfirmBank = 0x0110_0002, /** * Querying reserve status with the exchange. */ - PendingQueryingStatus = 12, + PendingQueryingStatus = 0x0100_0003, + SuspendedQueryingStatus = 0x0110_0003, /** * Ready for withdrawal. */ - PendingReady = 13, + PendingReady = 0x0100_0004, + SuspendedReady = 0x0110_0004, /** * We are telling the bank that we don't want to complete * the withdrawal! */ - AbortingBank = 14, + AbortingBank = 0x0103_0001, + SuspendedAbortingBank = 0x0113_0001, /** * Exchange wants KYC info from the user. */ - PendingKyc = 16, + PendingKyc = 0x0100_0005, + SuspendedKyc = 0x0110_005, /** * Exchange is doing AML checks. */ - PendingAml = 17, - - SuspendedRegisteringBank = 30, - SuspendedWaitConfirmBank = 31, - SuspendedQueryingStatus = 32, - SuspendedReady = 33, - SuspendedAbortingBank = 34, - SuspendedKyc = 35, - SuspendedAml = 36, + PendingAml = 0x0100_0006, + SuspendedAml = 0x0100_0006, /** * The corresponding withdraw record has been created. * No further processing is done, unless explicitly requested * by the user. */ - Finished = 50, + Done = 0x0500_0000, /** * The bank aborted the withdrawal. */ - FailedBankAborted = 51, + FailedBankAborted = 0x0501_0001, - FailedAbortingBank = 59, + FailedAbortingBank = 0x0501_0002, /** * Aborted in a state where we were supposed to * talk to the exchange. Money might have been * wired or not. */ - AbortedExchange = 60, + AbortedExchange = 0x0503_0001, - AbortedBank = 61, + AbortedBank = 0x0503_0002, } /** @@ -259,78 +278,23 @@ export interface ReserveBankInfo { } /** - * Record that indicates the wallet trusts - * a particular auditor. - */ -export interface AuditorTrustRecord { - /** - * Currency that we trust this auditor for. - */ - currency: string; - - /** - * Base URL of the auditor. - */ - auditorBaseUrl: string; - - /** - * Public key of the auditor. - */ - auditorPub: string; - - /** - * UIDs for the operation of adding this auditor - * as a trusted auditor. - * - * (Used for backup/sync merging and tombstones.) - */ - uids: string[]; -} - -/** - * Record to indicate trust for a particular exchange. - */ -export interface ExchangeTrustRecord { - /** - * Currency that we trust this exchange for. - */ - currency: string; - - /** - * Canonicalized exchange base URL. - */ - exchangeBaseUrl: string; - - /** - * Master public key of the exchange. - */ - exchangeMasterPub: string; - - /** - * UIDs for the operation of adding this exchange - * as trusted. - */ - uids: string[]; -} - -/** * Status of a denomination. */ export enum DenominationVerificationStatus { /** - * Verification was delayed. + * Verification was delayed (pending). */ - Unverified = OperationStatusRange.ACTIVE_START, + Unverified = 0x0100_0000, /** * Verified as valid. */ - VerifiedGood = OperationStatusRange.DORMANT_START, + VerifiedGood = 0x0500_0000, /** * Verified as invalid. */ - VerifiedBad = OperationStatusRange.DORMANT_START + 1, + VerifiedBad = 0x0501_0000, } export interface DenomFees { @@ -359,12 +323,14 @@ export interface DenomFees { * Denomination record as stored in the wallet's database. */ export interface DenominationRecord { + /** + * Currency of the denomination. + * + * Stored separately as we have an index on it. + */ currency: string; - // FIXME: Use binary encoding of amount instead? - amountVal: number; - - amountFrac: number; + value: AmountString; /** * The denomination public key. @@ -443,14 +409,6 @@ export interface DenominationRecord { } export namespace DenominationRecord { - export function getValue(d: DenominationRecord): AmountJson { - return { - currency: d.currency, - fraction: d.amountFrac, - value: d.amountVal, - }; - } - export function toDenomInfo(d: DenominationRecord): DenominationInfo { return { denomPub: d.denomPub, @@ -463,7 +421,7 @@ export namespace DenominationRecord { stampExpireLegal: d.stampExpireLegal, stampExpireWithdraw: d.stampExpireWithdraw, stampStart: d.stampStart, - value: Amounts.stringify(DenominationRecord.getValue(d)), + value: Amounts.stringify(d.value), exchangeBaseUrl: d.exchangeBaseUrl, }; } @@ -578,6 +536,7 @@ export enum ExchangeEntryDbRecordStatus { Used = 3, } +// FIXME: Use status ranges for this as well? export enum ExchangeEntryDbUpdateStatus { Initial = 1, InitialUpdate = 2, @@ -658,9 +617,9 @@ export interface ExchangeEntryRecord { } export enum PlanchetStatus { - Pending = 10 /* ACTIVE_START */, - KycRequired = 11 /* ACTIVE_START + 1 */, - WithdrawalDone = 50 /* DORMANT_START */, + Pending = 0x0100_0000, + KycRequired = 0x0100_0001, + WithdrawalDone = 0x0500_000, } /** @@ -933,50 +892,45 @@ export interface RewardRecord { } export enum RewardRecordStatus { - PendingPickup = 10, - - SuspendidPickup = 20, - - DialogAccept = 30, - - Done = 50, - Aborted = 51, + PendingPickup = 0x0100_0000, + SuspendedPickup = 0x0110_0000, + DialogAccept = 0x0101_0000, + Done = 0x0500_0000, + Aborted = 0x0500_0000, } export enum RefreshCoinStatus { - Pending = OperationStatusRange.ACTIVE_START, - Finished = OperationStatusRange.DORMANT_START, + Pending = 0x0100_0000, + Finished = 0x0500_0000, /** * The refresh for this coin has been frozen, because of a permanent error. * More info in lastErrorPerCoin. */ - Failed = OperationStatusRange.DORMANT_START + 1, -} - -export enum OperationStatus { - Finished = OperationStatusRange.DORMANT_START, - Pending = OperationStatusRange.ACTIVE_START, + Failed = 0x0501_000, } export enum RefreshOperationStatus { - Pending = 10 /* ACTIVE_START */, - Suspended = 20 /* DORMANT_START + 2 */, + Pending = 0x0100_0000, + Suspended = 0x0110_0000, - Finished = 50 /* DORMANT_START */, - Failed = 51 /* DORMANT_START + 1 */, + Finished = 0x0500_000, + Failed = 0x0501_000, } /** * Status of a single element of a deposit group. */ export enum DepositElementStatus { - Unknown = 10, - Accepted = 20, - KycRequired = 30, - Wired = 40, - RefundSuccess = 50, - RefundFailed = 51, + DepositPending = 0x0100_0000, + /** + * Accepted, but tracking. + */ + Tracking = 0x0100_0001, + KycRequired = 0x0100_0002, + Wired = 0x0500_0000, + RefundSuccess = 0x0503_0000, + RefundFailed = 0x0501_0000, } /** @@ -991,16 +945,10 @@ export interface RefreshReasonDetails { * Group of refresh operations. The refreshed coins do not * have to belong to the same exchange, but must have the same * currency. - * - * FIXME: Should include the currency as a top-level field, - * but we need to write a migration for that. */ export interface RefreshGroupRecord { operationStatus: RefreshOperationStatus; - // FIXME: Put this into a different object store? - lastErrorPerCoin: { [coinIndex: number]: TalerErrorDetail }; - /** * Unique, randomly generated identifier for this group of * refresh operations. @@ -1009,8 +957,6 @@ export interface RefreshGroupRecord { /** * Currency of this refresh group. - * - * FIXME: Write a migration to add this to earlier DB versions. */ currency: string; @@ -1026,13 +972,9 @@ export interface RefreshGroupRecord { oldCoinPubs: string[]; - // FIXME: Should this go into a separate - // object store for faster updates? - refreshSessionPerCoin: (RefreshSessionRecord | undefined)[]; - inputPerCoin: AmountString[]; - estimatedOutputPerCoin: AmountString[]; + expectedOutputPerCoin: AmountString[]; /** * Flag for each coin whether refreshing finished. @@ -1054,6 +996,13 @@ export interface RefreshGroupRecord { * Ongoing refresh */ export interface RefreshSessionRecord { + refreshGroupId: string; + + /** + * Index of the coin in the refresh group. + */ + coinIndex: number; + /** * 512-bit secret that can be used to derive * the other cryptographic material for the refresh session. @@ -1078,6 +1027,8 @@ export interface RefreshSessionRecord { * The no-reveal-index after we've done the melting. */ norevealIndex?: number; + + lastError?: TalerErrorDetail; } export enum RefundReason { @@ -1106,9 +1057,6 @@ export interface AllowedExchangeInfo { * processing in the wallet. */ export interface WalletContractData { - products?: Product[]; - summaryI18n: { [lang_tag: string]: string } | undefined; - /** * Fulfillment URL, or the empty string if the order has no fulfillment URL. * @@ -1126,6 +1074,7 @@ export interface WalletContractData { orderId: string; merchantBaseUrl: string; summary: string; + summaryI18n: { [lang_tag: string]: string } | undefined; autoRefund: TalerProtocolDuration | undefined; maxWireFee: AmountString; wireFeeAmortization: number; @@ -1137,89 +1086,90 @@ export interface WalletContractData { wireInfoHash: string; maxDepositFee: AmountString; minimumAge?: number; - deliveryDate: TalerProtocolTimestamp | undefined; - deliveryLocation: Location | undefined; } export enum PurchaseStatus { /** * Not downloaded yet. */ - PendingDownloadingProposal = 10, + PendingDownloadingProposal = 0x0100_0000, + SuspendedDownloadingProposal = 0x0110_0000, /** * The user has accepted the proposal. */ - PendingPaying = 11, + PendingPaying = 0x0100_0001, + SuspendedPaying = 0x0110_0001, /** * Currently in the process of aborting with a refund. */ - AbortingWithRefund = 12, + AbortingWithRefund = 0x0103_0000, + SuspendedAbortingWithRefund = 0x0113_0000, /** * Paying a second time, likely with different session ID */ - PendingPayingReplay = 13, + PendingPayingReplay = 0x0100_0002, + SuspendedPayingReplay = 0x0110_0002, /** * Query for refunds (until query succeeds). */ - PendingQueryingRefund = 14, + PendingQueryingRefund = 0x0100_0003, + SuspendedQueryingRefund = 0x0110_0003, /** * Query for refund (until auto-refund deadline is reached). */ - PendingQueryingAutoRefund = 15, + PendingQueryingAutoRefund = 0x0100_0004, + SuspendedQueryingAutoRefund = 0x0110_0004, - PendingAcceptRefund = 16, - - SuspendedDownloadingProposal = 20, - SuspendedPaying = 21, - SuspendedAbortingWithRefund = 22, - SuspendedPayingReplay = 23, - SuspendedQueryingRefund = 24, - SuspendedQueryingAutoRefund = 25, - SuspendedPendingAcceptRefund = 26, + PendingAcceptRefund = 0x0100_0005, + SuspendedPendingAcceptRefund = 0x0100_0005, /** * Proposal downloaded, but the user needs to accept/reject it. */ - DialogProposed = 30, + DialogProposed = 0x0101_0000, + /** * Proposal shared to other wallet or read from other wallet * the user needs to accept/reject it. */ - DialogShared = 31, + DialogShared = 0x0101_0001, /** * The user has rejected the proposal. */ - AbortedProposalRefused = 50, + AbortedProposalRefused = 0x0503_0000, /** * Downloading or processing the proposal has failed permanently. */ - FailedClaim = 51, + FailedClaim = 0x0501_0000, + + /** + * Payment was successful. + */ + Done = 0x0500_0000, /** * Downloaded proposal was detected as a re-purchase. */ - RepurchaseDetected = 52, + DoneRepurchaseDetected = 0x0500_0001, /** * The payment has been aborted. */ - AbortedIncompletePayment = 53, + AbortedIncompletePayment = 0x0503_0000, /** - * Payment was successful. + * Tried to abort, but aborting failed or was cancelled. */ - Done = 54, + FailedAbort = 0x0501_0001, - FailedAbort = 55, - - AbortedRefunded = 56, + AbortedRefunded = 0x0503_0000, } /** @@ -1334,14 +1284,6 @@ export interface PurchaseRecord { timestampAccept: TalerPreciseTimestamp | undefined; /** - * Pending refunds for the purchase. A refund is pending - * when the merchant reports a transient error from the exchange. - * - * FIXME: Put this into a separate object store? - */ - // refunds: { [refundKey: string]: WalletRefundItem }; - - /** * When was the last refund made? * Set to 0 if no refund was made on the purchase. */ @@ -1404,6 +1346,7 @@ export interface WalletBackupConfState { lastBackupNonce?: string; } +// FIXME: Should these be numeric codes? export const enum WithdrawalRecordType { BankManual = "bank-manual", BankIntegrated = "bank-integrated", @@ -1428,7 +1371,8 @@ export interface WgInfoBankManual { export interface WgInfoBankPeerPull { withdrawalType: WithdrawalRecordType.PeerPullCredit; - contractTerms: any; + // FIXME: include a transaction ID here? + /** * Needed to quickly construct the taler:// URI for the counterparty * without a join. @@ -1439,7 +1383,7 @@ export interface WgInfoBankPeerPull { export interface WgInfoBankPeerPush { withdrawalType: WithdrawalRecordType.PeerPushCredit; - contractTerms: any; + // FIXME: include a transaction ID here? } export interface WgInfoBankRecoup { @@ -1713,19 +1657,21 @@ export interface BackupProviderRecord { } export enum DepositOperationStatus { - PendingDeposit = 10, - Aborting = 11, - PendingTrack = 12, - PendingKyc = 13, + PendingDeposit = 0x0100_0000, + PendingTrack = 0x0100_0001, + PendingKyc = 0x0100_0002, + + Aborting = 0x0103_0000, - SuspendedDeposit = 20, - SuspendedAborting = 21, - SuspendedTrack = 22, - SuspendedKyc = 23, + SuspendedDeposit = 0x0110_0000, + SuspendedTrack = 0x0110_0001, + SuspendedKyc = 0x0110_0002, - Finished = 50, - Failed = 51, - Aborted = 52, + SuspendedAborting = 0x0113_0000, + + Finished = 0x0500_0000, + Failed = 0x0501_0000, + Aborted = 0x0503_0000, } export interface DepositTrackingInfo { @@ -1777,10 +1723,8 @@ export interface DepositGroupRecord { /** * The counterparty effective deposit amount. - * - * FIXME: If possible, rename to counterpartyEffectiveDepositAmount. */ - effectiveDepositAmount: AmountString; + counterpartyEffectiveDepositAmount: AmountString; timestampCreated: TalerPreciseTimestamp; @@ -1788,11 +1732,7 @@ export interface DepositGroupRecord { operationStatus: DepositOperationStatus; - // FIXME: Duplication between this and transactionPerCoin! - depositedPerCoin: boolean[]; - - // FIXME: Improve name! - transactionPerCoin: DepositElementStatus[]; + statusPerCoin: DepositElementStatus[]; /** * When the deposit transaction was aborted and @@ -1816,31 +1756,6 @@ export interface DepositKycInfo { exchangeBaseUrl: string; } -/** - * Record for a deposits that the wallet observed - * as a result of double spending, but which is not - * present in the wallet's own database otherwise. - */ -export interface GhostDepositGroupRecord { - /** - * When multiple deposits for the same contract terms hash - * have a different timestamp, we choose the earliest one. - */ - timestamp: TalerPreciseTimestamp; - - contractTermsHash: string; - - deposits: { - coinPub: string; - amount: AmountString; - timestamp: TalerProtocolTimestamp; - depositFee: AmountString; - merchantPub: string; - coinSig: string; - wireHash: string; - }[]; -} - export interface TombstoneRecord { /** * Tombstone ID, with the syntax "tmb:<type>:<key>". @@ -1848,24 +1763,24 @@ export interface TombstoneRecord { id: string; } -export enum PeerPushPaymentInitiationStatus { +export enum PeerPushDebitStatus { /** * Initiated, but no purse created yet. */ - PendingCreatePurse = 10 /* ACTIVE_START */, - PendingReady = 11, - AbortingDeletePurse = 12, - AbortingRefresh = 13, + PendingCreatePurse = 0x0100_0000 /* ACTIVE_START */, + PendingReady = 0x0100_0001, + AbortingDeletePurse = 0x0103_0000, + AbortingRefresh = 0x0103_0001, - SuspendedCreatePurse = 30, - SuspendedReady = 31, - SuspendedAbortingDeletePurse = 32, - SuspendedAbortingRefresh = 33, + SuspendedCreatePurse = 0x0110_0000, + SuspendedReady = 0x0110_0001, + SuspendedAbortingDeletePurse = 0x0113_0000, + SuspendedAbortingRefresh = 0x0113_0001, - Done = 50 /* DORMANT_START */, - Aborted = 51, - Failed = 52, - Expired = 53, + Done = 0x0500_0000, + Aborted = 0x0503_0000, + Failed = 0x0501_0000, + Expired = 0x0502_0000, } export interface PeerPushPaymentCoinSelection { @@ -1876,7 +1791,7 @@ export interface PeerPushPaymentCoinSelection { /** * Record for a push P2P payment that this wallet initiated. */ -export interface PeerPushPaymentInitiationRecord { +export interface PeerPushDebitRecord { /** * What exchange are funds coming from? */ @@ -1922,11 +1837,6 @@ export interface PeerPushPaymentInitiationRecord { */ contractEncNonce: string; - /** - * FIXME: Put those in a different object store! - */ - contractTerms: PeerContractTerms; - purseExpiration: TalerProtocolTimestamp; timestampCreated: TalerPreciseTimestamp; @@ -1936,32 +1846,34 @@ export interface PeerPushPaymentInitiationRecord { /** * Status of the peer push payment initiation. */ - status: PeerPushPaymentInitiationStatus; + status: PeerPushDebitStatus; } -export enum PeerPullPaymentInitiationStatus { - PendingCreatePurse = 10 /* ACTIVE_START */, +export enum PeerPullPaymentCreditStatus { + PendingCreatePurse = 0x0100_0000, /** * Purse created, waiting for the other party to accept the * invoice and deposit money into it. */ - PendingReady = 11 /* ACTIVE_START + 1 */, - PendingMergeKycRequired = 12 /* ACTIVE_START + 2 */, - PendingWithdrawing = 13, - AbortingDeletePurse = 14, + PendingReady = 0x0100_0001, + PendingMergeKycRequired = 0x0100_0002, + PendingWithdrawing = 0x0100_0003, + + AbortingDeletePurse = 0x0103_0000, + + SuspendedCreatePurse = 0x0110_0000, + SuspendedReady = 0x0110_0001, + SuspendedMergeKycRequired = 0x0110_0002, + SuspendedWithdrawing = 0x0110_0000, - SuspendedCreatePurse = 30, - SuspendedReady = 31, - SuspendedMergeKycRequired = 32, - SuspendedWithdrawing = 33, - SuspendedAbortingDeletePurse = 34, + SuspendedAbortingDeletePurse = 0x0113_0000, - Done = 50 /* DORMANT_START */, - Failed = 51, - Aborted = 52, + Done = 0x0500_0000, + Failed = 0x0501_0000, + Aborted = 0x0503_0000, } -export interface PeerPullPaymentInitiationRecord { +export interface PeerPullCreditRecord { /** * What exchange are we using for the payment request? */ @@ -1969,6 +1881,7 @@ export interface PeerPullPaymentInitiationRecord { /** * Amount requested. + * FIXME: What type of instructed amount is i? */ amount: AmountString; @@ -1999,11 +1912,6 @@ export interface PeerPullPaymentInitiationRecord { contractEncNonce: string; - /** - * FIXME: Put in separate object store! - */ - contractTerms: PeerContractTerms; - mergeTimestamp: TalerPreciseTimestamp; mergeReserveRowId: number; @@ -2011,7 +1919,7 @@ export interface PeerPullPaymentInitiationRecord { /** * Status of the peer pull payment initiation. */ - status: PeerPullPaymentInitiationStatus; + status: PeerPullPaymentCreditStatus; kycInfo?: KycPendingInfo; @@ -2020,24 +1928,24 @@ export interface PeerPullPaymentInitiationRecord { withdrawalGroupId: string | undefined; } -export enum PeerPushPaymentIncomingStatus { - PendingMerge = 10 /* ACTIVE_START */, - PendingMergeKycRequired = 11 /* ACTIVE_START + 1 */, +export enum PeerPushCreditStatus { + PendingMerge = 0x0100_0000, + PendingMergeKycRequired = 0x0100_0001, /** * Merge was successful and withdrawal group has been created, now * everything is in the hand of the withdrawal group. */ - PendingWithdrawing = 12, + PendingWithdrawing = 0x0100_0002, - SuspendedMerge = 20, - SuspendedMergeKycRequired = 21, - SuspendedWithdrawing = 22, + SuspendedMerge = 0x0110_0000, + SuspendedMergeKycRequired = 0x0110_0001, + SuspendedWithdrawing = 0x0110_0002, - DialogProposed = 30 /* USER_ATTENTION_START */, + DialogProposed = 0x0101_0000, - Done = 50 /* DORMANT_START */, - Aborted = 51, - Failed = 52, + Done = 0x0500_0000, + Aborted = 0x0503_0000, + Failed = 0x0501_0000, } /** @@ -2046,7 +1954,7 @@ export enum PeerPushPaymentIncomingStatus { * Unique: (exchangeBaseUrl, pursePub) */ export interface PeerPushPaymentIncomingRecord { - peerPushPaymentIncomingId: string; + peerPushCreditId: string; exchangeBaseUrl: string; @@ -2069,7 +1977,7 @@ export interface PeerPushPaymentIncomingRecord { /** * Status of the peer push payment incoming initiation. */ - status: PeerPushPaymentIncomingStatus; + status: PeerPushCreditStatus; /** * Associated withdrawal group. @@ -2090,17 +1998,17 @@ export interface PeerPushPaymentIncomingRecord { } export enum PeerPullDebitRecordStatus { - PendingDeposit = 10 /* ACTIVE_START */, - AbortingRefresh = 11, + PendingDeposit = 0x0100_0001, + AbortingRefresh = 0x0103_0001, - SuspendedDeposit = 20, - SuspendedAbortingRefresh = 21, + SuspendedDeposit = 0x0110_0001, + SuspendedAbortingRefresh = 0x0113_0001, - DialogProposed = 30 /* USER_ATTENTION_START */, + DialogProposed = 0x0101_0001, - DonePaid = 50 /* DORMANT_START */, - Aborted = 51, - Failed = 52, + Done = 0x0500_0000, + Aborted = 0x0503_0000, + Failed = 0x0501_0000, } export interface PeerPullPaymentCoinSelection { @@ -2118,7 +2026,7 @@ export interface PeerPullPaymentCoinSelection { * AKA PeerPullDebit. */ export interface PeerPullPaymentIncomingRecord { - peerPullPaymentIncomingId: string; + peerPullDebitId: string; pursePub: string; @@ -2184,8 +2092,7 @@ export interface OperationRetryRecord { */ export interface CoinAvailabilityRecord { currency: string; - amountVal: number; - amountFrac: number; + value: AmountString; denomPubHash: string; exchangeBaseUrl: string; @@ -2263,10 +2170,10 @@ export interface CurrencySettingsRecord { } export enum RefundGroupStatus { - Pending = 10, - Done = 50, - Failed = 51, - Aborted = 52, + Pending = 0x0100_0000, + Done = 0x0500_0000, + Failed = 0x0501_0000, + Aborted = 0x0503_0000, } /** @@ -2302,16 +2209,16 @@ export enum RefundItemStatus { * * We'll try again! */ - Pending = 10, + Pending = 0x0100_0000, /** * Refund was obtained successfully. */ - Done = 50, + Done = 0x0500_0000, /** * Permanent error reported by the exchange * for the refund. */ - Failed = 51, + Failed = 0x0501_0000, } /** @@ -2327,7 +2234,9 @@ export interface RefundItemRecord { refundGroupId: string; - // Execution time as claimed by the merchant + /** + * Execution time as claimed by the merchant + */ executionTime: TalerProtocolTimestamp; /** @@ -2337,22 +2246,15 @@ export interface RefundItemRecord { refundAmount: AmountString; - //refundFee: AmountString; - - /** - * Upper bound on the refresh cost incurred by - * applying this refund. - * - * Might be lower in practice when two refunds on the same - * coin are refreshed in the same refresh operation. - */ - //totalRefreshCostBound: AmountString; - coinPub: string; rtxid: number; } +export function passthroughCodec<T>(): Codec<T> { + return codecForAny(); +} + /** * Schema definition for the IndexedDB * wallet database. @@ -2362,7 +2264,6 @@ export const WalletStoresV1 = { "currencySettings", describeContents<CurrencySettingsRecord>({ keyPath: ["currency"], - versionAdded: 3, }), {}, ), @@ -2411,42 +2312,11 @@ export const WalletStoresV1 = { byReservePub: describeIndex("byReservePub", "reservePub", {}), }, ), - exchangeTos: describeStore( - "exchangeTos", - describeContents<ExchangeTosRecord>({ - keyPath: ["exchangeBaseUrl", "etag"], - }), - {}, - ), config: describeStore( "config", describeContents<ConfigRecord>({ keyPath: "key" }), {}, ), - auditorTrust: describeStore( - "auditorTrust", - describeContents<AuditorTrustRecord>({ - keyPath: ["currency", "auditorBaseUrl"], - }), - { - byAuditorPub: describeIndex("byAuditorPub", "auditorPub"), - byUid: describeIndex("byUid", "uids", { - multiEntry: true, - }), - }, - ), - exchangeTrust: describeStore( - "exchangeTrust", - describeContents<ExchangeTrustRecord>({ - keyPath: ["currency", "exchangeBaseUrl"], - }), - { - byExchangeMasterPub: describeIndex( - "byExchangeMasterPub", - "exchangeMasterPub", - ), - }, - ), denominations: describeStore( "denominations", describeContents<DenominationRecord>({ @@ -2499,6 +2369,13 @@ export const WalletStoresV1 = { byStatus: describeIndex("byStatus", "operationStatus"), }, ), + refreshSessions: describeStore( + "refreshSessions", + describeContents<RefreshSessionRecord>({ + keyPath: ["refreshGroupId", "coinIndex"], + }), + {}, + ), recoupGroups: describeStore( "recoupGroups", describeContents<RecoupGroupRecord>({ @@ -2607,17 +2484,10 @@ export const WalletStoresV1 = { }), {}, ), - ghostDepositGroups: describeStore( - "ghostDepositGroups", - describeContents<GhostDepositGroupRecord>({ - keyPath: "contractTermsHash", - }), - {}, - ), - peerPushPaymentIncoming: describeStore( - "peerPushPaymentIncoming", + peerPushCredit: describeStore( + "peerPushCredit", describeContents<PeerPushPaymentIncomingRecord>({ - keyPath: "peerPushPaymentIncomingId", + keyPath: "peerPushCreditId", }), { byExchangeAndPurse: describeIndex("byExchangeAndPurse", [ @@ -2628,24 +2498,21 @@ export const WalletStoresV1 = { "byExchangeAndContractPriv", ["exchangeBaseUrl", "contractPriv"], { - versionAdded: 5, unique: true, }, ), byWithdrawalGroupId: describeIndex( "byWithdrawalGroupId", "withdrawalGroupId", - { - versionAdded: 5, - }, + {}, ), byStatus: describeIndex("byStatus", "status"), }, ), - peerPullPaymentIncoming: describeStore( - "peerPullPaymentIncoming", + peerPullDebit: describeStore( + "peerPullDebit", describeContents<PeerPullPaymentIncomingRecord>({ - keyPath: "peerPullPaymentIncomingId", + keyPath: "peerPullDebitId", }), { byExchangeAndPurse: describeIndex("byExchangeAndPurse", [ @@ -2656,16 +2523,15 @@ export const WalletStoresV1 = { "byExchangeAndContractPriv", ["exchangeBaseUrl", "contractPriv"], { - versionAdded: 5, unique: true, }, ), byStatus: describeIndex("byStatus", "status"), }, ), - peerPullPaymentInitiations: describeStore( - "peerPullPaymentInitiations", - describeContents<PeerPullPaymentInitiationRecord>({ + peerPullCredit: describeStore( + "peerPullCredit", + describeContents<PeerPullCreditRecord>({ keyPath: "pursePub", }), { @@ -2673,15 +2539,13 @@ export const WalletStoresV1 = { byWithdrawalGroupId: describeIndex( "byWithdrawalGroupId", "withdrawalGroupId", - { - versionAdded: 5, - }, + {}, ), }, ), - peerPushPaymentInitiations: describeStore( - "peerPushPaymentInitiations", - describeContents<PeerPushPaymentInitiationRecord>({ + peerPushDebit: describeStore( + "peerPushDebit", + describeContents<PeerPushDebitRecord>({ keyPath: "pursePub", }), { @@ -2706,7 +2570,6 @@ export const WalletStoresV1 = { "userAttention", describeContents<UserAttentionRecord>({ keyPath: ["entityId", "info.type"], - versionAdded: 2, }), {}, ), @@ -2714,20 +2577,16 @@ export const WalletStoresV1 = { "refundGroups", describeContents<RefundGroupRecord>({ keyPath: "refundGroupId", - versionAdded: 7, }), { byProposalId: describeIndex("byProposalId", "proposalId"), - byStatus: describeIndex("byStatus", "status", { - versionAdded: 10, - }), + byStatus: describeIndex("byStatus", "status", {}), }, ), refundItems: describeStore( "refundItems", describeContents<RefundItemRecord>({ keyPath: "id", - versionAdded: 7, autoIncrement: true, }), { @@ -2742,7 +2601,6 @@ export const WalletStoresV1 = { "fixups", describeContents<FixupRecord>({ keyPath: "fixupName", - versionAdded: 2, }), {}, ), @@ -2845,11 +2703,10 @@ export async function exportSingleDb( dbName, undefined, () => { - // May not happen, since we're not requesting a specific version - throw Error("unexpected version change"); + logger.info(`unexpected onversionchange in exportSingleDb of ${dbName}`); }, () => { - logger.info("unexpected onupgradeneeded"); + logger.info(`unexpected onupgradeneeded in exportSingleDb of ${dbName}`); }, ); @@ -2861,7 +2718,7 @@ export async function exportSingleDb( return new Promise((resolve, reject) => { const tx = myDb.transaction(Array.from(myDb.objectStoreNames)); tx.addEventListener("complete", () => { - myDb.close(); + //myDb.close(); resolve(singleDbDump); }); // tslint:disable-next-line:prefer-for-of @@ -2897,6 +2754,7 @@ export async function exportSingleDb( if (store.keyPath == null) { rec.key = structuredEncapsulate(cursor.key); } + storeDump.records.push(rec); cursor.continue(); } }); @@ -2925,21 +2783,22 @@ async function recoverFromDump( db: IDBDatabase, dbDump: DbDumpDatabase, ): Promise<void> { - return new Promise((resolve, reject) => { - const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite"); - tx.addEventListener("complete", () => { - resolve(); - }); - for (let i = 0; i < db.objectStoreNames.length; i++) { - const name = db.objectStoreNames[i]; - const storeDump = dbDump.stores[name]; - if (!storeDump) continue; - for (let rec of storeDump.records) { - tx.objectStore(name).put(rec.value, rec.key); - } + const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite"); + const txProm = promiseFromTransaction(tx); + const storeNames = db.objectStoreNames; + for (let i = 0; i < storeNames.length; i++) { + const name = db.objectStoreNames[i]; + const storeDump = dbDump.stores[name]; + if (!storeDump) continue; + await promiseFromRequest(tx.objectStore(name).clear()); + logger.info(`importing ${storeDump.records.length} records into ${name}`); + for (let rec of storeDump.records) { + await promiseFromRequest(tx.objectStore(name).put(rec.value, rec.key)); + logger.info("importing record done"); } - tx.commit(); - }); + } + tx.commit(); + return await txProm; } function checkDbDump(x: any): x is DbDump { @@ -2969,106 +2828,7 @@ export interface FixupDescription { /** * Manual migrations between minor versions of the DB schema. */ -export const walletDbFixups: FixupDescription[] = [ - { - name: "RefreshGroupRecord_currency", - async fn(tx): Promise<void> { - await tx.refreshGroups.iter().forEachAsync(async (rg) => { - if (rg.currency) { - return; - } - // Empty refresh group without input coin, delete it! - if (rg.inputPerCoin.length === 0) { - await tx.refreshGroups.delete(rg.refreshGroupId); - return; - } - rg.currency = Amounts.parseOrThrow(rg.inputPerCoin[0]).currency; - await tx.refreshGroups.put(rg); - }); - }, - }, - { - name: "DepositGroupRecord_transactionPerCoin", - async fn(tx): Promise<void> { - await tx.depositGroups.iter().forEachAsync(async (dg) => { - if (dg.transactionPerCoin) { - return; - } - dg.transactionPerCoin = dg.depositedPerCoin.map( - (c) => DepositElementStatus.Unknown, - ); - await tx.depositGroups.put(dg); - }); - }, - }, - { - name: "PeerPullPaymentIncomingRecord_totalCostEstimated_add", - async fn(tx): Promise<void> { - await tx.peerPullPaymentIncoming.iter().forEachAsync(async (pi) => { - if (pi.totalCostEstimated) { - return; - } - // Not really the cost, but a good substitute for older transactions - // that don't sture the effective cost of the transaction. - pi.totalCostEstimated = pi.contractTerms.amount; - await tx.peerPullPaymentIncoming.put(pi); - }); - }, - }, - { - name: "PeerPushPaymentIncomingRecord_totalCostEstimated_add", - async fn(tx): Promise<void> { - await tx.peerPushPaymentIncoming.iter().forEachAsync(async (pi) => { - if (pi.estimatedAmountEffective) { - return; - } - const contractTerms = await tx.contractTerms.get(pi.contractTermsHash); - if (!contractTerms) { - // Not sure what we can do here! - } else { - // Not really the cost, but a good substitute for older transactions - // that don't sture the effective cost of the transaction. - pi.estimatedAmountEffective = contractTerms.contractTermsRaw.amount; - await tx.peerPushPaymentIncoming.put(pi); - } - }); - }, - }, - { - name: "PeerPullPaymentInitiationRecord_estimatedAmountEffective_add", - async fn(tx): Promise<void> { - await tx.peerPullPaymentInitiations.iter().forEachAsync(async (pi) => { - if (pi.estimatedAmountEffective) { - return; - } - pi.estimatedAmountEffective = pi.amount; - await tx.peerPullPaymentInitiations.put(pi); - }); - }, - }, - { - name: "PeerPushPaymentInitiationRecord_ALL_removeLegacyTx", - async fn(tx): Promise<void> { - await tx.peerPushPaymentInitiations.iter().forEachAsync(async (pi) => { - // Remove legacy transactions that don't have the totalCost field yet. - if (!pi.totalCost) { - await tx.peerPushPaymentInitiations.delete(pi.pursePub); - } - }); - }, - }, - { - name: "CoinAvailabilityRecord_visibleCoinCount_add", - async fn(tx): Promise<void> { - await tx.coinAvailability.iter().forEachAsync(async (r) => { - if (r.visibleCoinCount == null) { - r.visibleCoinCount = r.freshCoinCount; - await tx.coinAvailability.put(r); - } - }); - }, - }, -]; +export const walletDbFixups: FixupDescription[] = []; const logger = new Logger("db.ts"); @@ -3196,6 +2956,17 @@ function promiseFromTransaction(transaction: IDBTransaction): Promise<void> { }); } +export function promiseFromRequest(request: IDBRequest): Promise<any> { + return new Promise((resolve, reject) => { + request.onsuccess = () => { + resolve(request.result); + }; + request.onerror = () => { + reject(request.error); + }; + }); +} + /** * Purge all data in the given database. */ diff --git a/packages/taler-wallet-core/src/dbless.ts b/packages/taler-wallet-core/src/dbless.ts index 5532345ae..11c6c0f74 100644 --- a/packages/taler-wallet-core/src/dbless.ts +++ b/packages/taler-wallet-core/src/dbless.ts @@ -48,24 +48,20 @@ import { parsePaytoUri, UnblindedSignature, } from "@gnu-taler/taler-util"; -import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js"; -import { DenominationRecord } from "./db.js"; -import { - BankAccessApi, - BankApi, - BankServiceHandle, -} from "./bank-api-client.js"; import { HttpRequestLibrary, readSuccessResponseJsonOrThrow, } from "@gnu-taler/taler-util/http"; +import { BankAccessApiClient } from "../../taler-util/src/bank-api-client.js"; +import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js"; +import { DenominationRecord } from "./db.js"; +import { isWithdrawableDenom } from "./index.js"; +import { ExchangeInfo } from "./operations/exchanges.js"; +import { assembleRefreshRevealRequest } from "./operations/refresh.js"; import { getBankStatusUrl, getBankWithdrawalInfo, } from "./operations/withdraw.js"; -import { ExchangeInfo } from "./operations/exchanges.js"; -import { assembleRefreshRevealRequest } from "./operations/refresh.js"; -import { isWithdrawableDenom, WalletConfig } from "./index.js"; const logger = new Logger("dbless.ts"); @@ -121,14 +117,10 @@ export async function topupReserveWithDemobank( args: TopupReserveWithDemobankArgs, ) { const { http, bankAccessApiBaseUrl, amount, exchangeInfo, reservePub } = args; - const bankHandle: BankServiceHandle = { - bankAccessApiBaseUrl: bankAccessApiBaseUrl, - http, - }; - const bankUser = await BankApi.createRandomBankUser(bankHandle); - const wopi = await BankAccessApi.createWithdrawalOperation( - bankHandle, - bankUser, + const bankClient = new BankAccessApiClient(bankAccessApiBaseUrl); + const bankUser = await bankClient.createRandomBankUser(); + const wopi = await bankClient.createWithdrawalOperation( + bankUser.username, amount, ); const bankInfo = await getBankWithdrawalInfo(http, wopi.taler_withdraw_uri); @@ -149,7 +141,7 @@ export async function topupReserveWithDemobank( httpResp, codecForBankWithdrawalOperationPostResponse(), ); - await BankApi.confirmWithdrawalOperation(bankHandle, bankUser, wopi); + await bankClient.confirmWithdrawalOperation(bankUser.username, wopi); } export async function withdrawCoin(args: { @@ -167,11 +159,7 @@ export async function withdrawCoin(args: { reservePriv: reserveKeyPair.reservePriv, reservePub: reserveKeyPair.reservePub, secretSeed: encodeCrock(getRandomBytes(32)), - value: { - currency: denom.currency, - fraction: denom.amountFrac, - value: denom.amountVal, - }, + value: Amounts.parseOrThrow(denom.value), }); const reqBody: ExchangeWithdrawRequest = { @@ -219,11 +207,7 @@ export function findDenomOrThrow( ): DenominationRecord { const denomselAllowLate = options.denomselAllowLate ?? false; for (const d of exchangeInfo.keys.currentDenominations) { - const value: AmountJson = { - currency: d.currency, - fraction: d.amountFrac, - value: d.amountVal, - }; + const value: AmountJson = Amounts.parseOrThrow(d.value); if ( Amounts.cmp(value, amount) === 0 && isWithdrawableDenom(d, denomselAllowLate) @@ -311,11 +295,7 @@ export async function refreshCoin(req: { denomPub: x.denomPub, denomPubHash: x.denomPubHash, feeWithdraw: x.fees.feeWithdraw, - value: Amounts.stringify({ - currency: x.currency, - fraction: x.amountFrac, - value: x.amountVal, - }), + value: x.value, })), meltCoinMaxAge: oldCoin.maxAge, }); diff --git a/packages/taler-wallet-core/src/host-impl.node.ts b/packages/taler-wallet-core/src/host-impl.node.ts index 0b6539306..a6dae58a1 100644 --- a/packages/taler-wallet-core/src/host-impl.node.ts +++ b/packages/taler-wallet-core/src/host-impl.node.ts @@ -52,7 +52,6 @@ interface MakeDbResult { async function makeFileDb( args: DefaultNodeWalletArgs = {}, ): Promise<MakeDbResult> { - BridgeIDBFactory.enableTracing = false; const myBackend = new MemoryBackend(); myBackend.enableTracing = false; const storagePath = args.persistentStoragePath; @@ -135,13 +134,16 @@ export async function createNativeWalletHost2( } else { myHttpLib = createPlatformHttpLib({ enableThrottling: true, - allowHttp: args.config?.features?.allowHttp, + requireTls: !args.config?.features?.allowHttp, }); } let dbResp: MakeDbResult; - if (args.persistentStoragePath &&args.persistentStoragePath.endsWith(".json")) { + if ( + args.persistentStoragePath && + args.persistentStoragePath.endsWith(".json") + ) { logger.info("using legacy file-based DB backend"); dbResp = await makeFileDb(args); } else { diff --git a/packages/taler-wallet-core/src/host-impl.qtart.ts b/packages/taler-wallet-core/src/host-impl.qtart.ts index 81dbe0acd..85f8df6e5 100644 --- a/packages/taler-wallet-core/src/host-impl.qtart.ts +++ b/packages/taler-wallet-core/src/host-impl.qtart.ts @@ -188,7 +188,7 @@ export async function createNativeWalletHost2( } else { myHttpLib = createPlatformHttpLib({ enableThrottling: true, - allowHttp: args.config?.features?.allowHttp, + requireTls: !args.config?.features?.allowHttp, }); } diff --git a/packages/taler-wallet-core/src/index.ts b/packages/taler-wallet-core/src/index.ts index d64f7d5e6..643d65620 100644 --- a/packages/taler-wallet-core/src/index.ts +++ b/packages/taler-wallet-core/src/index.ts @@ -44,8 +44,6 @@ export * from "./operations/backup/index.js"; export * from "./operations/exchanges.js"; -export * from "./bank-api-client.js"; - export * from "./operations/withdraw.js"; export * from "./operations/refresh.js"; diff --git a/packages/taler-wallet-core/src/internal-wallet-state.ts b/packages/taler-wallet-core/src/internal-wallet-state.ts index a189c9cb3..20f8a7511 100644 --- a/packages/taler-wallet-core/src/internal-wallet-state.ts +++ b/packages/taler-wallet-core/src/internal-wallet-state.ts @@ -107,10 +107,6 @@ export interface ExchangeOperations { }>, exchangeBaseUrl: string, ): Promise<ExchangeDetailsRecord | undefined>; - getExchangeTrust( - ws: InternalWalletState, - exchangeInfo: ExchangeEntryRecord, - ): Promise<TrustInfo>; updateExchangeFromUrl( ws: InternalWalletState, baseUrl: string, diff --git a/packages/taler-wallet-core/src/operations/balance.ts b/packages/taler-wallet-core/src/operations/balance.ts index 0fcab0542..a20ded2af 100644 --- a/packages/taler-wallet-core/src/operations/balance.ts +++ b/packages/taler-wallet-core/src/operations/balance.ts @@ -95,14 +95,7 @@ function computeRefreshGroupAvailableAmount(r: RefreshGroupRecord): AmountJson { return available; } for (let i = 0; i < r.oldCoinPubs.length; i++) { - const session = r.refreshSessionPerCoin[i]; - if (session) { - // We are always assuming the refresh will succeed, thus we - // report the output as available balance. - available = Amounts.add(available, session.amountRefreshOutput).amount; - } else { - available = Amounts.add(available, r.estimatedOutputPerCoin[i]).amount; - } + available = Amounts.add(available, r.expectedOutputPerCoin[i]).amount; } return available; } @@ -140,11 +133,7 @@ export async function getBalancesInsideTransaction( const b = initBalance(ca.currency); const count = ca.visibleCoinCount ?? 0; for (let i = 0; i < count; i++) { - b.available = Amounts.add(b.available, { - currency: ca.currency, - fraction: ca.amountFrac, - value: ca.amountVal, - }).amount; + b.available = Amounts.add(b.available, ca.value).amount; } }); @@ -163,7 +152,7 @@ export async function getBalancesInsideTransaction( case WithdrawalGroupStatus.AbortedExchange: case WithdrawalGroupStatus.FailedAbortingBank: case WithdrawalGroupStatus.FailedBankAborted: - case WithdrawalGroupStatus.Finished: + case WithdrawalGroupStatus.Done: // Does not count as pendingIncoming return; case WithdrawalGroupStatus.PendingReady: @@ -281,7 +270,7 @@ export async function getAcceptableExchangeBaseUrls( const acceptableExchangeUrls = new Set<string>(); const depositableExchangeUrls = new Set<string>(); await ws.db - .mktx((x) => [x.exchanges, x.exchangeDetails, x.auditorTrust]) + .mktx((x) => [x.exchanges, x.exchangeDetails]) .runReadOnly(async (tx) => { // FIXME: We should have a DB index to look up all exchanges // for a particular auditor ... @@ -415,11 +404,7 @@ export async function getMerchantPaymentBalanceDetails( if (ca.currency != req.currency) { return; } - const singleCoinAmount: AmountJson = { - currency: ca.currency, - fraction: ca.amountFrac, - value: ca.amountVal, - }; + const singleCoinAmount: AmountJson = Amounts.parseOrThrow(ca.value); const coinAmount: AmountJson = Amounts.mult( singleCoinAmount, ca.freshCoinCount, @@ -537,11 +522,7 @@ export async function getPeerPaymentBalanceDetailsInTx( ) { return; } - const singleCoinAmount: AmountJson = { - currency: ca.currency, - fraction: ca.amountFrac, - value: ca.amountVal, - }; + const singleCoinAmount: AmountJson = Amounts.parseOrThrow(ca.value); const coinAmount: AmountJson = Amounts.mult( singleCoinAmount, ca.freshCoinCount, diff --git a/packages/taler-wallet-core/src/operations/common.ts b/packages/taler-wallet-core/src/operations/common.ts index e96beb5b2..50dd3dc5c 100644 --- a/packages/taler-wallet-core/src/operations/common.ts +++ b/packages/taler-wallet-core/src/operations/common.ts @@ -26,7 +26,6 @@ import { CoinRefreshRequest, CoinStatus, Duration, - ErrorInfoSummary, ExchangeEntryStatus, ExchangeListItem, ExchangeTosStatus, @@ -34,9 +33,11 @@ import { getErrorDetailFromException, j2s, Logger, + makeErrorDetail, NotificationType, OperationErrorInfo, RefreshReason, + TalerError, TalerErrorCode, TalerErrorDetail, TombstoneIdStr, @@ -44,32 +45,31 @@ import { TransactionType, WalletNotification, } from "@gnu-taler/taler-util"; +import { CryptoApiStoppedError } from "../crypto/workers/crypto-dispatcher.js"; import { - WalletStoresV1, + BackupProviderRecord, CoinRecord, + DepositGroupRecord, ExchangeDetailsRecord, + ExchangeEntryDbRecordStatus, + ExchangeEntryDbUpdateStatus, ExchangeEntryRecord, - BackupProviderRecord, - DepositGroupRecord, + PeerPullCreditRecord, PeerPullPaymentIncomingRecord, - PeerPullPaymentInitiationRecord, + PeerPushDebitRecord, PeerPushPaymentIncomingRecord, - PeerPushPaymentInitiationRecord, PurchaseRecord, RecoupGroupRecord, RefreshGroupRecord, RewardRecord, + WalletStoresV1, WithdrawalGroupRecord, - ExchangeEntryDbUpdateStatus, - ExchangeEntryDbRecordStatus, } from "../db.js"; -import { makeErrorDetail, TalerError } from "@gnu-taler/taler-util"; import { InternalWalletState } from "../internal-wallet-state.js"; -import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; -import { GetReadOnlyAccess, GetReadWriteAccess } from "../util/query.js"; -import { CryptoApiStoppedError } from "../crypto/workers/crypto-dispatcher.js"; import { PendingTaskType, TaskId } from "../pending-types.js"; import { assertUnreachable } from "../util/assertUnreachable.js"; +import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; +import { GetReadOnlyAccess, GetReadWriteAccess } from "../util/query.js"; import { constructTransactionIdentifier } from "./transactions.js"; const logger = new Logger("operations/common.ts"); @@ -144,8 +144,7 @@ export async function makeCoinAvailable( if (!car) { car = { maxAge: ageRestriction, - amountFrac: denom.amountFrac, - amountVal: denom.amountVal, + value: denom.value, currency: denom.currency, denomPubHash: denom.denomPubHash, exchangeBaseUrl: denom.exchangeBaseUrl, @@ -271,7 +270,7 @@ function convertTaskToTransactionId( case PendingTaskType.PeerPullDebit: return constructTransactionIdentifier({ tag: TransactionType.PeerPullDebit, - peerPullPaymentIncomingId: parsedTaskId.peerPullPaymentIncomingId, + peerPullDebitId: parsedTaskId.peerPullDebitId, }); // FIXME: This doesn't distinguish internal-withdrawal. // Maybe we should have a different task type for that as well? @@ -284,7 +283,7 @@ function convertTaskToTransactionId( case PendingTaskType.PeerPushCredit: return constructTransactionIdentifier({ tag: TransactionType.PeerPushCredit, - peerPushPaymentIncomingId: parsedTaskId.peerPushPaymentIncomingId, + peerPushCreditId: parsedTaskId.peerPushCreditId, }); case PendingTaskType.Deposit: return constructTransactionIdentifier({ @@ -836,9 +835,9 @@ export type ParsedTaskIdentifier = | { tag: PendingTaskType.Deposit; depositGroupId: string } | { tag: PendingTaskType.ExchangeCheckRefresh; exchangeBaseUrl: string } | { tag: PendingTaskType.ExchangeUpdate; exchangeBaseUrl: string } - | { tag: PendingTaskType.PeerPullDebit; peerPullPaymentIncomingId: string } + | { tag: PendingTaskType.PeerPullDebit; peerPullDebitId: string } | { tag: PendingTaskType.PeerPullCredit; pursePub: string } - | { tag: PendingTaskType.PeerPushCredit; peerPushPaymentIncomingId: string } + | { tag: PendingTaskType.PeerPushCredit; peerPushCreditId: string } | { tag: PendingTaskType.PeerPushDebit; pursePub: string } | { tag: PendingTaskType.Purchase; proposalId: string } | { tag: PendingTaskType.Recoup; recoupGroupId: string } @@ -865,9 +864,9 @@ export function parseTaskIdentifier(x: string): ParsedTaskIdentifier { case PendingTaskType.PeerPullCredit: return { tag: type, pursePub: rest[0] }; case PendingTaskType.PeerPullDebit: - return { tag: type, peerPullPaymentIncomingId: rest[0] }; + return { tag: type, peerPullDebitId: rest[0] }; case PendingTaskType.PeerPushCredit: - return { tag: type, peerPushPaymentIncomingId: rest[0] }; + return { tag: type, peerPushCreditId: rest[0] }; case PendingTaskType.PeerPushDebit: return { tag: type, pursePub: rest[0] }; case PendingTaskType.Purchase: @@ -896,9 +895,9 @@ export function constructTaskIdentifier(p: ParsedTaskIdentifier): TaskId { case PendingTaskType.ExchangeUpdate: return `${p.tag}:${p.exchangeBaseUrl}` as TaskId; case PendingTaskType.PeerPullDebit: - return `${p.tag}:${p.peerPullPaymentIncomingId}` as TaskId; + return `${p.tag}:${p.peerPullDebitId}` as TaskId; case PendingTaskType.PeerPushCredit: - return `${p.tag}:${p.peerPushPaymentIncomingId}` as TaskId; + return `${p.tag}:${p.peerPushCreditId}` as TaskId; case PendingTaskType.PeerPullCredit: return `${p.tag}:${p.pursePub}` as TaskId; case PendingTaskType.PeerPushDebit: @@ -950,23 +949,23 @@ export namespace TaskIdentifiers { return `${PendingTaskType.Backup}:${backupRecord.baseUrl}` as TaskId; } export function forPeerPushPaymentInitiation( - ppi: PeerPushPaymentInitiationRecord, + ppi: PeerPushDebitRecord, ): TaskId { return `${PendingTaskType.PeerPushDebit}:${ppi.pursePub}` as TaskId; } export function forPeerPullPaymentInitiation( - ppi: PeerPullPaymentInitiationRecord, + ppi: PeerPullCreditRecord, ): TaskId { return `${PendingTaskType.PeerPullCredit}:${ppi.pursePub}` as TaskId; } export function forPeerPullPaymentDebit( ppi: PeerPullPaymentIncomingRecord, ): TaskId { - return `${PendingTaskType.PeerPullDebit}:${ppi.peerPullPaymentIncomingId}` as TaskId; + return `${PendingTaskType.PeerPullDebit}:${ppi.peerPullDebitId}` as TaskId; } export function forPeerPushCredit( ppi: PeerPushPaymentIncomingRecord, ): TaskId { - return `${PendingTaskType.PeerPushCredit}:${ppi.peerPushPaymentIncomingId}` as TaskId; + return `${PendingTaskType.PeerPushCredit}:${ppi.peerPushCreditId}` as TaskId; } } diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts index a8ec859cf..8ea792d91 100644 --- a/packages/taler-wallet-core/src/operations/deposits.ts +++ b/packages/taler-wallet-core/src/operations/deposits.ts @@ -510,10 +510,10 @@ async function refundDepositGroup( ws: InternalWalletState, depositGroup: DepositGroupRecord, ): Promise<TaskRunResult> { - const newTxPerCoin = [...depositGroup.transactionPerCoin]; - logger.info(`status per coin: ${j2s(depositGroup.transactionPerCoin)}`); - for (let i = 0; i < depositGroup.transactionPerCoin.length; i++) { - const st = depositGroup.transactionPerCoin[i]; + const newTxPerCoin = [...depositGroup.statusPerCoin]; + logger.info(`status per coin: ${j2s(depositGroup.statusPerCoin)}`); + for (let i = 0; i < depositGroup.statusPerCoin.length; i++) { + const st = depositGroup.statusPerCoin[i]; switch (st) { case DepositElementStatus.RefundFailed: case DepositElementStatus.RefundSuccess: @@ -593,7 +593,7 @@ async function refundDepositGroup( if (!newDg) { return; } - newDg.transactionPerCoin = newTxPerCoin; + newDg.statusPerCoin = newTxPerCoin; const refreshCoins: CoinRefreshRequest[] = []; for (let i = 0; i < newTxPerCoin.length; i++) { refreshCoins.push({ @@ -766,7 +766,7 @@ async function processDepositGroupPendingTrack( cancellationToken?: CancellationToken, ): Promise<TaskRunResult> { const { depositGroupId } = depositGroup; - for (let i = 0; i < depositGroup.depositedPerCoin.length; i++) { + for (let i = 0; i < depositGroup.statusPerCoin.length; i++) { const coinPub = depositGroup.payCoinSelection.coinPubs[i]; // FIXME: Make the URL part of the coin selection? const exchangeBaseUrl = await ws.db @@ -785,7 +785,7 @@ async function processDepositGroupPendingTrack( } | undefined; - if (depositGroup.transactionPerCoin[i] !== DepositElementStatus.Wired) { + if (depositGroup.statusPerCoin[i] !== DepositElementStatus.Wired) { const track = await trackDeposit( ws, depositGroup, @@ -810,7 +810,7 @@ async function processDepositGroupPendingTrack( exchangeBaseUrl, ); } else { - updatedTxStatus = DepositElementStatus.Accepted; + updatedTxStatus = DepositElementStatus.Tracking; } } else if (track.type === "wired") { updatedTxStatus = DepositElementStatus.Wired; @@ -840,7 +840,7 @@ async function processDepositGroupPendingTrack( id: track.exchange_sig, }; } else { - updatedTxStatus = DepositElementStatus.Unknown; + updatedTxStatus = DepositElementStatus.DepositPending; } } @@ -853,7 +853,7 @@ async function processDepositGroupPendingTrack( return; } if (updatedTxStatus !== undefined) { - dg.transactionPerCoin[i] = updatedTxStatus; + dg.statusPerCoin[i] = updatedTxStatus; } if (newWiredCoin) { /** @@ -885,8 +885,8 @@ async function processDepositGroupPendingTrack( return undefined; } const oldTxState = computeDepositTransactionStatus(dg); - for (let i = 0; i < depositGroup.depositedPerCoin.length; i++) { - if (depositGroup.transactionPerCoin[i] !== DepositElementStatus.Wired) { + for (let i = 0; i < depositGroup.statusPerCoin.length; i++) { + if (depositGroup.statusPerCoin[i] !== DepositElementStatus.Wired) { allWired = false; break; } @@ -943,7 +943,7 @@ async function processDepositGroupPendingDeposit( for (let i = 0; i < depositPermissions.length; i++) { const perm = depositPermissions[i]; - if (depositGroup.depositedPerCoin[i]) { + if (depositGroup.statusPerCoin[i] !== DepositElementStatus.DepositPending) { continue; } @@ -980,8 +980,12 @@ async function processDepositGroupPendingDeposit( if (!dg) { return; } - dg.depositedPerCoin[i] = true; - await tx.depositGroups.put(dg); + const coinStatus = dg.statusPerCoin[i]; + switch (coinStatus) { + case DepositElementStatus.DepositPending: + dg.statusPerCoin[i] = DepositElementStatus.Tracking; + await tx.depositGroups.put(dg); + } }); } @@ -1373,16 +1377,15 @@ export async function createDepositGroup( noncePub: noncePair.pub, timestampCreated: AbsoluteTime.toPreciseTimestamp(now), timestampFinished: undefined, - transactionPerCoin: payCoinSel.coinSel.coinPubs.map( - () => DepositElementStatus.Unknown, + statusPerCoin: payCoinSel.coinSel.coinPubs.map( + () => DepositElementStatus.DepositPending, ), payCoinSelection: payCoinSel.coinSel, payCoinSelectionUid: encodeCrock(getRandomBytes(32)), - depositedPerCoin: payCoinSel.coinSel.coinPubs.map(() => false), merchantPriv: merchantPair.priv, merchantPub: merchantPair.pub, totalPayCost: Amounts.stringify(totalDepositCost), - effectiveDepositAmount: Amounts.stringify( + counterpartyEffectiveDepositAmount: Amounts.stringify( counterpartyEffectiveDepositAmount, ), wire: { @@ -1536,7 +1539,7 @@ async function getTotalFeesForDepositAmount( .iter(coin.exchangeBaseUrl) .filter((x) => Amounts.isSameCurrency( - DenominationRecord.getValue(x), + x.value, pcs.coinContributions[i], ), ); diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts index 311a71a6e..43a08ed3b 100644 --- a/packages/taler-wallet-core/src/operations/exchanges.ts +++ b/packages/taler-wallet-core/src/operations/exchanges.ts @@ -158,35 +158,6 @@ getExchangeDetails.makeContext = (db: DbAccess<typeof WalletStoresV1>) => db.mktx((x) => [x.exchanges, x.exchangeDetails]); /** - * Update the database based on the download of the terms of service. - */ -export async function updateExchangeTermsOfService( - ws: InternalWalletState, - exchangeBaseUrl: string, - tos: ExchangeTosDownloadResult, -): Promise<void> { - await ws.db - .mktx((x) => [x.exchanges, x.exchangeTos, x.exchangeDetails]) - .runReadWrite(async (tx) => { - const d = await getExchangeDetails(tx, exchangeBaseUrl); - let tosRecord = await tx.exchangeTos.get([exchangeBaseUrl, tos.tosEtag]); - if (!tosRecord) { - tosRecord = { - etag: tos.tosEtag, - exchangeBaseUrl, - termsOfServiceContentType: tos.tosContentType, - termsOfServiceText: tos.tosText, - }; - await tx.exchangeTos.put(tosRecord); - } - if (d) { - d.tosCurrentEtag = tos.tosEtag; - await tx.exchangeDetails.put(d); - } - }); -} - -/** * Mark a ToS version as accepted by the user. * * @param etag version of the ToS to accept, or current ToS version of not given @@ -472,8 +443,7 @@ async function downloadExchangeKeysInfo( exchangeMasterPub: exchangeKeysJsonUnchecked.master_public_key, isOffered: true, isRevoked: false, - amountFrac: value.fraction, - amountVal: value.value, + value: Amounts.stringify(value), currency: value.currency, stampExpireDeposit: denomIn.stamp_expire_deposit, stampExpireLegal: denomIn.stamp_expire_legal, @@ -740,7 +710,6 @@ export async function updateExchangeFromUrlHandler( const updated = await ws.db .mktx((x) => [ x.exchanges, - x.exchangeTos, x.exchangeDetails, x.exchangeSignKeys, x.denominations, @@ -801,21 +770,6 @@ export async function updateExchangeFromUrlHandler( const drRowId = await tx.exchangeDetails.put(newDetails); checkDbInvariant(typeof drRowId.key === "number"); - let tosRecord = await tx.exchangeTos.get([ - exchangeBaseUrl, - tosDownload.tosEtag, - ]); - - if (!tosRecord || tosRecord.etag !== existingTosAccepted?.etag) { - tosRecord = { - etag: tosDownload.tosEtag, - exchangeBaseUrl, - termsOfServiceContentType: tosDownload.tosContentType, - termsOfServiceText: tosDownload.tosText, - }; - await tx.exchangeTos.put(tosRecord); - } - for (const sk of keysInfo.signingKeys) { // FIXME: validate signing keys before inserting them await tx.exchangeSignKeys.put({ @@ -972,54 +926,3 @@ export async function getExchangePaytoUri( )}`, ); } - -/** - * Check if and how an exchange is trusted and/or audited. - */ -export async function getExchangeTrust( - ws: InternalWalletState, - exchangeInfo: ExchangeEntryRecord, -): Promise<TrustInfo> { - let isTrusted = false; - let isAudited = false; - - return await ws.db - .mktx((x) => [ - x.exchanges, - x.exchangeDetails, - x.exchangeTrust, - x.auditorTrust, - ]) - .runReadOnly(async (tx) => { - const exchangeDetails = await getExchangeDetails( - tx, - exchangeInfo.baseUrl, - ); - - if (!exchangeDetails) { - throw Error(`exchange ${exchangeInfo.baseUrl} details not available`); - } - const exchangeTrustRecord = - await tx.exchangeTrust.indexes.byExchangeMasterPub.get( - exchangeDetails.masterPublicKey, - ); - if ( - exchangeTrustRecord && - exchangeTrustRecord.uids.length > 0 && - exchangeTrustRecord.currency === exchangeDetails.currency - ) { - isTrusted = true; - } - - for (const auditor of exchangeDetails.auditors) { - const auditorTrustRecord = - await tx.auditorTrust.indexes.byAuditorPub.get(auditor.auditor_pub); - if (auditorTrustRecord && auditorTrustRecord.uids.length > 0) { - isAudited = true; - break; - } - } - - return { isTrusted, isAudited }; - }); -} diff --git a/packages/taler-wallet-core/src/operations/pay-merchant.ts b/packages/taler-wallet-core/src/operations/pay-merchant.ts index 2580c97f5..57367bb20 100644 --- a/packages/taler-wallet-core/src/operations/pay-merchant.ts +++ b/packages/taler-wallet-core/src/operations/pay-merchant.ts @@ -174,12 +174,12 @@ export async function getTotalPaymentCost( .iter(coin.exchangeBaseUrl) .filter((x) => Amounts.isSameCurrency( - DenominationRecord.getValue(x), + x.value, pcs.coinContributions[i], ), ); const amountLeft = Amounts.sub( - DenominationRecord.getValue(denom), + denom.value, pcs.coinContributions[i], ).amount; const refreshCost = getTotalRefreshCost( @@ -317,11 +317,8 @@ export function extractContractData( wireInfoHash: parsedContractTerms.h_wire, maxDepositFee: Amounts.stringify(parsedContractTerms.max_fee), merchant: parsedContractTerms.merchant, - products: parsedContractTerms.products, summaryI18n: parsedContractTerms.summary_i18n, minimumAge: parsedContractTerms.minimum_age, - deliveryDate: parsedContractTerms.delivery_date, - deliveryLocation: parsedContractTerms.delivery_location, }; } @@ -541,7 +538,7 @@ async function processDownloadProposal( // if original order is refunded. if (otherPurchase && otherPurchase.refundAmountAwaiting === undefined) { logger.warn("repurchase detected"); - p.purchaseStatus = PurchaseStatus.RepurchaseDetected; + p.purchaseStatus = PurchaseStatus.DoneRepurchaseDetected; p.repurchaseProposalId = otherPurchase.proposalId; await tx.purchases.put(p); } else { @@ -974,7 +971,7 @@ export async function checkPaymentByProposalId( if (!proposal) { throw Error(`could not get proposal ${proposalId}`); } - if (proposal.purchaseStatus === PurchaseStatus.RepurchaseDetected) { + if (proposal.purchaseStatus === PurchaseStatus.DoneRepurchaseDetected) { const existingProposalId = proposal.repurchaseProposalId; if (existingProposalId) { logger.trace("using existing purchase for same product"); @@ -1527,7 +1524,7 @@ export async function processPurchase( return processPurchaseDialogShared(ws, purchase); case PurchaseStatus.FailedClaim: case PurchaseStatus.Done: - case PurchaseStatus.RepurchaseDetected: + case PurchaseStatus.DoneRepurchaseDetected: case PurchaseStatus.DialogProposed: case PurchaseStatus.AbortedProposalRefused: case PurchaseStatus.AbortedIncompletePayment: @@ -2099,7 +2096,7 @@ export function computePayMerchantTransactionState( return { major: TransactionMajorState.Done, }; - case PurchaseStatus.RepurchaseDetected: + case PurchaseStatus.DoneRepurchaseDetected: return { major: TransactionMajorState.Failed, minor: TransactionMinorState.Repurchase, @@ -2176,7 +2173,7 @@ export function computePayMerchantTransactionActions( return [TransactionAction.Delete]; case PurchaseStatus.Done: return [TransactionAction.Delete]; - case PurchaseStatus.RepurchaseDetected: + case PurchaseStatus.DoneRepurchaseDetected: return [TransactionAction.Delete]; case PurchaseStatus.AbortedIncompletePayment: return [TransactionAction.Delete]; diff --git a/packages/taler-wallet-core/src/operations/pay-peer-common.ts b/packages/taler-wallet-core/src/operations/pay-peer-common.ts index 9e05e43d8..6d425289d 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer-common.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer-common.ts @@ -108,16 +108,8 @@ export async function getTotalPeerPaymentCost( } const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl .iter(coin.exchangeBaseUrl) - .filter((x) => - Amounts.isSameCurrency( - DenominationRecord.getValue(x), - pcs[i].contribution, - ), - ); - const amountLeft = Amounts.sub( - DenominationRecord.getValue(denom), - pcs[i].contribution, - ).amount; + .filter((x) => Amounts.isSameCurrency(x.value, pcs[i].contribution)); + const amountLeft = Amounts.sub(denom.value, pcs[i].contribution).amount; const refreshCost = getTotalRefreshCost( allDenoms, DenominationRecord.toDenomInfo(denom), diff --git a/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts index 29c0fff9e..0355eb152 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts @@ -27,6 +27,7 @@ import { InitiatePeerPullCreditResponse, Logger, NotificationType, + PeerContractTerms, TalerErrorCode, TalerPreciseTimestamp, TalerProtocolTimestamp, @@ -55,8 +56,8 @@ import { import { KycPendingInfo, KycUserType, - PeerPullPaymentInitiationRecord, - PeerPullPaymentInitiationStatus, + PeerPullCreditRecord, + PeerPullPaymentCreditStatus, WithdrawalGroupStatus, WithdrawalRecordType, updateExchangeFromUrl, @@ -90,7 +91,7 @@ const logger = new Logger("pay-peer-pull-credit.ts"); async function queryPurseForPeerPullCredit( ws: InternalWalletState, - pullIni: PeerPullPaymentInitiationRecord, + pullIni: PeerPullCreditRecord, cancellationToken: CancellationToken, ): Promise<LongpollResult> { const purseDepositUrl = new URL( @@ -143,7 +144,6 @@ async function queryPurseForPeerPullCredit( amount: Amounts.parseOrThrow(pullIni.amount), wgInfo: { withdrawalType: WithdrawalRecordType.PeerPullCredit, - contractTerms: pullIni.contractTerms, contractPriv: pullIni.contractPriv, }, forcedWithdrawalGroupId: pullIni.withdrawalGroupId, @@ -159,18 +159,18 @@ async function queryPurseForPeerPullCredit( pursePub: pullIni.pursePub, }); const transitionInfo = await ws.db - .mktx((x) => [x.peerPullPaymentInitiations]) + .mktx((x) => [x.peerPullCredit]) .runReadWrite(async (tx) => { - const finPi = await tx.peerPullPaymentInitiations.get(pullIni.pursePub); + const finPi = await tx.peerPullCredit.get(pullIni.pursePub); if (!finPi) { - logger.warn("peerPullPaymentInitiation not found anymore"); + logger.warn("peerPullCredit not found anymore"); return; } const oldTxState = computePeerPullCreditTransactionState(finPi); - if (finPi.status === PeerPullPaymentInitiationStatus.PendingReady) { - finPi.status = PeerPullPaymentInitiationStatus.PendingWithdrawing; + if (finPi.status === PeerPullPaymentCreditStatus.PendingReady) { + finPi.status = PeerPullPaymentCreditStatus.PendingWithdrawing; } - await tx.peerPullPaymentInitiations.put(finPi); + await tx.peerPullCredit.put(finPi); const newTxState = computePeerPullCreditTransactionState(finPi); return { oldTxState, newTxState }; }); @@ -214,22 +214,22 @@ async function longpollKycStatus( kycStatusRes.status === HttpStatusCode.NoContent ) { const transitionInfo = await ws.db - .mktx((x) => [x.peerPullPaymentInitiations]) + .mktx((x) => [x.peerPullCredit]) .runReadWrite(async (tx) => { - const peerIni = await tx.peerPullPaymentInitiations.get(pursePub); + const peerIni = await tx.peerPullCredit.get(pursePub); if (!peerIni) { return; } if ( peerIni.status !== - PeerPullPaymentInitiationStatus.PendingMergeKycRequired + PeerPullPaymentCreditStatus.PendingMergeKycRequired ) { return; } const oldTxState = computePeerPullCreditTransactionState(peerIni); - peerIni.status = PeerPullPaymentInitiationStatus.PendingCreatePurse; + peerIni.status = PeerPullPaymentCreditStatus.PendingCreatePurse; const newTxState = computePeerPullCreditTransactionState(peerIni); - await tx.peerPullPaymentInitiations.put(peerIni); + await tx.peerPullCredit.put(peerIni); return { oldTxState, newTxState }; }); notifyTransition(ws, transactionId, transitionInfo); @@ -250,7 +250,7 @@ async function longpollKycStatus( async function processPeerPullCreditAbortingDeletePurse( ws: InternalWalletState, - peerPullIni: PeerPullPaymentInitiationRecord, + peerPullIni: PeerPullCreditRecord, ): Promise<TaskRunResult> { const { pursePub, pursePriv } = peerPullIni; const transactionId = constructTransactionIdentifier({ @@ -272,24 +272,22 @@ async function processPeerPullCreditAbortingDeletePurse( const transitionInfo = await ws.db .mktx((x) => [ - x.peerPullPaymentInitiations, + x.peerPullCredit, x.refreshGroups, x.denominations, x.coinAvailability, x.coins, ]) .runReadWrite(async (tx) => { - const ppiRec = await tx.peerPullPaymentInitiations.get(pursePub); + const ppiRec = await tx.peerPullCredit.get(pursePub); if (!ppiRec) { return undefined; } - if ( - ppiRec.status !== PeerPullPaymentInitiationStatus.AbortingDeletePurse - ) { + if (ppiRec.status !== PeerPullPaymentCreditStatus.AbortingDeletePurse) { return undefined; } const oldTxState = computePeerPullCreditTransactionState(ppiRec); - ppiRec.status = PeerPullPaymentInitiationStatus.Aborted; + ppiRec.status = PeerPullPaymentCreditStatus.Aborted; const newTxState = computePeerPullCreditTransactionState(ppiRec); return { oldTxState, @@ -303,7 +301,7 @@ async function processPeerPullCreditAbortingDeletePurse( async function handlePeerPullCreditWithdrawing( ws: InternalWalletState, - pullIni: PeerPullPaymentInitiationRecord, + pullIni: PeerPullCreditRecord, ): Promise<TaskRunResult> { if (!pullIni.withdrawalGroupId) { throw Error("invalid db state (withdrawing, but no withdrawal group ID"); @@ -315,14 +313,14 @@ async function handlePeerPullCreditWithdrawing( const wgId = pullIni.withdrawalGroupId; let finished: boolean = false; const transitionInfo = await ws.db - .mktx((x) => [x.peerPullPaymentInitiations, x.withdrawalGroups]) + .mktx((x) => [x.peerPullCredit, x.withdrawalGroups]) .runReadWrite(async (tx) => { - const ppi = await tx.peerPullPaymentInitiations.get(pullIni.pursePub); + const ppi = await tx.peerPullCredit.get(pullIni.pursePub); if (!ppi) { finished = true; return; } - if (ppi.status !== PeerPullPaymentInitiationStatus.PendingWithdrawing) { + if (ppi.status !== PeerPullPaymentCreditStatus.PendingWithdrawing) { finished = true; return; } @@ -333,13 +331,13 @@ async function handlePeerPullCreditWithdrawing( return undefined; } switch (wg.status) { - case WithdrawalGroupStatus.Finished: + case WithdrawalGroupStatus.Done: finished = true; - ppi.status = PeerPullPaymentInitiationStatus.Done; + ppi.status = PeerPullPaymentCreditStatus.Done; break; // FIXME: Also handle other final states! } - await tx.peerPullPaymentInitiations.put(ppi); + await tx.peerPullCredit.put(ppi); const newTxState = computePeerPullCreditTransactionState(ppi); return { oldTxState, @@ -357,7 +355,7 @@ async function handlePeerPullCreditWithdrawing( async function handlePeerPullCreditCreatePurse( ws: InternalWalletState, - pullIni: PeerPullPaymentInitiationRecord, + pullIni: PeerPullCreditRecord, ): Promise<TaskRunResult> { const purseFee = Amounts.stringify(Amounts.zeroOfAmount(pullIni.amount)); const pursePub = pullIni.pursePub; @@ -371,6 +369,18 @@ async function handlePeerPullCreditCreatePurse( throw Error("merge reserve for peer pull payment not found in database"); } + const contractTermsRecord = await ws.db + .mktx((x) => [x.contractTerms]) + .runReadOnly(async (tx) => { + return tx.contractTerms.get(pullIni.contractTermsHash); + }); + + if (!contractTermsRecord) { + throw Error("contract terms for peer pull payment not found in database"); + } + + const contractTerms: PeerContractTerms = contractTermsRecord.contractTermsRaw; + const reservePayto = talerPaytoFromExchangeReserve( pullIni.exchangeBaseUrl, mergeReserve.reservePub, @@ -379,19 +389,19 @@ async function handlePeerPullCreditCreatePurse( const econtractResp = await ws.cryptoApi.encryptContractForDeposit({ contractPriv: pullIni.contractPriv, contractPub: pullIni.contractPub, - contractTerms: pullIni.contractTerms, + contractTerms: contractTermsRecord, pursePriv: pullIni.pursePriv, pursePub: pullIni.pursePub, nonce: pullIni.contractEncNonce, }); - const purseExpiration = pullIni.contractTerms.purse_expiration; + const purseExpiration = contractTerms.purse_expiration; const sigRes = await ws.cryptoApi.signReservePurseCreate({ contractTermsHash: pullIni.contractTermsHash, flags: WalletAccountMergeFlags.CreateWithPurseFee, mergePriv: pullIni.mergePriv, mergeTimestamp: TalerPreciseTimestamp.round(pullIni.mergeTimestamp), - purseAmount: pullIni.contractTerms.amount, + purseAmount: pullIni.amount, purseExpiration: purseExpiration, purseFee: purseFee, pursePriv: pullIni.pursePriv, @@ -410,7 +420,7 @@ async function handlePeerPullCreditCreatePurse( purse_fee: purseFee, purse_pub: pullIni.pursePub, purse_sig: sigRes.purseSig, - purse_value: pullIni.contractTerms.amount, + purse_value: pullIni.amount, reserve_sig: sigRes.accountSig, econtract: econtractResp.econtract, }; @@ -444,15 +454,15 @@ async function handlePeerPullCreditCreatePurse( }); const transitionInfo = await ws.db - .mktx((x) => [x.peerPullPaymentInitiations]) + .mktx((x) => [x.peerPullCredit]) .runReadWrite(async (tx) => { - const pi2 = await tx.peerPullPaymentInitiations.get(pursePub); + const pi2 = await tx.peerPullCredit.get(pursePub); if (!pi2) { return; } const oldTxState = computePeerPullCreditTransactionState(pi2); - pi2.status = PeerPullPaymentInitiationStatus.PendingReady; - await tx.peerPullPaymentInitiations.put(pi2); + pi2.status = PeerPullPaymentCreditStatus.PendingReady; + await tx.peerPullCredit.put(pi2); const newTxState = computePeerPullCreditTransactionState(pi2); return { oldTxState, newTxState }; }); @@ -466,9 +476,9 @@ export async function processPeerPullCredit( pursePub: string, ): Promise<TaskRunResult> { const pullIni = await ws.db - .mktx((x) => [x.peerPullPaymentInitiations]) + .mktx((x) => [x.peerPullCredit]) .runReadOnly(async (tx) => { - return tx.peerPullPaymentInitiations.get(pursePub); + return tx.peerPullCredit.get(pursePub); }); if (!pullIni) { throw Error("peer pull payment initiation not found in database"); @@ -490,10 +500,10 @@ export async function processPeerPullCredit( logger.trace(`processing ${retryTag}, status=${pullIni.status}`); switch (pullIni.status) { - case PeerPullPaymentInitiationStatus.Done: { + case PeerPullPaymentCreditStatus.Done: { return TaskRunResult.finished(); } - case PeerPullPaymentInitiationStatus.PendingReady: + case PeerPullPaymentCreditStatus.PendingReady: runLongpollAsync(ws, retryTag, async (cancellationToken) => queryPurseForPeerPullCredit(ws, pullIni, cancellationToken), ); @@ -503,7 +513,7 @@ export async function processPeerPullCredit( return { type: TaskRunResultType.Longpoll, }; - case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: { + case PeerPullPaymentCreditStatus.PendingMergeKycRequired: { if (!pullIni.kycInfo) { throw Error("invalid state, kycInfo required"); } @@ -515,19 +525,19 @@ export async function processPeerPullCredit( "individual", ); } - case PeerPullPaymentInitiationStatus.PendingCreatePurse: + case PeerPullPaymentCreditStatus.PendingCreatePurse: return handlePeerPullCreditCreatePurse(ws, pullIni); - case PeerPullPaymentInitiationStatus.AbortingDeletePurse: + case PeerPullPaymentCreditStatus.AbortingDeletePurse: return await processPeerPullCreditAbortingDeletePurse(ws, pullIni); - case PeerPullPaymentInitiationStatus.PendingWithdrawing: + case PeerPullPaymentCreditStatus.PendingWithdrawing: return handlePeerPullCreditWithdrawing(ws, pullIni); - case PeerPullPaymentInitiationStatus.Aborted: - case PeerPullPaymentInitiationStatus.Failed: - case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse: - case PeerPullPaymentInitiationStatus.SuspendedCreatePurse: - case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired: - case PeerPullPaymentInitiationStatus.SuspendedReady: - case PeerPullPaymentInitiationStatus.SuspendedWithdrawing: + case PeerPullPaymentCreditStatus.Aborted: + case PeerPullPaymentCreditStatus.Failed: + case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse: + case PeerPullPaymentCreditStatus.SuspendedCreatePurse: + case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired: + case PeerPullPaymentCreditStatus.SuspendedReady: + case PeerPullPaymentCreditStatus.SuspendedWithdrawing: break; default: assertUnreachable(pullIni.status); @@ -538,7 +548,7 @@ export async function processPeerPullCredit( async function processPeerPullCreditKycRequired( ws: InternalWalletState, - peerIni: PeerPullPaymentInitiationRecord, + peerIni: PeerPullCreditRecord, kycPending: WalletKycUuid, ): Promise<TaskRunResult> { const transactionId = constructTransactionIdentifier({ @@ -570,9 +580,9 @@ async function processPeerPullCreditKycRequired( const kycStatus = await kycStatusRes.json(); logger.info(`kyc status: ${j2s(kycStatus)}`); const { transitionInfo, result } = await ws.db - .mktx((x) => [x.peerPullPaymentInitiations]) + .mktx((x) => [x.peerPullCredit]) .runReadWrite(async (tx) => { - const peerInc = await tx.peerPullPaymentInitiations.get(pursePub); + const peerInc = await tx.peerPullCredit.get(pursePub); if (!peerInc) { return { transitionInfo: undefined, @@ -585,10 +595,9 @@ async function processPeerPullCreditKycRequired( requirementRow: kycPending.requirement_row, }; peerInc.kycUrl = kycStatus.kyc_url; - peerInc.status = - PeerPullPaymentInitiationStatus.PendingMergeKycRequired; + peerInc.status = PeerPullPaymentCreditStatus.PendingMergeKycRequired; const newTxState = computePeerPullCreditTransactionState(peerInc); - await tx.peerPullPaymentInitiations.put(peerInc); + await tx.peerPullCredit.put(peerInc); // We'll remove this eventually! New clients should rely on the // kycUrl field of the transaction, not the error code. const res: TaskRunResult = { @@ -758,9 +767,9 @@ export async function initiatePeerPullPayment( ); const transitionInfo = await ws.db - .mktx((x) => [x.peerPullPaymentInitiations, x.contractTerms]) + .mktx((x) => [x.peerPullCredit, x.contractTerms]) .runReadWrite(async (tx) => { - const ppi: PeerPullPaymentInitiationRecord = { + const ppi: PeerPullCreditRecord = { amount: req.partialContractTerms.amount, contractTermsHash: hContractTerms, exchangeBaseUrl: exchangeBaseUrl, @@ -768,8 +777,7 @@ export async function initiatePeerPullPayment( pursePub: pursePair.pub, mergePriv: mergePair.priv, mergePub: mergePair.pub, - status: PeerPullPaymentInitiationStatus.PendingCreatePurse, - contractTerms: contractTerms, + status: PeerPullPaymentCreditStatus.PendingCreatePurse, mergeTimestamp, contractEncNonce, mergeReserveRowId: mergeReserveRowId, @@ -778,7 +786,7 @@ export async function initiatePeerPullPayment( withdrawalGroupId, estimatedAmountEffective: wi.withdrawalAmountEffective, }; - await tx.peerPullPaymentInitiations.put(ppi); + await tx.peerPullCredit.put(ppi); const oldTxState: TransactionState = { major: TransactionMajorState.None, }; @@ -826,39 +834,38 @@ export async function suspendPeerPullCreditTransaction( }); stopLongpolling(ws, taskId); const transitionInfo = await ws.db - .mktx((x) => [x.peerPullPaymentInitiations]) + .mktx((x) => [x.peerPullCredit]) .runReadWrite(async (tx) => { - const pullCreditRec = await tx.peerPullPaymentInitiations.get(pursePub); + const pullCreditRec = await tx.peerPullCredit.get(pursePub); if (!pullCreditRec) { logger.warn(`peer pull credit ${pursePub} not found`); return; } - let newStatus: PeerPullPaymentInitiationStatus | undefined = undefined; + let newStatus: PeerPullPaymentCreditStatus | undefined = undefined; switch (pullCreditRec.status) { - case PeerPullPaymentInitiationStatus.PendingCreatePurse: - newStatus = PeerPullPaymentInitiationStatus.SuspendedCreatePurse; + case PeerPullPaymentCreditStatus.PendingCreatePurse: + newStatus = PeerPullPaymentCreditStatus.SuspendedCreatePurse; break; - case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: - newStatus = PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired; + case PeerPullPaymentCreditStatus.PendingMergeKycRequired: + newStatus = PeerPullPaymentCreditStatus.SuspendedMergeKycRequired; break; - case PeerPullPaymentInitiationStatus.PendingWithdrawing: - newStatus = PeerPullPaymentInitiationStatus.SuspendedWithdrawing; + case PeerPullPaymentCreditStatus.PendingWithdrawing: + newStatus = PeerPullPaymentCreditStatus.SuspendedWithdrawing; break; - case PeerPullPaymentInitiationStatus.PendingReady: - newStatus = PeerPullPaymentInitiationStatus.SuspendedReady; + case PeerPullPaymentCreditStatus.PendingReady: + newStatus = PeerPullPaymentCreditStatus.SuspendedReady; break; - case PeerPullPaymentInitiationStatus.AbortingDeletePurse: - newStatus = - PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse; + case PeerPullPaymentCreditStatus.AbortingDeletePurse: + newStatus = PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse; break; - case PeerPullPaymentInitiationStatus.Done: - case PeerPullPaymentInitiationStatus.SuspendedCreatePurse: - case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired: - case PeerPullPaymentInitiationStatus.SuspendedReady: - case PeerPullPaymentInitiationStatus.SuspendedWithdrawing: - case PeerPullPaymentInitiationStatus.Aborted: - case PeerPullPaymentInitiationStatus.Failed: - case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse: + case PeerPullPaymentCreditStatus.Done: + case PeerPullPaymentCreditStatus.SuspendedCreatePurse: + case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired: + case PeerPullPaymentCreditStatus.SuspendedReady: + case PeerPullPaymentCreditStatus.SuspendedWithdrawing: + case PeerPullPaymentCreditStatus.Aborted: + case PeerPullPaymentCreditStatus.Failed: + case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse: break; default: assertUnreachable(pullCreditRec.status); @@ -867,7 +874,7 @@ export async function suspendPeerPullCreditTransaction( const oldTxState = computePeerPullCreditTransactionState(pullCreditRec); pullCreditRec.status = newStatus; const newTxState = computePeerPullCreditTransactionState(pullCreditRec); - await tx.peerPullPaymentInitiations.put(pullCreditRec); + await tx.peerPullCredit.put(pullCreditRec); return { oldTxState, newTxState, @@ -892,33 +899,33 @@ export async function abortPeerPullCreditTransaction( }); stopLongpolling(ws, taskId); const transitionInfo = await ws.db - .mktx((x) => [x.peerPullPaymentInitiations]) + .mktx((x) => [x.peerPullCredit]) .runReadWrite(async (tx) => { - const pullCreditRec = await tx.peerPullPaymentInitiations.get(pursePub); + const pullCreditRec = await tx.peerPullCredit.get(pursePub); if (!pullCreditRec) { logger.warn(`peer pull credit ${pursePub} not found`); return; } - let newStatus: PeerPullPaymentInitiationStatus | undefined = undefined; + let newStatus: PeerPullPaymentCreditStatus | undefined = undefined; switch (pullCreditRec.status) { - case PeerPullPaymentInitiationStatus.PendingCreatePurse: - case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: - newStatus = PeerPullPaymentInitiationStatus.AbortingDeletePurse; + case PeerPullPaymentCreditStatus.PendingCreatePurse: + case PeerPullPaymentCreditStatus.PendingMergeKycRequired: + newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse; break; - case PeerPullPaymentInitiationStatus.PendingWithdrawing: + case PeerPullPaymentCreditStatus.PendingWithdrawing: throw Error("can't abort anymore"); - case PeerPullPaymentInitiationStatus.PendingReady: - newStatus = PeerPullPaymentInitiationStatus.AbortingDeletePurse; + case PeerPullPaymentCreditStatus.PendingReady: + newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse; break; - case PeerPullPaymentInitiationStatus.Done: - case PeerPullPaymentInitiationStatus.SuspendedCreatePurse: - case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired: - case PeerPullPaymentInitiationStatus.SuspendedReady: - case PeerPullPaymentInitiationStatus.SuspendedWithdrawing: - case PeerPullPaymentInitiationStatus.Aborted: - case PeerPullPaymentInitiationStatus.AbortingDeletePurse: - case PeerPullPaymentInitiationStatus.Failed: - case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse: + case PeerPullPaymentCreditStatus.Done: + case PeerPullPaymentCreditStatus.SuspendedCreatePurse: + case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired: + case PeerPullPaymentCreditStatus.SuspendedReady: + case PeerPullPaymentCreditStatus.SuspendedWithdrawing: + case PeerPullPaymentCreditStatus.Aborted: + case PeerPullPaymentCreditStatus.AbortingDeletePurse: + case PeerPullPaymentCreditStatus.Failed: + case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse: break; default: assertUnreachable(pullCreditRec.status); @@ -927,7 +934,7 @@ export async function abortPeerPullCreditTransaction( const oldTxState = computePeerPullCreditTransactionState(pullCreditRec); pullCreditRec.status = newStatus; const newTxState = computePeerPullCreditTransactionState(pullCreditRec); - await tx.peerPullPaymentInitiations.put(pullCreditRec); + await tx.peerPullCredit.put(pullCreditRec); return { oldTxState, newTxState, @@ -952,30 +959,30 @@ export async function failPeerPullCreditTransaction( }); stopLongpolling(ws, taskId); const transitionInfo = await ws.db - .mktx((x) => [x.peerPullPaymentInitiations]) + .mktx((x) => [x.peerPullCredit]) .runReadWrite(async (tx) => { - const pullCreditRec = await tx.peerPullPaymentInitiations.get(pursePub); + const pullCreditRec = await tx.peerPullCredit.get(pursePub); if (!pullCreditRec) { logger.warn(`peer pull credit ${pursePub} not found`); return; } - let newStatus: PeerPullPaymentInitiationStatus | undefined = undefined; + let newStatus: PeerPullPaymentCreditStatus | undefined = undefined; switch (pullCreditRec.status) { - case PeerPullPaymentInitiationStatus.PendingCreatePurse: - case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: - case PeerPullPaymentInitiationStatus.PendingWithdrawing: - case PeerPullPaymentInitiationStatus.PendingReady: - case PeerPullPaymentInitiationStatus.Done: - case PeerPullPaymentInitiationStatus.SuspendedCreatePurse: - case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired: - case PeerPullPaymentInitiationStatus.SuspendedReady: - case PeerPullPaymentInitiationStatus.SuspendedWithdrawing: - case PeerPullPaymentInitiationStatus.Aborted: - case PeerPullPaymentInitiationStatus.Failed: + case PeerPullPaymentCreditStatus.PendingCreatePurse: + case PeerPullPaymentCreditStatus.PendingMergeKycRequired: + case PeerPullPaymentCreditStatus.PendingWithdrawing: + case PeerPullPaymentCreditStatus.PendingReady: + case PeerPullPaymentCreditStatus.Done: + case PeerPullPaymentCreditStatus.SuspendedCreatePurse: + case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired: + case PeerPullPaymentCreditStatus.SuspendedReady: + case PeerPullPaymentCreditStatus.SuspendedWithdrawing: + case PeerPullPaymentCreditStatus.Aborted: + case PeerPullPaymentCreditStatus.Failed: break; - case PeerPullPaymentInitiationStatus.AbortingDeletePurse: - case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse: - newStatus = PeerPullPaymentInitiationStatus.Failed; + case PeerPullPaymentCreditStatus.AbortingDeletePurse: + case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse: + newStatus = PeerPullPaymentCreditStatus.Failed; break; default: assertUnreachable(pullCreditRec.status); @@ -984,7 +991,7 @@ export async function failPeerPullCreditTransaction( const oldTxState = computePeerPullCreditTransactionState(pullCreditRec); pullCreditRec.status = newStatus; const newTxState = computePeerPullCreditTransactionState(pullCreditRec); - await tx.peerPullPaymentInitiations.put(pullCreditRec); + await tx.peerPullCredit.put(pullCreditRec); return { oldTxState, newTxState, @@ -1009,38 +1016,38 @@ export async function resumePeerPullCreditTransaction( }); stopLongpolling(ws, taskId); const transitionInfo = await ws.db - .mktx((x) => [x.peerPullPaymentInitiations]) + .mktx((x) => [x.peerPullCredit]) .runReadWrite(async (tx) => { - const pullCreditRec = await tx.peerPullPaymentInitiations.get(pursePub); + const pullCreditRec = await tx.peerPullCredit.get(pursePub); if (!pullCreditRec) { logger.warn(`peer pull credit ${pursePub} not found`); return; } - let newStatus: PeerPullPaymentInitiationStatus | undefined = undefined; + let newStatus: PeerPullPaymentCreditStatus | undefined = undefined; switch (pullCreditRec.status) { - case PeerPullPaymentInitiationStatus.PendingCreatePurse: - case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: - case PeerPullPaymentInitiationStatus.PendingWithdrawing: - case PeerPullPaymentInitiationStatus.PendingReady: - case PeerPullPaymentInitiationStatus.AbortingDeletePurse: - case PeerPullPaymentInitiationStatus.Done: - case PeerPullPaymentInitiationStatus.Failed: - case PeerPullPaymentInitiationStatus.Aborted: + case PeerPullPaymentCreditStatus.PendingCreatePurse: + case PeerPullPaymentCreditStatus.PendingMergeKycRequired: + case PeerPullPaymentCreditStatus.PendingWithdrawing: + case PeerPullPaymentCreditStatus.PendingReady: + case PeerPullPaymentCreditStatus.AbortingDeletePurse: + case PeerPullPaymentCreditStatus.Done: + case PeerPullPaymentCreditStatus.Failed: + case PeerPullPaymentCreditStatus.Aborted: break; - case PeerPullPaymentInitiationStatus.SuspendedCreatePurse: - newStatus = PeerPullPaymentInitiationStatus.PendingCreatePurse; + case PeerPullPaymentCreditStatus.SuspendedCreatePurse: + newStatus = PeerPullPaymentCreditStatus.PendingCreatePurse; break; - case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired: - newStatus = PeerPullPaymentInitiationStatus.PendingMergeKycRequired; + case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired: + newStatus = PeerPullPaymentCreditStatus.PendingMergeKycRequired; break; - case PeerPullPaymentInitiationStatus.SuspendedReady: - newStatus = PeerPullPaymentInitiationStatus.PendingReady; + case PeerPullPaymentCreditStatus.SuspendedReady: + newStatus = PeerPullPaymentCreditStatus.PendingReady; break; - case PeerPullPaymentInitiationStatus.SuspendedWithdrawing: - newStatus = PeerPullPaymentInitiationStatus.PendingWithdrawing; + case PeerPullPaymentCreditStatus.SuspendedWithdrawing: + newStatus = PeerPullPaymentCreditStatus.PendingWithdrawing; break; - case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse: - newStatus = PeerPullPaymentInitiationStatus.AbortingDeletePurse; + case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse: + newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse; break; default: assertUnreachable(pullCreditRec.status); @@ -1049,7 +1056,7 @@ export async function resumePeerPullCreditTransaction( const oldTxState = computePeerPullCreditTransactionState(pullCreditRec); pullCreditRec.status = newStatus; const newTxState = computePeerPullCreditTransactionState(pullCreditRec); - await tx.peerPullPaymentInitiations.put(pullCreditRec); + await tx.peerPullCredit.put(pullCreditRec); return { oldTxState, newTxState, @@ -1062,67 +1069,67 @@ export async function resumePeerPullCreditTransaction( } export function computePeerPullCreditTransactionState( - pullCreditRecord: PeerPullPaymentInitiationRecord, + pullCreditRecord: PeerPullCreditRecord, ): TransactionState { switch (pullCreditRecord.status) { - case PeerPullPaymentInitiationStatus.PendingCreatePurse: + case PeerPullPaymentCreditStatus.PendingCreatePurse: return { major: TransactionMajorState.Pending, minor: TransactionMinorState.CreatePurse, }; - case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: + case PeerPullPaymentCreditStatus.PendingMergeKycRequired: return { major: TransactionMajorState.Pending, minor: TransactionMinorState.MergeKycRequired, }; - case PeerPullPaymentInitiationStatus.PendingReady: + case PeerPullPaymentCreditStatus.PendingReady: return { major: TransactionMajorState.Pending, minor: TransactionMinorState.Ready, }; - case PeerPullPaymentInitiationStatus.Done: + case PeerPullPaymentCreditStatus.Done: return { major: TransactionMajorState.Done, }; - case PeerPullPaymentInitiationStatus.PendingWithdrawing: + case PeerPullPaymentCreditStatus.PendingWithdrawing: return { major: TransactionMajorState.Pending, minor: TransactionMinorState.Withdraw, }; - case PeerPullPaymentInitiationStatus.SuspendedCreatePurse: + case PeerPullPaymentCreditStatus.SuspendedCreatePurse: return { major: TransactionMajorState.Suspended, minor: TransactionMinorState.CreatePurse, }; - case PeerPullPaymentInitiationStatus.SuspendedReady: + case PeerPullPaymentCreditStatus.SuspendedReady: return { major: TransactionMajorState.Suspended, minor: TransactionMinorState.Ready, }; - case PeerPullPaymentInitiationStatus.SuspendedWithdrawing: + case PeerPullPaymentCreditStatus.SuspendedWithdrawing: return { major: TransactionMajorState.Pending, minor: TransactionMinorState.Withdraw, }; - case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired: + case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired: return { major: TransactionMajorState.Suspended, minor: TransactionMinorState.MergeKycRequired, }; - case PeerPullPaymentInitiationStatus.Aborted: + case PeerPullPaymentCreditStatus.Aborted: return { major: TransactionMajorState.Aborted, }; - case PeerPullPaymentInitiationStatus.AbortingDeletePurse: + case PeerPullPaymentCreditStatus.AbortingDeletePurse: return { major: TransactionMajorState.Aborting, minor: TransactionMinorState.DeletePurse, }; - case PeerPullPaymentInitiationStatus.Failed: + case PeerPullPaymentCreditStatus.Failed: return { major: TransactionMajorState.Failed, }; - case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse: + case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse: return { major: TransactionMajorState.Aborting, minor: TransactionMinorState.DeletePurse, @@ -1131,34 +1138,34 @@ export function computePeerPullCreditTransactionState( } export function computePeerPullCreditTransactionActions( - pullCreditRecord: PeerPullPaymentInitiationRecord, + pullCreditRecord: PeerPullCreditRecord, ): TransactionAction[] { switch (pullCreditRecord.status) { - case PeerPullPaymentInitiationStatus.PendingCreatePurse: + case PeerPullPaymentCreditStatus.PendingCreatePurse: return [TransactionAction.Abort, TransactionAction.Suspend]; - case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: + case PeerPullPaymentCreditStatus.PendingMergeKycRequired: return [TransactionAction.Abort, TransactionAction.Suspend]; - case PeerPullPaymentInitiationStatus.PendingReady: + case PeerPullPaymentCreditStatus.PendingReady: return [TransactionAction.Abort, TransactionAction.Suspend]; - case PeerPullPaymentInitiationStatus.Done: + case PeerPullPaymentCreditStatus.Done: return [TransactionAction.Delete]; - case PeerPullPaymentInitiationStatus.PendingWithdrawing: + case PeerPullPaymentCreditStatus.PendingWithdrawing: return [TransactionAction.Abort, TransactionAction.Suspend]; - case PeerPullPaymentInitiationStatus.SuspendedCreatePurse: + case PeerPullPaymentCreditStatus.SuspendedCreatePurse: return [TransactionAction.Resume, TransactionAction.Abort]; - case PeerPullPaymentInitiationStatus.SuspendedReady: + case PeerPullPaymentCreditStatus.SuspendedReady: return [TransactionAction.Abort, TransactionAction.Resume]; - case PeerPullPaymentInitiationStatus.SuspendedWithdrawing: + case PeerPullPaymentCreditStatus.SuspendedWithdrawing: return [TransactionAction.Resume, TransactionAction.Fail]; - case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired: + case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired: return [TransactionAction.Resume, TransactionAction.Fail]; - case PeerPullPaymentInitiationStatus.Aborted: + case PeerPullPaymentCreditStatus.Aborted: return [TransactionAction.Delete]; - case PeerPullPaymentInitiationStatus.AbortingDeletePurse: + case PeerPullPaymentCreditStatus.AbortingDeletePurse: return [TransactionAction.Suspend, TransactionAction.Fail]; - case PeerPullPaymentInitiationStatus.Failed: + case PeerPullPaymentCreditStatus.Failed: return [TransactionAction.Delete]; - case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse: + case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse: return [TransactionAction.Resume, TransactionAction.Fail]; } } diff --git a/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts index 0de91bf97..f357c41d5 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts @@ -140,10 +140,10 @@ async function handlePurseCreationConflict( ); await ws.db - .mktx((x) => [x.peerPullPaymentIncoming]) + .mktx((x) => [x.peerPullDebit]) .runReadWrite(async (tx) => { - const myPpi = await tx.peerPullPaymentIncoming.get( - peerPullInc.peerPullPaymentIncomingId, + const myPpi = await tx.peerPullDebit.get( + peerPullInc.peerPullDebitId, ); if (!myPpi) { return; @@ -162,7 +162,7 @@ async function handlePurseCreationConflict( default: return; } - await tx.peerPullPaymentIncoming.put(myPpi); + await tx.peerPullDebit.put(myPpi); }); return TaskRunResult.finished(); } @@ -171,7 +171,7 @@ async function processPeerPullDebitPendingDeposit( ws: InternalWalletState, peerPullInc: PeerPullPaymentIncomingRecord, ): Promise<TaskRunResult> { - const peerPullPaymentIncomingId = peerPullInc.peerPullPaymentIncomingId; + const peerPullDebitId = peerPullInc.peerPullDebitId; const pursePub = peerPullInc.pursePub; const coinSel = peerPullInc.coinSel; @@ -202,7 +202,7 @@ async function processPeerPullDebitPendingDeposit( const transactionId = constructTransactionIdentifier({ tag: TransactionType.PeerPullDebit, - peerPullPaymentIncomingId, + peerPullDebitId, }); const httpResp = await ws.http.fetch(purseDepositUrl.href, { @@ -218,10 +218,10 @@ async function processPeerPullDebitPendingDeposit( logger.trace(`purse deposit response: ${j2s(resp)}`); const transitionInfo = await ws.db - .mktx((x) => [x.peerPullPaymentIncoming]) + .mktx((x) => [x.peerPullDebit]) .runReadWrite(async (tx) => { - const pi = await tx.peerPullPaymentIncoming.get( - peerPullPaymentIncomingId, + const pi = await tx.peerPullDebit.get( + peerPullDebitId, ); if (!pi) { throw Error("peer pull payment not found anymore"); @@ -230,9 +230,9 @@ async function processPeerPullDebitPendingDeposit( return; } const oldTxState = computePeerPullDebitTransactionState(pi); - pi.status = PeerPullDebitRecordStatus.DonePaid; + pi.status = PeerPullDebitRecordStatus.Done; const newTxState = computePeerPullDebitTransactionState(pi); - await tx.peerPullPaymentIncoming.put(pi); + await tx.peerPullDebit.put(pi); return { oldTxState, newTxState }; }); notifyTransition(ws, transactionId, transitionInfo); @@ -241,15 +241,15 @@ async function processPeerPullDebitPendingDeposit( case HttpStatusCode.Gone: { const transitionInfo = await ws.db .mktx((x) => [ - x.peerPullPaymentIncoming, + x.peerPullDebit, x.refreshGroups, x.denominations, x.coinAvailability, x.coins, ]) .runReadWrite(async (tx) => { - const pi = await tx.peerPullPaymentIncoming.get( - peerPullPaymentIncomingId, + const pi = await tx.peerPullDebit.get( + peerPullDebitId, ); if (!pi) { throw Error("peer pull payment not found anymore"); @@ -284,7 +284,7 @@ async function processPeerPullDebitPendingDeposit( pi.status = PeerPullDebitRecordStatus.AbortingRefresh; pi.abortRefreshGroupId = refresh.refreshGroupId; const newTxState = computePeerPullDebitTransactionState(pi); - await tx.peerPullPaymentIncoming.put(pi); + await tx.peerPullDebit.put(pi); return { oldTxState, newTxState }; }); notifyTransition(ws, transactionId, transitionInfo); @@ -308,15 +308,15 @@ async function processPeerPullDebitAbortingRefresh( ws: InternalWalletState, peerPullInc: PeerPullPaymentIncomingRecord, ): Promise<TaskRunResult> { - const peerPullPaymentIncomingId = peerPullInc.peerPullPaymentIncomingId; + const peerPullDebitId = peerPullInc.peerPullDebitId; const abortRefreshGroupId = peerPullInc.abortRefreshGroupId; checkLogicInvariant(!!abortRefreshGroupId); const transactionId = constructTransactionIdentifier({ tag: TransactionType.PeerPullDebit, - peerPullPaymentIncomingId, + peerPullDebitId, }); const transitionInfo = await ws.db - .mktx((x) => [x.refreshGroups, x.peerPullPaymentIncoming]) + .mktx((x) => [x.refreshGroups, x.peerPullDebit]) .runReadWrite(async (tx) => { const refreshGroup = await tx.refreshGroups.get(abortRefreshGroupId); let newOpState: PeerPullDebitRecordStatus | undefined; @@ -335,8 +335,8 @@ async function processPeerPullDebitAbortingRefresh( } } if (newOpState) { - const newDg = await tx.peerPullPaymentIncoming.get( - peerPullPaymentIncomingId, + const newDg = await tx.peerPullDebit.get( + peerPullDebitId, ); if (!newDg) { return; @@ -344,7 +344,7 @@ async function processPeerPullDebitAbortingRefresh( const oldTxState = computePeerPullDebitTransactionState(newDg); newDg.status = newOpState; const newTxState = computePeerPullDebitTransactionState(newDg); - await tx.peerPullPaymentIncoming.put(newDg); + await tx.peerPullDebit.put(newDg); return { oldTxState, newTxState }; } return undefined; @@ -356,12 +356,12 @@ async function processPeerPullDebitAbortingRefresh( export async function processPeerPullDebit( ws: InternalWalletState, - peerPullPaymentIncomingId: string, + peerPullDebitId: string, ): Promise<TaskRunResult> { const peerPullInc = await ws.db - .mktx((x) => [x.peerPullPaymentIncoming]) + .mktx((x) => [x.peerPullDebit]) .runReadOnly(async (tx) => { - return tx.peerPullPaymentIncoming.get(peerPullPaymentIncomingId); + return tx.peerPullDebit.get(peerPullDebitId); }); if (!peerPullInc) { throw Error("peer pull debit not found"); @@ -380,31 +380,31 @@ export async function confirmPeerPullDebit( ws: InternalWalletState, req: ConfirmPeerPullDebitRequest, ): Promise<AcceptPeerPullPaymentResponse> { - let peerPullPaymentIncomingId: string; + let peerPullDebitId: string; if (req.transactionId) { const parsedTx = parseTransactionIdentifier(req.transactionId); if (!parsedTx || parsedTx.tag !== TransactionType.PeerPullDebit) { throw Error("invalid peer-pull-debit transaction identifier"); } - peerPullPaymentIncomingId = parsedTx.peerPullPaymentIncomingId; - } else if (req.peerPullPaymentIncomingId) { - peerPullPaymentIncomingId = req.peerPullPaymentIncomingId; + peerPullDebitId = parsedTx.peerPullDebitId; + } else if (req.peerPullDebitId) { + peerPullDebitId = req.peerPullDebitId; } else { throw Error( - "invalid request, transactionId or peerPullPaymentIncomingId required", + "invalid request, transactionId or peerPullDebitId required", ); } const peerPullInc = await ws.db - .mktx((x) => [x.peerPullPaymentIncoming]) + .mktx((x) => [x.peerPullDebit]) .runReadOnly(async (tx) => { - return tx.peerPullPaymentIncoming.get(peerPullPaymentIncomingId); + return tx.peerPullDebit.get(peerPullDebitId); }); if (!peerPullInc) { throw Error( - `can't accept unknown incoming p2p pull payment (${req.peerPullPaymentIncomingId})`, + `can't accept unknown incoming p2p pull payment (${req.peerPullDebitId})`, ); } @@ -437,15 +437,15 @@ export async function confirmPeerPullDebit( x.coins, x.denominations, x.refreshGroups, - x.peerPullPaymentIncoming, + x.peerPullDebit, x.coinAvailability, ]) .runReadWrite(async (tx) => { await spendCoins(ws, tx, { - // allocationId: `txn:peer-pull-debit:${req.peerPullPaymentIncomingId}`, + // allocationId: `txn:peer-pull-debit:${req.peerPullDebitId}`, allocationId: constructTransactionIdentifier({ tag: TransactionType.PeerPullDebit, - peerPullPaymentIncomingId, + peerPullDebitId, }), coinPubs: sel.coins.map((x) => x.coinPub), contributions: sel.coins.map((x) => @@ -454,8 +454,8 @@ export async function confirmPeerPullDebit( refreshReason: RefreshReason.PayPeerPull, }); - const pi = await tx.peerPullPaymentIncoming.get( - peerPullPaymentIncomingId, + const pi = await tx.peerPullDebit.get( + peerPullDebitId, ); if (!pi) { throw Error(); @@ -468,7 +468,7 @@ export async function confirmPeerPullDebit( totalCost: Amounts.stringify(totalAmount), }; } - await tx.peerPullPaymentIncoming.put(pi); + await tx.peerPullDebit.put(pi); return pi; }); @@ -476,7 +476,7 @@ export async function confirmPeerPullDebit( const transactionId = constructTransactionIdentifier({ tag: TransactionType.PeerPullDebit, - peerPullPaymentIncomingId, + peerPullDebitId, }); return { @@ -499,9 +499,9 @@ export async function preparePeerPullDebit( } const existingPullIncomingRecord = await ws.db - .mktx((x) => [x.peerPullPaymentIncoming]) + .mktx((x) => [x.peerPullDebit]) .runReadOnly(async (tx) => { - return tx.peerPullPaymentIncoming.indexes.byExchangeAndContractPriv.get([ + return tx.peerPullDebit.indexes.byExchangeAndContractPriv.get([ uri.exchangeBaseUrl, uri.contractPriv, ]); @@ -513,12 +513,12 @@ export async function preparePeerPullDebit( amountRaw: existingPullIncomingRecord.contractTerms.amount, amountEffective: existingPullIncomingRecord.totalCostEstimated, contractTerms: existingPullIncomingRecord.contractTerms, - peerPullPaymentIncomingId: - existingPullIncomingRecord.peerPullPaymentIncomingId, + peerPullDebitId: + existingPullIncomingRecord.peerPullDebitId, transactionId: constructTransactionIdentifier({ tag: TransactionType.PeerPullDebit, - peerPullPaymentIncomingId: - existingPullIncomingRecord.peerPullPaymentIncomingId, + peerPullDebitId: + existingPullIncomingRecord.peerPullDebitId, }), }; } @@ -553,7 +553,7 @@ export async function preparePeerPullDebit( codecForExchangePurseStatus(), ); - const peerPullPaymentIncomingId = encodeCrock(getRandomBytes(32)); + const peerPullDebitId = encodeCrock(getRandomBytes(32)); let contractTerms: PeerContractTerms; @@ -588,10 +588,10 @@ export async function preparePeerPullDebit( ); await ws.db - .mktx((x) => [x.peerPullPaymentIncoming]) + .mktx((x) => [x.peerPullDebit]) .runReadWrite(async (tx) => { - await tx.peerPullPaymentIncoming.add({ - peerPullPaymentIncomingId, + await tx.peerPullDebit.add({ + peerPullDebitId, contractPriv: contractPriv, exchangeBaseUrl: exchangeBaseUrl, pursePub: pursePub, @@ -607,42 +607,42 @@ export async function preparePeerPullDebit( amountEffective: Amounts.stringify(totalAmount), amountRaw: contractTerms.amount, contractTerms: contractTerms, - peerPullPaymentIncomingId, + peerPullDebitId, transactionId: constructTransactionIdentifier({ tag: TransactionType.PeerPullDebit, - peerPullPaymentIncomingId: peerPullPaymentIncomingId, + peerPullDebitId: peerPullDebitId, }), }; } export async function suspendPeerPullDebitTransaction( ws: InternalWalletState, - peerPullPaymentIncomingId: string, + peerPullDebitId: string, ) { const taskId = constructTaskIdentifier({ tag: PendingTaskType.PeerPullDebit, - peerPullPaymentIncomingId, + peerPullDebitId, }); const transactionId = constructTransactionIdentifier({ tag: TransactionType.PeerPullDebit, - peerPullPaymentIncomingId, + peerPullDebitId, }); stopLongpolling(ws, taskId); const transitionInfo = await ws.db - .mktx((x) => [x.peerPullPaymentIncoming]) + .mktx((x) => [x.peerPullDebit]) .runReadWrite(async (tx) => { - const pullDebitRec = await tx.peerPullPaymentIncoming.get( - peerPullPaymentIncomingId, + const pullDebitRec = await tx.peerPullDebit.get( + peerPullDebitId, ); if (!pullDebitRec) { - logger.warn(`peer pull debit ${peerPullPaymentIncomingId} not found`); + logger.warn(`peer pull debit ${peerPullDebitId} not found`); return; } let newStatus: PeerPullDebitRecordStatus | undefined = undefined; switch (pullDebitRec.status) { case PeerPullDebitRecordStatus.DialogProposed: break; - case PeerPullDebitRecordStatus.DonePaid: + case PeerPullDebitRecordStatus.Done: break; case PeerPullDebitRecordStatus.PendingDeposit: newStatus = PeerPullDebitRecordStatus.SuspendedDeposit; @@ -665,7 +665,7 @@ export async function suspendPeerPullDebitTransaction( const oldTxState = computePeerPullDebitTransactionState(pullDebitRec); pullDebitRec.status = newStatus; const newTxState = computePeerPullDebitTransactionState(pullDebitRec); - await tx.peerPullPaymentIncoming.put(pullDebitRec); + await tx.peerPullDebit.put(pullDebitRec); return { oldTxState, newTxState, @@ -678,25 +678,25 @@ export async function suspendPeerPullDebitTransaction( export async function abortPeerPullDebitTransaction( ws: InternalWalletState, - peerPullPaymentIncomingId: string, + peerPullDebitId: string, ) { const taskId = constructTaskIdentifier({ tag: PendingTaskType.PeerPullDebit, - peerPullPaymentIncomingId, + peerPullDebitId, }); const transactionId = constructTransactionIdentifier({ tag: TransactionType.PeerPullDebit, - peerPullPaymentIncomingId, + peerPullDebitId, }); stopLongpolling(ws, taskId); const transitionInfo = await ws.db - .mktx((x) => [x.peerPullPaymentIncoming]) + .mktx((x) => [x.peerPullDebit]) .runReadWrite(async (tx) => { - const pullDebitRec = await tx.peerPullPaymentIncoming.get( - peerPullPaymentIncomingId, + const pullDebitRec = await tx.peerPullDebit.get( + peerPullDebitId, ); if (!pullDebitRec) { - logger.warn(`peer pull debit ${peerPullPaymentIncomingId} not found`); + logger.warn(`peer pull debit ${peerPullDebitId} not found`); return; } let newStatus: PeerPullDebitRecordStatus | undefined = undefined; @@ -704,7 +704,7 @@ export async function abortPeerPullDebitTransaction( case PeerPullDebitRecordStatus.DialogProposed: newStatus = PeerPullDebitRecordStatus.Aborted; break; - case PeerPullDebitRecordStatus.DonePaid: + case PeerPullDebitRecordStatus.Done: break; case PeerPullDebitRecordStatus.PendingDeposit: newStatus = PeerPullDebitRecordStatus.AbortingRefresh; @@ -726,7 +726,7 @@ export async function abortPeerPullDebitTransaction( const oldTxState = computePeerPullDebitTransactionState(pullDebitRec); pullDebitRec.status = newStatus; const newTxState = computePeerPullDebitTransactionState(pullDebitRec); - await tx.peerPullPaymentIncoming.put(pullDebitRec); + await tx.peerPullDebit.put(pullDebitRec); return { oldTxState, newTxState, @@ -739,25 +739,25 @@ export async function abortPeerPullDebitTransaction( export async function failPeerPullDebitTransaction( ws: InternalWalletState, - peerPullPaymentIncomingId: string, + peerPullDebitId: string, ) { const taskId = constructTaskIdentifier({ tag: PendingTaskType.PeerPullDebit, - peerPullPaymentIncomingId, + peerPullDebitId, }); const transactionId = constructTransactionIdentifier({ tag: TransactionType.PeerPullDebit, - peerPullPaymentIncomingId, + peerPullDebitId, }); stopLongpolling(ws, taskId); const transitionInfo = await ws.db - .mktx((x) => [x.peerPullPaymentIncoming]) + .mktx((x) => [x.peerPullDebit]) .runReadWrite(async (tx) => { - const pullDebitRec = await tx.peerPullPaymentIncoming.get( - peerPullPaymentIncomingId, + const pullDebitRec = await tx.peerPullDebit.get( + peerPullDebitId, ); if (!pullDebitRec) { - logger.warn(`peer pull debit ${peerPullPaymentIncomingId} not found`); + logger.warn(`peer pull debit ${peerPullDebitId} not found`); return; } let newStatus: PeerPullDebitRecordStatus | undefined = undefined; @@ -765,7 +765,7 @@ export async function failPeerPullDebitTransaction( case PeerPullDebitRecordStatus.DialogProposed: newStatus = PeerPullDebitRecordStatus.Aborted; break; - case PeerPullDebitRecordStatus.DonePaid: + case PeerPullDebitRecordStatus.Done: break; case PeerPullDebitRecordStatus.PendingDeposit: break; @@ -787,7 +787,7 @@ export async function failPeerPullDebitTransaction( const oldTxState = computePeerPullDebitTransactionState(pullDebitRec); pullDebitRec.status = newStatus; const newTxState = computePeerPullDebitTransactionState(pullDebitRec); - await tx.peerPullPaymentIncoming.put(pullDebitRec); + await tx.peerPullDebit.put(pullDebitRec); return { oldTxState, newTxState, @@ -800,31 +800,31 @@ export async function failPeerPullDebitTransaction( export async function resumePeerPullDebitTransaction( ws: InternalWalletState, - peerPullPaymentIncomingId: string, + peerPullDebitId: string, ) { const taskId = constructTaskIdentifier({ tag: PendingTaskType.PeerPullDebit, - peerPullPaymentIncomingId, + peerPullDebitId, }); const transactionId = constructTransactionIdentifier({ tag: TransactionType.PeerPullDebit, - peerPullPaymentIncomingId, + peerPullDebitId, }); stopLongpolling(ws, taskId); const transitionInfo = await ws.db - .mktx((x) => [x.peerPullPaymentIncoming]) + .mktx((x) => [x.peerPullDebit]) .runReadWrite(async (tx) => { - const pullDebitRec = await tx.peerPullPaymentIncoming.get( - peerPullPaymentIncomingId, + const pullDebitRec = await tx.peerPullDebit.get( + peerPullDebitId, ); if (!pullDebitRec) { - logger.warn(`peer pull debit ${peerPullPaymentIncomingId} not found`); + logger.warn(`peer pull debit ${peerPullDebitId} not found`); return; } let newStatus: PeerPullDebitRecordStatus | undefined = undefined; switch (pullDebitRec.status) { case PeerPullDebitRecordStatus.DialogProposed: - case PeerPullDebitRecordStatus.DonePaid: + case PeerPullDebitRecordStatus.Done: case PeerPullDebitRecordStatus.PendingDeposit: break; case PeerPullDebitRecordStatus.SuspendedDeposit: @@ -846,7 +846,7 @@ export async function resumePeerPullDebitTransaction( const oldTxState = computePeerPullDebitTransactionState(pullDebitRec); pullDebitRec.status = newStatus; const newTxState = computePeerPullDebitTransactionState(pullDebitRec); - await tx.peerPullPaymentIncoming.put(pullDebitRec); + await tx.peerPullDebit.put(pullDebitRec); return { oldTxState, newTxState, @@ -872,7 +872,7 @@ export function computePeerPullDebitTransactionState( major: TransactionMajorState.Pending, minor: TransactionMinorState.Deposit, }; - case PeerPullDebitRecordStatus.DonePaid: + case PeerPullDebitRecordStatus.Done: return { major: TransactionMajorState.Done, }; @@ -910,7 +910,7 @@ export function computePeerPullDebitTransactionActions( return []; case PeerPullDebitRecordStatus.PendingDeposit: return [TransactionAction.Abort, TransactionAction.Suspend]; - case PeerPullDebitRecordStatus.DonePaid: + case PeerPullDebitRecordStatus.Done: return [TransactionAction.Delete]; case PeerPullDebitRecordStatus.SuspendedDeposit: return [TransactionAction.Resume, TransactionAction.Abort]; diff --git a/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts index 47e9eaddd..89d9e3b49 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts @@ -55,7 +55,7 @@ import { KycPendingInfo, KycUserType, PeerPushPaymentIncomingRecord, - PeerPushPaymentIncomingStatus, + PeerPushCreditStatus, PendingTaskType, WithdrawalGroupStatus, WithdrawalRecordType, @@ -99,10 +99,10 @@ export async function preparePeerPushCredit( } const existing = await ws.db - .mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming]) + .mktx((x) => [x.contractTerms, x.peerPushCredit]) .runReadOnly(async (tx) => { const existingPushInc = - await tx.peerPushPaymentIncoming.indexes.byExchangeAndContractPriv.get([ + await tx.peerPushCredit.indexes.byExchangeAndContractPriv.get([ uri.exchangeBaseUrl, uri.contractPriv, ]); @@ -129,12 +129,12 @@ export async function preparePeerPushCredit( amountEffective: existing.existingPushInc.estimatedAmountEffective, amountRaw: existing.existingContractTerms.amount, contractTerms: existing.existingContractTerms, - peerPushPaymentIncomingId: - existing.existingPushInc.peerPushPaymentIncomingId, + peerPushCreditId: + existing.existingPushInc.peerPushCreditId, transactionId: constructTransactionIdentifier({ tag: TransactionType.PeerPushCredit, - peerPushPaymentIncomingId: - existing.existingPushInc.peerPushPaymentIncomingId, + peerPushCreditId: + existing.existingPushInc.peerPushCreditId, }), }; } @@ -172,7 +172,7 @@ export async function preparePeerPushCredit( codecForExchangePurseStatus(), ); - const peerPushPaymentIncomingId = encodeCrock(getRandomBytes(32)); + const peerPushCreditId = encodeCrock(getRandomBytes(32)); const contractTermsHash = ContractTermsUtil.hashContractTerms( dec.contractTerms, @@ -188,17 +188,17 @@ export async function preparePeerPushCredit( ); await ws.db - .mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming]) + .mktx((x) => [x.contractTerms, x.peerPushCredit]) .runReadWrite(async (tx) => { - await tx.peerPushPaymentIncoming.add({ - peerPushPaymentIncomingId, + await tx.peerPushCredit.add({ + peerPushCreditId, contractPriv: contractPriv, exchangeBaseUrl: exchangeBaseUrl, mergePriv: dec.mergePriv, pursePub: pursePub, timestamp: TalerPreciseTimestamp.now(), contractTermsHash, - status: PeerPushPaymentIncomingStatus.DialogProposed, + status: PeerPushCreditStatus.DialogProposed, withdrawalGroupId, currency: Amounts.currencyOf(purseStatus.balance), estimatedAmountEffective: Amounts.stringify( @@ -219,28 +219,28 @@ export async function preparePeerPushCredit( amountEffective: wi.withdrawalAmountEffective, amountRaw: purseStatus.balance, contractTerms: dec.contractTerms, - peerPushPaymentIncomingId, + peerPushCreditId, transactionId: constructTransactionIdentifier({ tag: TransactionType.PeerPushCredit, - peerPushPaymentIncomingId, + peerPushCreditId, }), }; } async function longpollKycStatus( ws: InternalWalletState, - peerPushPaymentIncomingId: string, + peerPushCreditId: string, exchangeUrl: string, kycInfo: KycPendingInfo, userType: KycUserType, ): Promise<TaskRunResult> { const transactionId = constructTransactionIdentifier({ tag: TransactionType.PeerPushCredit, - peerPushPaymentIncomingId, + peerPushCreditId, }); const retryTag = constructTaskIdentifier({ tag: PendingTaskType.PeerPushCredit, - peerPushPaymentIncomingId, + peerPushCreditId, }); runLongpollAsync(ws, retryTag, async (ct) => { @@ -261,24 +261,24 @@ async function longpollKycStatus( kycStatusRes.status === HttpStatusCode.NoContent ) { const transitionInfo = await ws.db - .mktx((x) => [x.peerPushPaymentIncoming]) + .mktx((x) => [x.peerPushCredit]) .runReadWrite(async (tx) => { - const peerInc = await tx.peerPushPaymentIncoming.get( - peerPushPaymentIncomingId, + const peerInc = await tx.peerPushCredit.get( + peerPushCreditId, ); if (!peerInc) { return; } if ( peerInc.status !== - PeerPushPaymentIncomingStatus.PendingMergeKycRequired + PeerPushCreditStatus.PendingMergeKycRequired ) { return; } const oldTxState = computePeerPushCreditTransactionState(peerInc); - peerInc.status = PeerPushPaymentIncomingStatus.PendingMerge; + peerInc.status = PeerPushCreditStatus.PendingMerge; const newTxState = computePeerPushCreditTransactionState(peerInc); - await tx.peerPushPaymentIncoming.put(peerInc); + await tx.peerPushCredit.put(peerInc); return { oldTxState, newTxState }; }); notifyTransition(ws, transactionId, transitionInfo); @@ -304,9 +304,9 @@ async function processPeerPushCreditKycRequired( ): Promise<TaskRunResult> { const transactionId = constructTransactionIdentifier({ tag: TransactionType.PeerPushCredit, - peerPushPaymentIncomingId: peerInc.peerPushPaymentIncomingId, + peerPushCreditId: peerInc.peerPushCreditId, }); - const { peerPushPaymentIncomingId } = peerInc; + const { peerPushCreditId } = peerInc; const userType = "individual"; const url = new URL( @@ -331,10 +331,10 @@ async function processPeerPushCreditKycRequired( const kycStatus = await kycStatusRes.json(); logger.info(`kyc status: ${j2s(kycStatus)}`); const { transitionInfo, result } = await ws.db - .mktx((x) => [x.peerPushPaymentIncoming]) + .mktx((x) => [x.peerPushCredit]) .runReadWrite(async (tx) => { - const peerInc = await tx.peerPushPaymentIncoming.get( - peerPushPaymentIncomingId, + const peerInc = await tx.peerPushCredit.get( + peerPushCreditId, ); if (!peerInc) { return { @@ -348,9 +348,9 @@ async function processPeerPushCreditKycRequired( requirementRow: kycPending.requirement_row, }; peerInc.kycUrl = kycStatus.kyc_url; - peerInc.status = PeerPushPaymentIncomingStatus.PendingMergeKycRequired; + peerInc.status = PeerPushCreditStatus.PendingMergeKycRequired; const newTxState = computePeerPushCreditTransactionState(peerInc); - await tx.peerPushPaymentIncoming.put(peerInc); + await tx.peerPushCredit.put(peerInc); // We'll remove this eventually! New clients should rely on the // kycUrl field of the transaction, not the error code. const res: TaskRunResult = { @@ -379,10 +379,10 @@ async function handlePendingMerge( peerInc: PeerPushPaymentIncomingRecord, contractTerms: PeerContractTerms, ): Promise<TaskRunResult> { - const { peerPushPaymentIncomingId } = peerInc; + const { peerPushCreditId } = peerInc; const transactionId = constructTransactionIdentifier({ tag: TransactionType.PeerPushCredit, - peerPushPaymentIncomingId, + peerPushCreditId, }); const amount = Amounts.parseOrThrow(contractTerms.amount); @@ -446,7 +446,6 @@ async function handlePendingMerge( amount, wgInfo: { withdrawalType: WithdrawalRecordType.PeerPushCredit, - contractTerms, }, forcedWithdrawalGroupId: peerInc.withdrawalGroupId, exchangeBaseUrl: peerInc.exchangeBaseUrl, @@ -460,16 +459,15 @@ async function handlePendingMerge( const txRes = await ws.db .mktx((x) => [ x.contractTerms, - x.peerPushPaymentIncoming, + x.peerPushCredit, x.withdrawalGroups, x.reserves, x.exchanges, x.exchangeDetails, - x.exchangeTrust, ]) .runReadWrite(async (tx) => { - const peerInc = await tx.peerPushPaymentIncoming.get( - peerPushPaymentIncomingId, + const peerInc = await tx.peerPushCredit.get( + peerPushCreditId, ); if (!peerInc) { return undefined; @@ -477,9 +475,9 @@ async function handlePendingMerge( let withdrawalTransition: TransitionInfo | undefined; const oldTxState = computePeerPushCreditTransactionState(peerInc); switch (peerInc.status) { - case PeerPushPaymentIncomingStatus.PendingMerge: - case PeerPushPaymentIncomingStatus.PendingMergeKycRequired: { - peerInc.status = PeerPushPaymentIncomingStatus.PendingWithdrawing; + case PeerPushCreditStatus.PendingMerge: + case PeerPushCreditStatus.PendingMergeKycRequired: { + peerInc.status = PeerPushCreditStatus.PendingWithdrawing; const wgRes = await internalPerformCreateWithdrawalGroup( ws, tx, @@ -489,7 +487,7 @@ async function handlePendingMerge( break; } } - await tx.peerPushPaymentIncoming.put(peerInc); + await tx.peerPushCredit.put(peerInc); const newTxState = computePeerPushCreditTransactionState(peerInc); return { peerPushCreditTransition: { oldTxState, newTxState }, @@ -515,21 +513,21 @@ async function handlePendingWithdrawing( } const transactionId = constructTransactionIdentifier({ tag: TransactionType.PeerPushCredit, - peerPushPaymentIncomingId: peerInc.peerPushPaymentIncomingId, + peerPushCreditId: peerInc.peerPushCreditId, }); const wgId = peerInc.withdrawalGroupId; let finished: boolean = false; const transitionInfo = await ws.db - .mktx((x) => [x.peerPushPaymentIncoming, x.withdrawalGroups]) + .mktx((x) => [x.peerPushCredit, x.withdrawalGroups]) .runReadWrite(async (tx) => { - const ppi = await tx.peerPushPaymentIncoming.get( - peerInc.peerPushPaymentIncomingId, + const ppi = await tx.peerPushCredit.get( + peerInc.peerPushCreditId, ); if (!ppi) { finished = true; return; } - if (ppi.status !== PeerPushPaymentIncomingStatus.PendingWithdrawing) { + if (ppi.status !== PeerPushCreditStatus.PendingWithdrawing) { finished = true; return; } @@ -540,13 +538,13 @@ async function handlePendingWithdrawing( return undefined; } switch (wg.status) { - case WithdrawalGroupStatus.Finished: + case WithdrawalGroupStatus.Done: finished = true; - ppi.status = PeerPushPaymentIncomingStatus.Done; + ppi.status = PeerPushCreditStatus.Done; break; // FIXME: Also handle other final states! } - await tx.peerPushPaymentIncoming.put(ppi); + await tx.peerPushCredit.put(ppi); const newTxState = computePeerPushCreditTransactionState(ppi); return { oldTxState, @@ -564,14 +562,14 @@ async function handlePendingWithdrawing( export async function processPeerPushCredit( ws: InternalWalletState, - peerPushPaymentIncomingId: string, + peerPushCreditId: string, ): Promise<TaskRunResult> { let peerInc: PeerPushPaymentIncomingRecord | undefined; let contractTerms: PeerContractTerms | undefined; await ws.db - .mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming]) + .mktx((x) => [x.contractTerms, x.peerPushCredit]) .runReadWrite(async (tx) => { - peerInc = await tx.peerPushPaymentIncoming.get(peerPushPaymentIncomingId); + peerInc = await tx.peerPushCredit.get(peerPushCreditId); if (!peerInc) { return; } @@ -579,35 +577,35 @@ export async function processPeerPushCredit( if (ctRec) { contractTerms = ctRec.contractTermsRaw; } - await tx.peerPushPaymentIncoming.put(peerInc); + await tx.peerPushCredit.put(peerInc); }); checkDbInvariant(!!contractTerms); if (!peerInc) { throw Error( - `can't accept unknown incoming p2p push payment (${peerPushPaymentIncomingId})`, + `can't accept unknown incoming p2p push payment (${peerPushCreditId})`, ); } switch (peerInc.status) { - case PeerPushPaymentIncomingStatus.PendingMergeKycRequired: { + case PeerPushCreditStatus.PendingMergeKycRequired: { if (!peerInc.kycInfo) { throw Error("invalid state, kycInfo required"); } return await longpollKycStatus( ws, - peerPushPaymentIncomingId, + peerPushCreditId, peerInc.exchangeBaseUrl, peerInc.kycInfo, "individual", ); } - case PeerPushPaymentIncomingStatus.PendingMerge: + case PeerPushCreditStatus.PendingMerge: return handlePendingMerge(ws, peerInc, contractTerms); - case PeerPushPaymentIncomingStatus.PendingWithdrawing: + case PeerPushCreditStatus.PendingWithdrawing: return handlePendingWithdrawing(ws, peerInc); default: @@ -620,9 +618,9 @@ export async function confirmPeerPushCredit( req: ConfirmPeerPushCreditRequest, ): Promise<AcceptPeerPushPaymentResponse> { let peerInc: PeerPushPaymentIncomingRecord | undefined; - let peerPushPaymentIncomingId: string; - if (req.peerPushPaymentIncomingId) { - peerPushPaymentIncomingId = req.peerPushPaymentIncomingId; + let peerPushCreditId: string; + if (req.peerPushCreditId) { + peerPushCreditId = req.peerPushCreditId; } else if (req.transactionId) { const parsedTx = parseTransactionIdentifier(req.transactionId); if (!parsedTx) { @@ -631,29 +629,29 @@ export async function confirmPeerPushCredit( if (parsedTx.tag !== TransactionType.PeerPushCredit) { throw Error("invalid transaction ID type"); } - peerPushPaymentIncomingId = parsedTx.peerPushPaymentIncomingId; + peerPushCreditId = parsedTx.peerPushCreditId; } else { throw Error( - "no transaction ID (or deprecated peerPushPaymentIncomingId) provided", + "no transaction ID (or deprecated peerPushCreditId) provided", ); } await ws.db - .mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming]) + .mktx((x) => [x.contractTerms, x.peerPushCredit]) .runReadWrite(async (tx) => { - peerInc = await tx.peerPushPaymentIncoming.get(peerPushPaymentIncomingId); + peerInc = await tx.peerPushCredit.get(peerPushCreditId); if (!peerInc) { return; } - if (peerInc.status === PeerPushPaymentIncomingStatus.DialogProposed) { - peerInc.status = PeerPushPaymentIncomingStatus.PendingMerge; + if (peerInc.status === PeerPushCreditStatus.DialogProposed) { + peerInc.status = PeerPushCreditStatus.PendingMerge; } - await tx.peerPushPaymentIncoming.put(peerInc); + await tx.peerPushCredit.put(peerInc); }); if (!peerInc) { throw Error( - `can't accept unknown incoming p2p push payment (${req.peerPushPaymentIncomingId})`, + `can't accept unknown incoming p2p push payment (${req.peerPushCreditId})`, ); } @@ -661,7 +659,7 @@ export async function confirmPeerPushCredit( const transactionId = constructTransactionIdentifier({ tag: TransactionType.PeerPushCredit, - peerPushPaymentIncomingId, + peerPushCreditId, }); return { @@ -671,48 +669,48 @@ export async function confirmPeerPushCredit( export async function suspendPeerPushCreditTransaction( ws: InternalWalletState, - peerPushPaymentIncomingId: string, + peerPushCreditId: string, ) { const taskId = constructTaskIdentifier({ tag: PendingTaskType.PeerPushCredit, - peerPushPaymentIncomingId, + peerPushCreditId, }); const transactionId = constructTransactionIdentifier({ tag: TransactionType.PeerPushCredit, - peerPushPaymentIncomingId, + peerPushCreditId, }); stopLongpolling(ws, taskId); const transitionInfo = await ws.db - .mktx((x) => [x.peerPushPaymentIncoming]) + .mktx((x) => [x.peerPushCredit]) .runReadWrite(async (tx) => { - const pushCreditRec = await tx.peerPushPaymentIncoming.get( - peerPushPaymentIncomingId, + const pushCreditRec = await tx.peerPushCredit.get( + peerPushCreditId, ); if (!pushCreditRec) { - logger.warn(`peer push credit ${peerPushPaymentIncomingId} not found`); + logger.warn(`peer push credit ${peerPushCreditId} not found`); return; } - let newStatus: PeerPushPaymentIncomingStatus | undefined = undefined; + let newStatus: PeerPushCreditStatus | undefined = undefined; switch (pushCreditRec.status) { - case PeerPushPaymentIncomingStatus.DialogProposed: - case PeerPushPaymentIncomingStatus.Done: - case PeerPushPaymentIncomingStatus.SuspendedMerge: - case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired: - case PeerPushPaymentIncomingStatus.SuspendedWithdrawing: + case PeerPushCreditStatus.DialogProposed: + case PeerPushCreditStatus.Done: + case PeerPushCreditStatus.SuspendedMerge: + case PeerPushCreditStatus.SuspendedMergeKycRequired: + case PeerPushCreditStatus.SuspendedWithdrawing: break; - case PeerPushPaymentIncomingStatus.PendingMergeKycRequired: - newStatus = PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired; + case PeerPushCreditStatus.PendingMergeKycRequired: + newStatus = PeerPushCreditStatus.SuspendedMergeKycRequired; break; - case PeerPushPaymentIncomingStatus.PendingMerge: - newStatus = PeerPushPaymentIncomingStatus.SuspendedMerge; + case PeerPushCreditStatus.PendingMerge: + newStatus = PeerPushCreditStatus.SuspendedMerge; break; - case PeerPushPaymentIncomingStatus.PendingWithdrawing: + case PeerPushCreditStatus.PendingWithdrawing: // FIXME: Suspend internal withdrawal transaction! - newStatus = PeerPushPaymentIncomingStatus.SuspendedWithdrawing; + newStatus = PeerPushCreditStatus.SuspendedWithdrawing; break; - case PeerPushPaymentIncomingStatus.Aborted: + case PeerPushCreditStatus.Aborted: break; - case PeerPushPaymentIncomingStatus.Failed: + case PeerPushCreditStatus.Failed: break; default: assertUnreachable(pushCreditRec.status); @@ -721,7 +719,7 @@ export async function suspendPeerPushCreditTransaction( const oldTxState = computePeerPushCreditTransactionState(pushCreditRec); pushCreditRec.status = newStatus; const newTxState = computePeerPushCreditTransactionState(pushCreditRec); - await tx.peerPushPaymentIncoming.put(pushCreditRec); + await tx.peerPushCredit.put(pushCreditRec); return { oldTxState, newTxState, @@ -734,51 +732,51 @@ export async function suspendPeerPushCreditTransaction( export async function abortPeerPushCreditTransaction( ws: InternalWalletState, - peerPushPaymentIncomingId: string, + peerPushCreditId: string, ) { const taskId = constructTaskIdentifier({ tag: PendingTaskType.PeerPushCredit, - peerPushPaymentIncomingId, + peerPushCreditId, }); const transactionId = constructTransactionIdentifier({ tag: TransactionType.PeerPushCredit, - peerPushPaymentIncomingId, + peerPushCreditId, }); stopLongpolling(ws, taskId); const transitionInfo = await ws.db - .mktx((x) => [x.peerPushPaymentIncoming]) + .mktx((x) => [x.peerPushCredit]) .runReadWrite(async (tx) => { - const pushCreditRec = await tx.peerPushPaymentIncoming.get( - peerPushPaymentIncomingId, + const pushCreditRec = await tx.peerPushCredit.get( + peerPushCreditId, ); if (!pushCreditRec) { - logger.warn(`peer push credit ${peerPushPaymentIncomingId} not found`); + logger.warn(`peer push credit ${peerPushCreditId} not found`); return; } - let newStatus: PeerPushPaymentIncomingStatus | undefined = undefined; + let newStatus: PeerPushCreditStatus | undefined = undefined; switch (pushCreditRec.status) { - case PeerPushPaymentIncomingStatus.DialogProposed: - newStatus = PeerPushPaymentIncomingStatus.Aborted; + case PeerPushCreditStatus.DialogProposed: + newStatus = PeerPushCreditStatus.Aborted; break; - case PeerPushPaymentIncomingStatus.Done: + case PeerPushCreditStatus.Done: break; - case PeerPushPaymentIncomingStatus.SuspendedMerge: - case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired: - case PeerPushPaymentIncomingStatus.SuspendedWithdrawing: - newStatus = PeerPushPaymentIncomingStatus.Aborted; + case PeerPushCreditStatus.SuspendedMerge: + case PeerPushCreditStatus.SuspendedMergeKycRequired: + case PeerPushCreditStatus.SuspendedWithdrawing: + newStatus = PeerPushCreditStatus.Aborted; break; - case PeerPushPaymentIncomingStatus.PendingMergeKycRequired: - newStatus = PeerPushPaymentIncomingStatus.Aborted; + case PeerPushCreditStatus.PendingMergeKycRequired: + newStatus = PeerPushCreditStatus.Aborted; break; - case PeerPushPaymentIncomingStatus.PendingMerge: - newStatus = PeerPushPaymentIncomingStatus.Aborted; + case PeerPushCreditStatus.PendingMerge: + newStatus = PeerPushCreditStatus.Aborted; break; - case PeerPushPaymentIncomingStatus.PendingWithdrawing: - newStatus = PeerPushPaymentIncomingStatus.Aborted; + case PeerPushCreditStatus.PendingWithdrawing: + newStatus = PeerPushCreditStatus.Aborted; break; - case PeerPushPaymentIncomingStatus.Aborted: + case PeerPushCreditStatus.Aborted: break; - case PeerPushPaymentIncomingStatus.Failed: + case PeerPushCreditStatus.Failed: break; default: assertUnreachable(pushCreditRec.status); @@ -787,7 +785,7 @@ export async function abortPeerPushCreditTransaction( const oldTxState = computePeerPushCreditTransactionState(pushCreditRec); pushCreditRec.status = newStatus; const newTxState = computePeerPushCreditTransactionState(pushCreditRec); - await tx.peerPushPaymentIncoming.put(pushCreditRec); + await tx.peerPushCredit.put(pushCreditRec); return { oldTxState, newTxState, @@ -800,7 +798,7 @@ export async function abortPeerPushCreditTransaction( export async function failPeerPushCreditTransaction( ws: InternalWalletState, - peerPushPaymentIncomingId: string, + peerPushCreditId: string, ) { // We don't have any "aborting" states! throw Error("can't run cancel-aborting on peer-push-credit transaction"); @@ -808,47 +806,47 @@ export async function failPeerPushCreditTransaction( export async function resumePeerPushCreditTransaction( ws: InternalWalletState, - peerPushPaymentIncomingId: string, + peerPushCreditId: string, ) { const taskId = constructTaskIdentifier({ tag: PendingTaskType.PeerPushCredit, - peerPushPaymentIncomingId, + peerPushCreditId, }); const transactionId = constructTransactionIdentifier({ tag: TransactionType.PeerPushCredit, - peerPushPaymentIncomingId, + peerPushCreditId, }); stopLongpolling(ws, taskId); const transitionInfo = await ws.db - .mktx((x) => [x.peerPushPaymentIncoming]) + .mktx((x) => [x.peerPushCredit]) .runReadWrite(async (tx) => { - const pushCreditRec = await tx.peerPushPaymentIncoming.get( - peerPushPaymentIncomingId, + const pushCreditRec = await tx.peerPushCredit.get( + peerPushCreditId, ); if (!pushCreditRec) { - logger.warn(`peer push credit ${peerPushPaymentIncomingId} not found`); + logger.warn(`peer push credit ${peerPushCreditId} not found`); return; } - let newStatus: PeerPushPaymentIncomingStatus | undefined = undefined; + let newStatus: PeerPushCreditStatus | undefined = undefined; switch (pushCreditRec.status) { - case PeerPushPaymentIncomingStatus.DialogProposed: - case PeerPushPaymentIncomingStatus.Done: - case PeerPushPaymentIncomingStatus.PendingMergeKycRequired: - case PeerPushPaymentIncomingStatus.PendingMerge: - case PeerPushPaymentIncomingStatus.PendingWithdrawing: - case PeerPushPaymentIncomingStatus.SuspendedMerge: - newStatus = PeerPushPaymentIncomingStatus.PendingMerge; + case PeerPushCreditStatus.DialogProposed: + case PeerPushCreditStatus.Done: + case PeerPushCreditStatus.PendingMergeKycRequired: + case PeerPushCreditStatus.PendingMerge: + case PeerPushCreditStatus.PendingWithdrawing: + case PeerPushCreditStatus.SuspendedMerge: + newStatus = PeerPushCreditStatus.PendingMerge; break; - case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired: - newStatus = PeerPushPaymentIncomingStatus.PendingMergeKycRequired; + case PeerPushCreditStatus.SuspendedMergeKycRequired: + newStatus = PeerPushCreditStatus.PendingMergeKycRequired; break; - case PeerPushPaymentIncomingStatus.SuspendedWithdrawing: + case PeerPushCreditStatus.SuspendedWithdrawing: // FIXME: resume underlying "internal-withdrawal" transaction. - newStatus = PeerPushPaymentIncomingStatus.PendingWithdrawing; + newStatus = PeerPushCreditStatus.PendingWithdrawing; break; - case PeerPushPaymentIncomingStatus.Aborted: + case PeerPushCreditStatus.Aborted: break; - case PeerPushPaymentIncomingStatus.Failed: + case PeerPushCreditStatus.Failed: break; default: assertUnreachable(pushCreditRec.status); @@ -857,7 +855,7 @@ export async function resumePeerPushCreditTransaction( const oldTxState = computePeerPushCreditTransactionState(pushCreditRec); pushCreditRec.status = newStatus; const newTxState = computePeerPushCreditTransactionState(pushCreditRec); - await tx.peerPushPaymentIncoming.put(pushCreditRec); + await tx.peerPushCredit.put(pushCreditRec); return { oldTxState, newTxState, @@ -873,50 +871,50 @@ export function computePeerPushCreditTransactionState( pushCreditRecord: PeerPushPaymentIncomingRecord, ): TransactionState { switch (pushCreditRecord.status) { - case PeerPushPaymentIncomingStatus.DialogProposed: + case PeerPushCreditStatus.DialogProposed: return { major: TransactionMajorState.Dialog, minor: TransactionMinorState.Proposed, }; - case PeerPushPaymentIncomingStatus.PendingMerge: + case PeerPushCreditStatus.PendingMerge: return { major: TransactionMajorState.Pending, minor: TransactionMinorState.Merge, }; - case PeerPushPaymentIncomingStatus.Done: + case PeerPushCreditStatus.Done: return { major: TransactionMajorState.Done, }; - case PeerPushPaymentIncomingStatus.PendingMergeKycRequired: + case PeerPushCreditStatus.PendingMergeKycRequired: return { major: TransactionMajorState.Pending, minor: TransactionMinorState.KycRequired, }; - case PeerPushPaymentIncomingStatus.PendingWithdrawing: + case PeerPushCreditStatus.PendingWithdrawing: return { major: TransactionMajorState.Pending, minor: TransactionMinorState.Withdraw, }; - case PeerPushPaymentIncomingStatus.SuspendedMerge: + case PeerPushCreditStatus.SuspendedMerge: return { major: TransactionMajorState.Suspended, minor: TransactionMinorState.Merge, }; - case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired: + case PeerPushCreditStatus.SuspendedMergeKycRequired: return { major: TransactionMajorState.Suspended, minor: TransactionMinorState.MergeKycRequired, }; - case PeerPushPaymentIncomingStatus.SuspendedWithdrawing: + case PeerPushCreditStatus.SuspendedWithdrawing: return { major: TransactionMajorState.Suspended, minor: TransactionMinorState.Withdraw, }; - case PeerPushPaymentIncomingStatus.Aborted: + case PeerPushCreditStatus.Aborted: return { major: TransactionMajorState.Aborted, }; - case PeerPushPaymentIncomingStatus.Failed: + case PeerPushCreditStatus.Failed: return { major: TransactionMajorState.Failed, }; @@ -929,25 +927,25 @@ export function computePeerPushCreditTransactionActions( pushCreditRecord: PeerPushPaymentIncomingRecord, ): TransactionAction[] { switch (pushCreditRecord.status) { - case PeerPushPaymentIncomingStatus.DialogProposed: + case PeerPushCreditStatus.DialogProposed: return [TransactionAction.Delete]; - case PeerPushPaymentIncomingStatus.PendingMerge: + case PeerPushCreditStatus.PendingMerge: return [TransactionAction.Abort, TransactionAction.Suspend]; - case PeerPushPaymentIncomingStatus.Done: + case PeerPushCreditStatus.Done: return [TransactionAction.Delete]; - case PeerPushPaymentIncomingStatus.PendingMergeKycRequired: + case PeerPushCreditStatus.PendingMergeKycRequired: return [TransactionAction.Abort, TransactionAction.Suspend]; - case PeerPushPaymentIncomingStatus.PendingWithdrawing: + case PeerPushCreditStatus.PendingWithdrawing: return [TransactionAction.Suspend, TransactionAction.Fail]; - case PeerPushPaymentIncomingStatus.SuspendedMerge: + case PeerPushCreditStatus.SuspendedMerge: return [TransactionAction.Resume, TransactionAction.Abort]; - case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired: + case PeerPushCreditStatus.SuspendedMergeKycRequired: return [TransactionAction.Resume, TransactionAction.Abort]; - case PeerPushPaymentIncomingStatus.SuspendedWithdrawing: + case PeerPushCreditStatus.SuspendedWithdrawing: return [TransactionAction.Resume, TransactionAction.Fail]; - case PeerPushPaymentIncomingStatus.Aborted: + case PeerPushCreditStatus.Aborted: return [TransactionAction.Delete]; - case PeerPushPaymentIncomingStatus.Failed: + case PeerPushCreditStatus.Failed: return [TransactionAction.Delete]; default: assertUnreachable(pushCreditRecord.status); diff --git a/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts index 2349e5c4a..e80ffc059 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts @@ -51,8 +51,8 @@ import { } from "@gnu-taler/taler-util/http"; import { EncryptContractRequest } from "../crypto/cryptoTypes.js"; import { - PeerPushPaymentInitiationRecord, - PeerPushPaymentInitiationStatus, + PeerPushDebitRecord, + PeerPushDebitStatus, RefreshOperationStatus, createRefreshGroup, } from "../index.js"; @@ -107,7 +107,7 @@ export async function checkPeerPushDebit( async function handlePurseCreationConflict( ws: InternalWalletState, - peerPushInitiation: PeerPushPaymentInitiationRecord, + peerPushInitiation: PeerPushDebitRecord, resp: HttpResponse, ): Promise<TaskRunResult> { const pursePub = peerPushInitiation.pursePub; @@ -152,17 +152,15 @@ async function handlePurseCreationConflict( } await ws.db - .mktx((x) => [x.peerPushPaymentInitiations]) + .mktx((x) => [x.peerPushDebit]) .runReadWrite(async (tx) => { - const myPpi = await tx.peerPushPaymentInitiations.get( - peerPushInitiation.pursePub, - ); + const myPpi = await tx.peerPushDebit.get(peerPushInitiation.pursePub); if (!myPpi) { return; } switch (myPpi.status) { - case PeerPushPaymentInitiationStatus.PendingCreatePurse: - case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: { + case PeerPushDebitStatus.PendingCreatePurse: + case PeerPushDebitStatus.SuspendedCreatePurse: { const sel = coinSelRes.result; myPpi.coinSel = { coinPubs: sel.coins.map((x) => x.coinPub), @@ -173,19 +171,36 @@ async function handlePurseCreationConflict( default: return; } - await tx.peerPushPaymentInitiations.put(myPpi); + await tx.peerPushDebit.put(myPpi); }); return TaskRunResult.finished(); } async function processPeerPushDebitCreateReserve( ws: InternalWalletState, - peerPushInitiation: PeerPushPaymentInitiationRecord, + peerPushInitiation: PeerPushDebitRecord, ): Promise<TaskRunResult> { - logger.info("processing peer-push-debit pending(create-reserve)"); const pursePub = peerPushInitiation.pursePub; const purseExpiration = peerPushInitiation.purseExpiration; const hContractTerms = peerPushInitiation.contractTermsHash; + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPushDebit, + pursePub: pursePub, + }); + + logger.trace(`processing ${transactionId} pending(create-reserve)`); + + const contractTermsRecord = await ws.db + .mktx((x) => [x.contractTerms]) + .runReadOnly(async (tx) => { + return tx.contractTerms.get(hContractTerms); + }); + + if (!contractTermsRecord) { + throw Error( + `db invariant failed, contract terms for ${transactionId} missing`, + ); + } const purseSigResp = await ws.cryptoApi.signPurseCreation({ hContractTerms, @@ -208,7 +223,7 @@ async function processPeerPushDebitCreateReserve( }); const encryptContractRequest: EncryptContractRequest = { - contractTerms: peerPushInitiation.contractTerms, + contractTerms: contractTermsRecord.contractTermsRaw, mergePriv: peerPushInitiation.mergePriv, pursePriv: peerPushInitiation.pursePriv, pursePub: peerPushInitiation.pursePub, @@ -284,8 +299,8 @@ async function processPeerPushDebitCreateReserve( } await transitionPeerPushDebitTransaction(ws, pursePub, { - stFrom: PeerPushPaymentInitiationStatus.PendingCreatePurse, - stTo: PeerPushPaymentInitiationStatus.PendingReady, + stFrom: PeerPushDebitStatus.PendingCreatePurse, + stTo: PeerPushDebitStatus.PendingReady, }); return TaskRunResult.finished(); @@ -293,7 +308,7 @@ async function processPeerPushDebitCreateReserve( async function processPeerPushDebitAbortingDeletePurse( ws: InternalWalletState, - peerPushInitiation: PeerPushPaymentInitiationRecord, + peerPushInitiation: PeerPushDebitRecord, ): Promise<TaskRunResult> { const { pursePub, pursePriv } = peerPushInitiation; const transactionId = constructTransactionIdentifier({ @@ -318,20 +333,18 @@ async function processPeerPushDebitAbortingDeletePurse( const transitionInfo = await ws.db .mktx((x) => [ - x.peerPushPaymentInitiations, + x.peerPushDebit, x.refreshGroups, x.denominations, x.coinAvailability, x.coins, ]) .runReadWrite(async (tx) => { - const ppiRec = await tx.peerPushPaymentInitiations.get(pursePub); + const ppiRec = await tx.peerPushDebit.get(pursePub); if (!ppiRec) { return undefined; } - if ( - ppiRec.status !== PeerPushPaymentInitiationStatus.AbortingDeletePurse - ) { + if (ppiRec.status !== PeerPushDebitStatus.AbortingDeletePurse) { return undefined; } const currency = Amounts.currencyOf(ppiRec.amount); @@ -352,9 +365,9 @@ async function processPeerPushDebitAbortingDeletePurse( coinPubs, RefreshReason.AbortPeerPushDebit, ); - ppiRec.status = PeerPushPaymentInitiationStatus.AbortingRefresh; + ppiRec.status = PeerPushDebitStatus.AbortingRefresh; ppiRec.abortRefreshGroupId = refresh.refreshGroupId; - await tx.peerPushPaymentInitiations.put(ppiRec); + await tx.peerPushDebit.put(ppiRec); const newTxState = computePeerPushDebitTransactionState(ppiRec); return { oldTxState, @@ -367,8 +380,8 @@ async function processPeerPushDebitAbortingDeletePurse( } interface SimpleTransition { - stFrom: PeerPushPaymentInitiationStatus; - stTo: PeerPushPaymentInitiationStatus; + stFrom: PeerPushDebitStatus; + stTo: PeerPushDebitStatus; } async function transitionPeerPushDebitTransaction( @@ -381,9 +394,9 @@ async function transitionPeerPushDebitTransaction( pursePub, }); const transitionInfo = await ws.db - .mktx((x) => [x.peerPushPaymentInitiations]) + .mktx((x) => [x.peerPushDebit]) .runReadWrite(async (tx) => { - const ppiRec = await tx.peerPushPaymentInitiations.get(pursePub); + const ppiRec = await tx.peerPushDebit.get(pursePub); if (!ppiRec) { return undefined; } @@ -392,7 +405,7 @@ async function transitionPeerPushDebitTransaction( } const oldTxState = computePeerPushDebitTransactionState(ppiRec); ppiRec.status = transitionSpec.stTo; - await tx.peerPushPaymentInitiations.put(ppiRec); + await tx.peerPushDebit.put(ppiRec); const newTxState = computePeerPushDebitTransactionState(ppiRec); return { oldTxState, @@ -404,7 +417,7 @@ async function transitionPeerPushDebitTransaction( async function processPeerPushDebitAbortingRefresh( ws: InternalWalletState, - peerPushInitiation: PeerPushPaymentInitiationRecord, + peerPushInitiation: PeerPushDebitRecord, ): Promise<TaskRunResult> { const pursePub = peerPushInitiation.pursePub; const abortRefreshGroupId = peerPushInitiation.abortRefreshGroupId; @@ -414,33 +427,33 @@ async function processPeerPushDebitAbortingRefresh( pursePub: peerPushInitiation.pursePub, }); const transitionInfo = await ws.db - .mktx((x) => [x.refreshGroups, x.peerPushPaymentInitiations]) + .mktx((x) => [x.refreshGroups, x.peerPushDebit]) .runReadWrite(async (tx) => { const refreshGroup = await tx.refreshGroups.get(abortRefreshGroupId); - let newOpState: PeerPushPaymentInitiationStatus | undefined; + let newOpState: PeerPushDebitStatus | undefined; if (!refreshGroup) { // Maybe it got manually deleted? Means that we should // just go into failed. logger.warn("no aborting refresh group found for deposit group"); - newOpState = PeerPushPaymentInitiationStatus.Failed; + newOpState = PeerPushDebitStatus.Failed; } else { if (refreshGroup.operationStatus === RefreshOperationStatus.Finished) { - newOpState = PeerPushPaymentInitiationStatus.Aborted; + newOpState = PeerPushDebitStatus.Aborted; } else if ( refreshGroup.operationStatus === RefreshOperationStatus.Failed ) { - newOpState = PeerPushPaymentInitiationStatus.Failed; + newOpState = PeerPushDebitStatus.Failed; } } if (newOpState) { - const newDg = await tx.peerPushPaymentInitiations.get(pursePub); + const newDg = await tx.peerPushDebit.get(pursePub); if (!newDg) { return; } const oldTxState = computePeerPushDebitTransactionState(newDg); newDg.status = newOpState; const newTxState = computePeerPushDebitTransactionState(newDg); - await tx.peerPushPaymentInitiations.put(newDg); + await tx.peerPushDebit.put(newDg); return { oldTxState, newTxState }; } return undefined; @@ -455,7 +468,7 @@ async function processPeerPushDebitAbortingRefresh( */ async function processPeerPushDebitReady( ws: InternalWalletState, - peerPushInitiation: PeerPushPaymentInitiationRecord, + peerPushInitiation: PeerPushDebitRecord, ): Promise<TaskRunResult> { logger.info("processing peer-push-debit pending(ready)"); const pursePub = peerPushInitiation.pursePub; @@ -488,8 +501,8 @@ async function processPeerPushDebitReady( ws, peerPushInitiation.pursePub, { - stFrom: PeerPushPaymentInitiationStatus.PendingReady, - stTo: PeerPushPaymentInitiationStatus.Done, + stFrom: PeerPushDebitStatus.PendingReady, + stTo: PeerPushDebitStatus.Done, }, ); return { @@ -501,8 +514,8 @@ async function processPeerPushDebitReady( ws, peerPushInitiation.pursePub, { - stFrom: PeerPushPaymentInitiationStatus.PendingReady, - stTo: PeerPushPaymentInitiationStatus.Expired, + stFrom: PeerPushDebitStatus.PendingReady, + stTo: PeerPushDebitStatus.Expired, }, ); return { @@ -528,9 +541,9 @@ export async function processPeerPushDebit( pursePub: string, ): Promise<TaskRunResult> { const peerPushInitiation = await ws.db - .mktx((x) => [x.peerPushPaymentInitiations]) + .mktx((x) => [x.peerPushDebit]) .runReadOnly(async (tx) => { - return tx.peerPushPaymentInitiations.get(pursePub); + return tx.peerPushDebit.get(pursePub); }); if (!peerPushInitiation) { throw Error("peer push payment not found"); @@ -550,13 +563,13 @@ export async function processPeerPushDebit( } switch (peerPushInitiation.status) { - case PeerPushPaymentInitiationStatus.PendingCreatePurse: + case PeerPushDebitStatus.PendingCreatePurse: return processPeerPushDebitCreateReserve(ws, peerPushInitiation); - case PeerPushPaymentInitiationStatus.PendingReady: + case PeerPushDebitStatus.PendingReady: return processPeerPushDebitReady(ws, peerPushInitiation); - case PeerPushPaymentInitiationStatus.AbortingDeletePurse: + case PeerPushDebitStatus.AbortingDeletePurse: return processPeerPushDebitAbortingDeletePurse(ws, peerPushInitiation); - case PeerPushPaymentInitiationStatus.AbortingRefresh: + case PeerPushDebitStatus.AbortingRefresh: return processPeerPushDebitAbortingRefresh(ws, peerPushInitiation); default: { const txState = computePeerPushDebitTransactionState(peerPushInitiation); @@ -626,7 +639,7 @@ export async function initiatePeerPushDebit( x.coinAvailability, x.denominations, x.refreshGroups, - x.peerPushPaymentInitiations, + x.peerPushDebit, ]) .runReadWrite(async (tx) => { // FIXME: Instead of directly doing a spendCoin here, @@ -645,7 +658,7 @@ export async function initiatePeerPushDebit( refreshReason: RefreshReason.PayPeerPush, }); - const ppi: PeerPushPaymentInitiationRecord = { + const ppi: PeerPushDebitRecord = { amount: Amounts.stringify(instructedAmount), contractPriv: contractKeyPair.priv, contractPub: contractKeyPair.pub, @@ -657,8 +670,7 @@ export async function initiatePeerPushDebit( pursePriv: pursePair.priv, pursePub: pursePair.pub, timestampCreated: TalerPreciseTimestamp.now(), - status: PeerPushPaymentInitiationStatus.PendingCreatePurse, - contractTerms: contractTerms, + status: PeerPushDebitStatus.PendingCreatePurse, contractEncNonce, coinSel: { coinPubs: sel.coins.map((x) => x.coinPub), @@ -667,7 +679,7 @@ export async function initiatePeerPushDebit( totalCost: Amounts.stringify(totalAmount), }; - await tx.peerPushPaymentInitiations.add(ppi); + await tx.peerPushDebit.add(ppi); await tx.contractTerms.put({ h: hContractTerms, @@ -701,32 +713,32 @@ export async function initiatePeerPushDebit( } export function computePeerPushDebitTransactionActions( - ppiRecord: PeerPushPaymentInitiationRecord, + ppiRecord: PeerPushDebitRecord, ): TransactionAction[] { switch (ppiRecord.status) { - case PeerPushPaymentInitiationStatus.PendingCreatePurse: + case PeerPushDebitStatus.PendingCreatePurse: return [TransactionAction.Abort, TransactionAction.Suspend]; - case PeerPushPaymentInitiationStatus.PendingReady: + case PeerPushDebitStatus.PendingReady: return [TransactionAction.Abort, TransactionAction.Suspend]; - case PeerPushPaymentInitiationStatus.Aborted: + case PeerPushDebitStatus.Aborted: return [TransactionAction.Delete]; - case PeerPushPaymentInitiationStatus.AbortingDeletePurse: + case PeerPushDebitStatus.AbortingDeletePurse: return [TransactionAction.Suspend, TransactionAction.Fail]; - case PeerPushPaymentInitiationStatus.AbortingRefresh: + case PeerPushDebitStatus.AbortingRefresh: return [TransactionAction.Suspend, TransactionAction.Fail]; - case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse: + case PeerPushDebitStatus.SuspendedAbortingDeletePurse: return [TransactionAction.Resume, TransactionAction.Fail]; - case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh: + case PeerPushDebitStatus.SuspendedAbortingRefresh: return [TransactionAction.Resume, TransactionAction.Fail]; - case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: + case PeerPushDebitStatus.SuspendedCreatePurse: return [TransactionAction.Resume, TransactionAction.Abort]; - case PeerPushPaymentInitiationStatus.SuspendedReady: + case PeerPushDebitStatus.SuspendedReady: return [TransactionAction.Suspend, TransactionAction.Abort]; - case PeerPushPaymentInitiationStatus.Done: + case PeerPushDebitStatus.Done: return [TransactionAction.Delete]; - case PeerPushPaymentInitiationStatus.Expired: + case PeerPushDebitStatus.Expired: return [TransactionAction.Delete]; - case PeerPushPaymentInitiationStatus.Failed: + case PeerPushDebitStatus.Failed: return [TransactionAction.Delete]; } } @@ -745,32 +757,32 @@ export async function abortPeerPushDebitTransaction( }); stopLongpolling(ws, taskId); const transitionInfo = await ws.db - .mktx((x) => [x.peerPushPaymentInitiations]) + .mktx((x) => [x.peerPushDebit]) .runReadWrite(async (tx) => { - const pushDebitRec = await tx.peerPushPaymentInitiations.get(pursePub); + const pushDebitRec = await tx.peerPushDebit.get(pursePub); if (!pushDebitRec) { logger.warn(`peer push debit ${pursePub} not found`); return; } - let newStatus: PeerPushPaymentInitiationStatus | undefined = undefined; + let newStatus: PeerPushDebitStatus | undefined = undefined; switch (pushDebitRec.status) { - case PeerPushPaymentInitiationStatus.PendingReady: - case PeerPushPaymentInitiationStatus.SuspendedReady: - newStatus = PeerPushPaymentInitiationStatus.AbortingDeletePurse; + case PeerPushDebitStatus.PendingReady: + case PeerPushDebitStatus.SuspendedReady: + newStatus = PeerPushDebitStatus.AbortingDeletePurse; break; - case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: - case PeerPushPaymentInitiationStatus.PendingCreatePurse: + case PeerPushDebitStatus.SuspendedCreatePurse: + case PeerPushDebitStatus.PendingCreatePurse: // Network request might already be in-flight! - newStatus = PeerPushPaymentInitiationStatus.AbortingDeletePurse; + newStatus = PeerPushDebitStatus.AbortingDeletePurse; break; - case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh: - case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse: - case PeerPushPaymentInitiationStatus.AbortingRefresh: - case PeerPushPaymentInitiationStatus.Done: - case PeerPushPaymentInitiationStatus.AbortingDeletePurse: - case PeerPushPaymentInitiationStatus.Aborted: - case PeerPushPaymentInitiationStatus.Expired: - case PeerPushPaymentInitiationStatus.Failed: + case PeerPushDebitStatus.SuspendedAbortingRefresh: + case PeerPushDebitStatus.SuspendedAbortingDeletePurse: + case PeerPushDebitStatus.AbortingRefresh: + case PeerPushDebitStatus.Done: + case PeerPushDebitStatus.AbortingDeletePurse: + case PeerPushDebitStatus.Aborted: + case PeerPushDebitStatus.Expired: + case PeerPushDebitStatus.Failed: // Do nothing break; default: @@ -780,7 +792,7 @@ export async function abortPeerPushDebitTransaction( const oldTxState = computePeerPushDebitTransactionState(pushDebitRec); pushDebitRec.status = newStatus; const newTxState = computePeerPushDebitTransactionState(pushDebitRec); - await tx.peerPushPaymentInitiations.put(pushDebitRec); + await tx.peerPushDebit.put(pushDebitRec); return { oldTxState, newTxState, @@ -805,32 +817,32 @@ export async function failPeerPushDebitTransaction( }); stopLongpolling(ws, taskId); const transitionInfo = await ws.db - .mktx((x) => [x.peerPushPaymentInitiations]) + .mktx((x) => [x.peerPushDebit]) .runReadWrite(async (tx) => { - const pushDebitRec = await tx.peerPushPaymentInitiations.get(pursePub); + const pushDebitRec = await tx.peerPushDebit.get(pursePub); if (!pushDebitRec) { logger.warn(`peer push debit ${pursePub} not found`); return; } - let newStatus: PeerPushPaymentInitiationStatus | undefined = undefined; + let newStatus: PeerPushDebitStatus | undefined = undefined; switch (pushDebitRec.status) { - case PeerPushPaymentInitiationStatus.AbortingRefresh: - case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh: + case PeerPushDebitStatus.AbortingRefresh: + case PeerPushDebitStatus.SuspendedAbortingRefresh: // FIXME: What to do about the refresh group? - newStatus = PeerPushPaymentInitiationStatus.Failed; + newStatus = PeerPushDebitStatus.Failed; break; - case PeerPushPaymentInitiationStatus.AbortingDeletePurse: - case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse: - case PeerPushPaymentInitiationStatus.PendingReady: - case PeerPushPaymentInitiationStatus.SuspendedReady: - case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: - case PeerPushPaymentInitiationStatus.PendingCreatePurse: - newStatus = PeerPushPaymentInitiationStatus.Failed; + case PeerPushDebitStatus.AbortingDeletePurse: + case PeerPushDebitStatus.SuspendedAbortingDeletePurse: + case PeerPushDebitStatus.PendingReady: + case PeerPushDebitStatus.SuspendedReady: + case PeerPushDebitStatus.SuspendedCreatePurse: + case PeerPushDebitStatus.PendingCreatePurse: + newStatus = PeerPushDebitStatus.Failed; break; - case PeerPushPaymentInitiationStatus.Done: - case PeerPushPaymentInitiationStatus.Aborted: - case PeerPushPaymentInitiationStatus.Failed: - case PeerPushPaymentInitiationStatus.Expired: + case PeerPushDebitStatus.Done: + case PeerPushDebitStatus.Aborted: + case PeerPushDebitStatus.Failed: + case PeerPushDebitStatus.Expired: // Do nothing break; default: @@ -840,7 +852,7 @@ export async function failPeerPushDebitTransaction( const oldTxState = computePeerPushDebitTransactionState(pushDebitRec); pushDebitRec.status = newStatus; const newTxState = computePeerPushDebitTransactionState(pushDebitRec); - await tx.peerPushPaymentInitiations.put(pushDebitRec); + await tx.peerPushDebit.put(pushDebitRec); return { oldTxState, newTxState, @@ -865,36 +877,35 @@ export async function suspendPeerPushDebitTransaction( }); stopLongpolling(ws, taskId); const transitionInfo = await ws.db - .mktx((x) => [x.peerPushPaymentInitiations]) + .mktx((x) => [x.peerPushDebit]) .runReadWrite(async (tx) => { - const pushDebitRec = await tx.peerPushPaymentInitiations.get(pursePub); + const pushDebitRec = await tx.peerPushDebit.get(pursePub); if (!pushDebitRec) { logger.warn(`peer push debit ${pursePub} not found`); return; } - let newStatus: PeerPushPaymentInitiationStatus | undefined = undefined; + let newStatus: PeerPushDebitStatus | undefined = undefined; switch (pushDebitRec.status) { - case PeerPushPaymentInitiationStatus.PendingCreatePurse: - newStatus = PeerPushPaymentInitiationStatus.SuspendedCreatePurse; + case PeerPushDebitStatus.PendingCreatePurse: + newStatus = PeerPushDebitStatus.SuspendedCreatePurse; break; - case PeerPushPaymentInitiationStatus.AbortingRefresh: - newStatus = PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh; + case PeerPushDebitStatus.AbortingRefresh: + newStatus = PeerPushDebitStatus.SuspendedAbortingRefresh; break; - case PeerPushPaymentInitiationStatus.AbortingDeletePurse: - newStatus = - PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse; + case PeerPushDebitStatus.AbortingDeletePurse: + newStatus = PeerPushDebitStatus.SuspendedAbortingDeletePurse; break; - case PeerPushPaymentInitiationStatus.PendingReady: - newStatus = PeerPushPaymentInitiationStatus.SuspendedReady; + case PeerPushDebitStatus.PendingReady: + newStatus = PeerPushDebitStatus.SuspendedReady; break; - case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse: - case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh: - case PeerPushPaymentInitiationStatus.SuspendedReady: - case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: - case PeerPushPaymentInitiationStatus.Done: - case PeerPushPaymentInitiationStatus.Aborted: - case PeerPushPaymentInitiationStatus.Failed: - case PeerPushPaymentInitiationStatus.Expired: + case PeerPushDebitStatus.SuspendedAbortingDeletePurse: + case PeerPushDebitStatus.SuspendedAbortingRefresh: + case PeerPushDebitStatus.SuspendedReady: + case PeerPushDebitStatus.SuspendedCreatePurse: + case PeerPushDebitStatus.Done: + case PeerPushDebitStatus.Aborted: + case PeerPushDebitStatus.Failed: + case PeerPushDebitStatus.Expired: // Do nothing break; default: @@ -904,7 +915,7 @@ export async function suspendPeerPushDebitTransaction( const oldTxState = computePeerPushDebitTransactionState(pushDebitRec); pushDebitRec.status = newStatus; const newTxState = computePeerPushDebitTransactionState(pushDebitRec); - await tx.peerPushPaymentInitiations.put(pushDebitRec); + await tx.peerPushDebit.put(pushDebitRec); return { oldTxState, newTxState, @@ -929,35 +940,35 @@ export async function resumePeerPushDebitTransaction( }); stopLongpolling(ws, taskId); const transitionInfo = await ws.db - .mktx((x) => [x.peerPushPaymentInitiations]) + .mktx((x) => [x.peerPushDebit]) .runReadWrite(async (tx) => { - const pushDebitRec = await tx.peerPushPaymentInitiations.get(pursePub); + const pushDebitRec = await tx.peerPushDebit.get(pursePub); if (!pushDebitRec) { logger.warn(`peer push debit ${pursePub} not found`); return; } - let newStatus: PeerPushPaymentInitiationStatus | undefined = undefined; + let newStatus: PeerPushDebitStatus | undefined = undefined; switch (pushDebitRec.status) { - case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse: - newStatus = PeerPushPaymentInitiationStatus.AbortingDeletePurse; + case PeerPushDebitStatus.SuspendedAbortingDeletePurse: + newStatus = PeerPushDebitStatus.AbortingDeletePurse; break; - case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh: - newStatus = PeerPushPaymentInitiationStatus.AbortingRefresh; + case PeerPushDebitStatus.SuspendedAbortingRefresh: + newStatus = PeerPushDebitStatus.AbortingRefresh; break; - case PeerPushPaymentInitiationStatus.SuspendedReady: - newStatus = PeerPushPaymentInitiationStatus.PendingReady; + case PeerPushDebitStatus.SuspendedReady: + newStatus = PeerPushDebitStatus.PendingReady; break; - case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: - newStatus = PeerPushPaymentInitiationStatus.PendingCreatePurse; + case PeerPushDebitStatus.SuspendedCreatePurse: + newStatus = PeerPushDebitStatus.PendingCreatePurse; break; - case PeerPushPaymentInitiationStatus.PendingCreatePurse: - case PeerPushPaymentInitiationStatus.AbortingRefresh: - case PeerPushPaymentInitiationStatus.AbortingDeletePurse: - case PeerPushPaymentInitiationStatus.PendingReady: - case PeerPushPaymentInitiationStatus.Done: - case PeerPushPaymentInitiationStatus.Aborted: - case PeerPushPaymentInitiationStatus.Failed: - case PeerPushPaymentInitiationStatus.Expired: + case PeerPushDebitStatus.PendingCreatePurse: + case PeerPushDebitStatus.AbortingRefresh: + case PeerPushDebitStatus.AbortingDeletePurse: + case PeerPushDebitStatus.PendingReady: + case PeerPushDebitStatus.Done: + case PeerPushDebitStatus.Aborted: + case PeerPushDebitStatus.Failed: + case PeerPushDebitStatus.Expired: // Do nothing break; default: @@ -967,7 +978,7 @@ export async function resumePeerPushDebitTransaction( const oldTxState = computePeerPushDebitTransactionState(pushDebitRec); pushDebitRec.status = newStatus; const newTxState = computePeerPushDebitTransactionState(pushDebitRec); - await tx.peerPushPaymentInitiations.put(pushDebitRec); + await tx.peerPushDebit.put(pushDebitRec); return { oldTxState, newTxState, @@ -980,62 +991,62 @@ export async function resumePeerPushDebitTransaction( } export function computePeerPushDebitTransactionState( - ppiRecord: PeerPushPaymentInitiationRecord, + ppiRecord: PeerPushDebitRecord, ): TransactionState { switch (ppiRecord.status) { - case PeerPushPaymentInitiationStatus.PendingCreatePurse: + case PeerPushDebitStatus.PendingCreatePurse: return { major: TransactionMajorState.Pending, minor: TransactionMinorState.CreatePurse, }; - case PeerPushPaymentInitiationStatus.PendingReady: + case PeerPushDebitStatus.PendingReady: return { major: TransactionMajorState.Pending, minor: TransactionMinorState.Ready, }; - case PeerPushPaymentInitiationStatus.Aborted: + case PeerPushDebitStatus.Aborted: return { major: TransactionMajorState.Aborted, }; - case PeerPushPaymentInitiationStatus.AbortingDeletePurse: + case PeerPushDebitStatus.AbortingDeletePurse: return { major: TransactionMajorState.Aborting, minor: TransactionMinorState.DeletePurse, }; - case PeerPushPaymentInitiationStatus.AbortingRefresh: + case PeerPushDebitStatus.AbortingRefresh: return { major: TransactionMajorState.Aborting, minor: TransactionMinorState.Refresh, }; - case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse: + case PeerPushDebitStatus.SuspendedAbortingDeletePurse: return { major: TransactionMajorState.SuspendedAborting, minor: TransactionMinorState.DeletePurse, }; - case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh: + case PeerPushDebitStatus.SuspendedAbortingRefresh: return { major: TransactionMajorState.SuspendedAborting, minor: TransactionMinorState.Refresh, }; - case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: + case PeerPushDebitStatus.SuspendedCreatePurse: return { major: TransactionMajorState.Suspended, minor: TransactionMinorState.CreatePurse, }; - case PeerPushPaymentInitiationStatus.SuspendedReady: + case PeerPushDebitStatus.SuspendedReady: return { major: TransactionMajorState.Suspended, minor: TransactionMinorState.Ready, }; - case PeerPushPaymentInitiationStatus.Done: + case PeerPushDebitStatus.Done: return { major: TransactionMajorState.Done, }; - case PeerPushPaymentInitiationStatus.Failed: + case PeerPushDebitStatus.Failed: return { major: TransactionMajorState.Failed, }; - case PeerPushPaymentInitiationStatus.Expired: + case PeerPushDebitStatus.Expired: return { major: TransactionMajorState.Expired, }; diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts index e37e45c16..6115f848b 100644 --- a/packages/taler-wallet-core/src/operations/pending.ts +++ b/packages/taler-wallet-core/src/operations/pending.ts @@ -26,11 +26,10 @@ import { WalletStoresV1, BackupProviderStateTag, RefreshCoinStatus, - OperationStatusRange, - PeerPushPaymentInitiationStatus, + PeerPushDebitStatus, PeerPullDebitRecordStatus, - PeerPushPaymentIncomingStatus, - PeerPullPaymentInitiationStatus, + PeerPushCreditStatus, + PeerPullPaymentCreditStatus, WithdrawalGroupStatus, RewardRecordStatus, DepositOperationStatus, @@ -39,13 +38,15 @@ import { DepositGroupRecord, RewardRecord, PurchaseRecord, - PeerPullPaymentInitiationRecord, + PeerPullCreditRecord, PeerPullPaymentIncomingRecord, - PeerPushPaymentInitiationRecord, + PeerPushDebitRecord, PeerPushPaymentIncomingRecord, RefundGroupRecord, RefundGroupStatus, ExchangeEntryDbUpdateStatus, + RefreshOperationStatus, + DepositElementStatus, } from "../db.js"; import { PendingOperationsResponse, @@ -136,8 +137,8 @@ export async function iterRecordsForRefresh( let refreshGroups: RefreshGroupRecord[]; if (filter.onlyState === "nonfinal") { const keyRange = GlobalIDB.KeyRange.bound( - OperationStatusRange.ACTIVE_START, - OperationStatusRange.ACTIVE_END, + RefreshOperationStatus.Pending, + RefreshOperationStatus.Suspended, ); refreshGroups = await tx.refreshGroups.indexes.byStatus.getAll(keyRange); } else { @@ -277,8 +278,8 @@ async function gatherDepositPending( ): Promise<void> { await iterRecordsForDeposit(tx, { onlyState: "nonfinal" }, async (dg) => { let deposited = true; - for (const d of dg.depositedPerCoin) { - if (!d) { + for (const d of dg.statusPerCoin) { + if (d === DepositElementStatus.DepositPending) { deposited = false; } } @@ -470,28 +471,26 @@ async function gatherBackupPending( export async function iterRecordsForPeerPullInitiation( tx: GetReadOnlyAccess<{ - peerPullPaymentInitiations: typeof WalletStoresV1.peerPullPaymentInitiations; + peerPullCredit: typeof WalletStoresV1.peerPullCredit; }>, filter: TransactionRecordFilter, - f: (r: PeerPullPaymentInitiationRecord) => Promise<void>, + f: (r: PeerPullCreditRecord) => Promise<void>, ): Promise<void> { if (filter.onlyState === "nonfinal") { const keyRange = GlobalIDB.KeyRange.bound( - PeerPullPaymentInitiationStatus.PendingCreatePurse, - PeerPullPaymentInitiationStatus.AbortingDeletePurse, + PeerPullPaymentCreditStatus.PendingCreatePurse, + PeerPullPaymentCreditStatus.AbortingDeletePurse, ); - await tx.peerPullPaymentInitiations.indexes.byStatus - .iter(keyRange) - .forEachAsync(f); + await tx.peerPullCredit.indexes.byStatus.iter(keyRange).forEachAsync(f); } else { - await tx.peerPullPaymentInitiations.indexes.byStatus.iter().forEachAsync(f); + await tx.peerPullCredit.indexes.byStatus.iter().forEachAsync(f); } } async function gatherPeerPullInitiationPending( ws: InternalWalletState, tx: GetReadOnlyAccess<{ - peerPullPaymentInitiations: typeof WalletStoresV1.peerPullPaymentInitiations; + peerPullCredit: typeof WalletStoresV1.peerPullCredit; operationRetries: typeof WalletStoresV1.operationRetries; }>, now: AbsoluteTime, @@ -518,7 +517,7 @@ async function gatherPeerPullInitiationPending( export async function iterRecordsForPeerPullDebit( tx: GetReadOnlyAccess<{ - peerPullPaymentIncoming: typeof WalletStoresV1.peerPullPaymentIncoming; + peerPullDebit: typeof WalletStoresV1.peerPullDebit; }>, filter: TransactionRecordFilter, f: (r: PeerPullPaymentIncomingRecord) => Promise<void>, @@ -528,18 +527,16 @@ export async function iterRecordsForPeerPullDebit( PeerPullDebitRecordStatus.PendingDeposit, PeerPullDebitRecordStatus.AbortingRefresh, ); - await tx.peerPullPaymentIncoming.indexes.byStatus - .iter(keyRange) - .forEachAsync(f); + await tx.peerPullDebit.indexes.byStatus.iter(keyRange).forEachAsync(f); } else { - await tx.peerPullPaymentIncoming.indexes.byStatus.iter().forEachAsync(f); + await tx.peerPullDebit.indexes.byStatus.iter().forEachAsync(f); } } async function gatherPeerPullDebitPending( ws: InternalWalletState, tx: GetReadOnlyAccess<{ - peerPullPaymentIncoming: typeof WalletStoresV1.peerPullPaymentIncoming; + peerPullDebit: typeof WalletStoresV1.peerPullDebit; operationRetries: typeof WalletStoresV1.operationRetries; }>, now: AbsoluteTime, @@ -558,7 +555,7 @@ async function gatherPeerPullDebitPending( ...getPendingCommon(ws, opId, timestampDue), givesLifeness: true, retryInfo: retryRecord?.retryInfo, - peerPullPaymentIncomingId: pi.peerPullPaymentIncomingId, + peerPullDebitId: pi.peerPullDebitId, }); }, ); @@ -566,28 +563,26 @@ async function gatherPeerPullDebitPending( export async function iterRecordsForPeerPushInitiation( tx: GetReadOnlyAccess<{ - peerPushPaymentInitiations: typeof WalletStoresV1.peerPushPaymentInitiations; + peerPushDebit: typeof WalletStoresV1.peerPushDebit; }>, filter: TransactionRecordFilter, - f: (r: PeerPushPaymentInitiationRecord) => Promise<void>, + f: (r: PeerPushDebitRecord) => Promise<void>, ): Promise<void> { if (filter.onlyState === "nonfinal") { const keyRange = GlobalIDB.KeyRange.bound( - PeerPushPaymentInitiationStatus.PendingCreatePurse, - PeerPushPaymentInitiationStatus.AbortingRefresh, + PeerPushDebitStatus.PendingCreatePurse, + PeerPushDebitStatus.AbortingRefresh, ); - await tx.peerPushPaymentInitiations.indexes.byStatus - .iter(keyRange) - .forEachAsync(f); + await tx.peerPushDebit.indexes.byStatus.iter(keyRange).forEachAsync(f); } else { - await tx.peerPushPaymentInitiations.indexes.byStatus.iter().forEachAsync(f); + await tx.peerPushDebit.indexes.byStatus.iter().forEachAsync(f); } } async function gatherPeerPushInitiationPending( ws: InternalWalletState, tx: GetReadOnlyAccess<{ - peerPushPaymentInitiations: typeof WalletStoresV1.peerPushPaymentInitiations; + peerPushDebit: typeof WalletStoresV1.peerPushDebit; operationRetries: typeof WalletStoresV1.operationRetries; }>, now: AbsoluteTime, @@ -614,36 +609,34 @@ async function gatherPeerPushInitiationPending( export async function iterRecordsForPeerPushCredit( tx: GetReadOnlyAccess<{ - peerPushPaymentIncoming: typeof WalletStoresV1.peerPushPaymentIncoming; + peerPushCredit: typeof WalletStoresV1.peerPushCredit; }>, filter: TransactionRecordFilter, f: (r: PeerPushPaymentIncomingRecord) => Promise<void>, ): Promise<void> { if (filter.onlyState === "nonfinal") { const keyRange = GlobalIDB.KeyRange.bound( - PeerPushPaymentIncomingStatus.PendingMerge, - PeerPushPaymentIncomingStatus.PendingWithdrawing, + PeerPushCreditStatus.PendingMerge, + PeerPushCreditStatus.PendingWithdrawing, ); - await tx.peerPushPaymentIncoming.indexes.byStatus - .iter(keyRange) - .forEachAsync(f); + await tx.peerPushCredit.indexes.byStatus.iter(keyRange).forEachAsync(f); } else { - await tx.peerPushPaymentIncoming.indexes.byStatus.iter().forEachAsync(f); + await tx.peerPushCredit.indexes.byStatus.iter().forEachAsync(f); } } async function gatherPeerPushCreditPending( ws: InternalWalletState, tx: GetReadOnlyAccess<{ - peerPushPaymentIncoming: typeof WalletStoresV1.peerPushPaymentIncoming; + peerPushCredit: typeof WalletStoresV1.peerPushCredit; operationRetries: typeof WalletStoresV1.operationRetries; }>, now: AbsoluteTime, resp: PendingOperationsResponse, ): Promise<void> { const keyRange = GlobalIDB.KeyRange.bound( - PeerPushPaymentIncomingStatus.PendingMerge, - PeerPushPaymentIncomingStatus.PendingWithdrawing, + PeerPushCreditStatus.PendingMerge, + PeerPushCreditStatus.PendingWithdrawing, ); await iterRecordsForPeerPushCredit( tx, @@ -658,7 +651,7 @@ async function gatherPeerPushCreditPending( ...getPendingCommon(ws, opId, timestampDue), givesLifeness: true, retryInfo: retryRecord?.retryInfo, - peerPushPaymentIncomingId: pi.peerPushPaymentIncomingId, + peerPushCreditId: pi.peerPushCreditId, }); }, ); @@ -682,10 +675,10 @@ export async function getPendingOperations( x.depositGroups, x.recoupGroups, x.operationRetries, - x.peerPullPaymentInitiations, - x.peerPushPaymentInitiations, - x.peerPullPaymentIncoming, - x.peerPushPaymentIncoming, + x.peerPullCredit, + x.peerPushDebit, + x.peerPullDebit, + x.peerPushCredit, ]) .runReadWrite(async (tx) => { const resp: PendingOperationsResponse = { diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts index fb356f0fc..75adbc860 100644 --- a/packages/taler-wallet-core/src/operations/refresh.ts +++ b/packages/taler-wallet-core/src/operations/refresh.ts @@ -76,7 +76,11 @@ import { RefreshReasonDetails, WalletStoresV1, } from "../db.js"; -import { isWithdrawableDenom, PendingTaskType } from "../index.js"; +import { + isWithdrawableDenom, + PendingTaskType, + RefreshSessionRecord, +} from "../index.js"; import { EXCHANGE_COINS_LOCK, InternalWalletState, @@ -130,11 +134,7 @@ export function getTotalRefreshCost( const resultingAmount = Amounts.add( Amounts.zeroOfCurrency(withdrawAmount.currency), ...withdrawDenoms.selectedDenoms.map( - (d) => - Amounts.mult( - DenominationRecord.getValue(denomMap[d.denomPubHash]), - d.count, - ).amount, + (d) => Amounts.mult(denomMap[d.denomPubHash].value, d.count).amount, ), ).amount; const totalCost = Amounts.sub(amountLeft, resultingAmount).amount; @@ -170,18 +170,23 @@ function updateGroupStatus(rg: RefreshGroupRecord): { final: boolean } { /** * Create a refresh session for one particular coin inside a refresh group. + * + * If the session already exists, return the existing one. + * + * If the session doesn't need to be created (refresh group gone or session already + * finished), return undefined. */ -async function refreshCreateSession( +async function provideRefreshSession( ws: InternalWalletState, refreshGroupId: string, coinIndex: number, -): Promise<void> { +): Promise<RefreshSessionRecord | undefined> { logger.trace( `creating refresh session for coin ${coinIndex} in refresh group ${refreshGroupId}`, ); const d = await ws.db - .mktx((x) => [x.refreshGroups, x.coins]) + .mktx((x) => [x.refreshGroups, x.coins, x.refreshSessions]) .runReadWrite(async (tx) => { const refreshGroup = await tx.refreshGroups.get(refreshGroupId); if (!refreshGroup) { @@ -192,21 +197,24 @@ async function refreshCreateSession( ) { return; } - const existingRefreshSession = - refreshGroup.refreshSessionPerCoin[coinIndex]; - if (existingRefreshSession) { - return; - } + const existingRefreshSession = await tx.refreshSessions.get([ + refreshGroupId, + coinIndex, + ]); const oldCoinPub = refreshGroup.oldCoinPubs[coinIndex]; const coin = await tx.coins.get(oldCoinPub); if (!coin) { throw Error("Can't refresh, coin not found"); } - return { refreshGroup, coin }; + return { refreshGroup, coin, existingRefreshSession }; }); if (!d) { - return; + return undefined; + } + + if (d.existingRefreshSession) { + return d.existingRefreshSession; } const { refreshGroup, coin } = d; @@ -288,17 +296,23 @@ async function refreshCreateSession( const sessionSecretSeed = encodeCrock(getRandomBytes(64)); // Store refresh session for this coin in the database. - await ws.db - .mktx((x) => [x.refreshGroups, x.coins]) + const newSession = await ws.db + .mktx((x) => [x.refreshGroups, x.coins, x.refreshSessions]) .runReadWrite(async (tx) => { const rg = await tx.refreshGroups.get(refreshGroupId); if (!rg) { return; } - if (rg.refreshSessionPerCoin[coinIndex]) { + const existingSession = await tx.refreshSessions.get([ + refreshGroupId, + coinIndex, + ]); + if (existingSession) { return; } - rg.refreshSessionPerCoin[coinIndex] = { + const newSession: RefreshSessionRecord = { + coinIndex, + refreshGroupId, norevealIndex: undefined, sessionSecretSeed: sessionSecretSeed, newDenoms: newCoinDenoms.selectedDenoms.map((x) => ({ @@ -307,11 +321,13 @@ async function refreshCreateSession( })), amountRefreshOutput: Amounts.stringify(newCoinDenoms.totalCoinValue), }; - await tx.refreshGroups.put(rg); + await tx.refreshSessions.put(newSession); + return newSession; }); logger.trace( `created refresh session for coin #${coinIndex} in ${refreshGroupId}`, ); + return newSession; } function getRefreshRequestTimeout(rg: RefreshGroupRecord): Duration { @@ -326,13 +342,16 @@ async function refreshMelt( coinIndex: number, ): Promise<void> { const d = await ws.db - .mktx((x) => [x.refreshGroups, x.coins, x.denominations]) + .mktx((x) => [x.refreshGroups, x.refreshSessions, x.coins, x.denominations]) .runReadWrite(async (tx) => { const refreshGroup = await tx.refreshGroups.get(refreshGroupId); if (!refreshGroup) { return; } - const refreshSession = refreshGroup.refreshSessionPerCoin[coinIndex]; + const refreshSession = await tx.refreshSessions.get([ + refreshGroupId, + coinIndex, + ]); if (!refreshSession) { return; } @@ -442,7 +461,12 @@ async function refreshMelt( if (resp.status === HttpStatusCode.NotFound) { const errDetails = await readUnexpectedResponseDetails(resp); const transitionInfo = await ws.db - .mktx((x) => [x.refreshGroups, x.coins, x.coinAvailability]) + .mktx((x) => [ + x.refreshGroups, + x.refreshSessions, + x.coins, + x.coinAvailability, + ]) .runReadWrite(async (tx) => { const rg = await tx.refreshGroups.get(refreshGroupId); if (!rg) { @@ -456,12 +480,22 @@ async function refreshMelt( } const oldTxState = computeRefreshTransactionState(rg); rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Failed; - rg.lastErrorPerCoin[coinIndex] = errDetails; + const refreshSession = await tx.refreshSessions.get([ + refreshGroupId, + coinIndex, + ]); + if (!refreshSession) { + throw Error( + "db invariant failed: missing refresh session in database", + ); + } + refreshSession.lastError = errDetails; const updateRes = updateGroupStatus(rg); if (updateRes.final) { await makeCoinsVisible(ws, tx, transactionId); } await tx.refreshGroups.put(rg); + await tx.refreshSessions.put(refreshSession); const newTxState = computeRefreshTransactionState(rg); return { oldTxState, @@ -493,7 +527,7 @@ async function refreshMelt( refreshSession.norevealIndex = norevealIndex; await ws.db - .mktx((x) => [x.refreshGroups]) + .mktx((x) => [x.refreshGroups, x.refreshSessions]) .runReadWrite(async (tx) => { const rg = await tx.refreshGroups.get(refreshGroupId); if (!rg) { @@ -502,7 +536,7 @@ async function refreshMelt( if (rg.timestampFinished) { return; } - const rs = rg.refreshSessionPerCoin[coinIndex]; + const rs = await tx.refreshSessions.get([refreshGroupId, coinIndex]); if (!rs) { return; } @@ -510,7 +544,7 @@ async function refreshMelt( return; } rs.norevealIndex = norevealIndex; - await tx.refreshGroups.put(rg); + await tx.refreshSessions.put(rs); }); } @@ -581,13 +615,16 @@ async function refreshReveal( `doing refresh reveal for ${refreshGroupId} (old coin ${coinIndex})`, ); const d = await ws.db - .mktx((x) => [x.refreshGroups, x.coins, x.denominations]) + .mktx((x) => [x.refreshGroups, x.refreshSessions, x.coins, x.denominations]) .runReadOnly(async (tx) => { const refreshGroup = await tx.refreshGroups.get(refreshGroupId); if (!refreshGroup) { return; } - const refreshSession = refreshGroup.refreshSessionPerCoin[coinIndex]; + const refreshSession = await tx.refreshSessions.get([ + refreshGroupId, + coinIndex, + ]); if (!refreshSession) { return; } @@ -755,6 +792,7 @@ async function refreshReveal( x.denominations, x.coinAvailability, x.refreshGroups, + x.refreshSessions, ]) .runReadWrite(async (tx) => { const rg = await tx.refreshGroups.get(refreshGroupId); @@ -762,7 +800,7 @@ async function refreshReveal( logger.warn("no refresh session found"); return; } - const rs = rg.refreshSessionPerCoin[coinIndex]; + const rs = await tx.refreshSessions.get([refreshGroupId, coinIndex]); if (!rs) { return; } @@ -858,10 +896,15 @@ async function processRefreshSession( logger.trace( `processing refresh session for coin ${coinIndex} of group ${refreshGroupId}`, ); - let refreshGroup = await ws.db - .mktx((x) => [x.refreshGroups]) + let { refreshGroup, refreshSession } = await ws.db + .mktx((x) => [x.refreshGroups, x.refreshSessions]) .runReadOnly(async (tx) => { - return tx.refreshGroups.get(refreshGroupId); + const rg = await tx.refreshGroups.get(refreshGroupId); + const rs = await tx.refreshSessions.get([refreshGroupId, coinIndex]); + return { + refreshGroup: rg, + refreshSession: rs, + }; }); if (!refreshGroup) { return; @@ -869,18 +912,9 @@ async function processRefreshSession( if (refreshGroup.statusPerCoin[coinIndex] === RefreshCoinStatus.Finished) { return; } - if (!refreshGroup.refreshSessionPerCoin[coinIndex]) { - await refreshCreateSession(ws, refreshGroupId, coinIndex); - refreshGroup = await ws.db - .mktx((x) => [x.refreshGroups]) - .runReadOnly(async (tx) => { - return tx.refreshGroups.get(refreshGroupId); - }); - if (!refreshGroup) { - return; - } + if (!refreshSession) { + refreshSession = await provideRefreshSession(ws, refreshGroupId, coinIndex); } - const refreshSession = refreshGroup.refreshSessionPerCoin[coinIndex]; if (!refreshSession) { if (refreshGroup.statusPerCoin[coinIndex] !== RefreshCoinStatus.Finished) { throw Error( @@ -1058,13 +1092,11 @@ export async function createRefreshGroup( timestampFinished: undefined, statusPerCoin: oldCoinPubs.map(() => RefreshCoinStatus.Pending), oldCoinPubs: oldCoinPubs.map((x) => x.coinPub), - lastErrorPerCoin: {}, reasonDetails, reason, refreshGroupId, - refreshSessionPerCoin: oldCoinPubs.map(() => undefined), inputPerCoin: oldCoinPubs.map((x) => x.amount), - estimatedOutputPerCoin: estimatedOutputPerCoin.map((x) => + expectedOutputPerCoin: estimatedOutputPerCoin.map((x) => Amounts.stringify(x), ), timestampCreated: TalerPreciseTimestamp.now(), @@ -1164,11 +1196,7 @@ export async function autoRefresh( if (AbsoluteTime.isExpired(executeThreshold)) { refreshCoins.push({ coinPub: coin.coinPub, - amount: Amounts.stringify({ - value: denom.amountVal, - fraction: denom.amountFrac, - currency: denom.currency, - }), + amount: denom.value, }); } else { const checkThreshold = getAutoRefreshCheckThreshold(denom); diff --git a/packages/taler-wallet-core/src/operations/reward.ts b/packages/taler-wallet-core/src/operations/reward.ts index 6f9d3ce85..6ae021174 100644 --- a/packages/taler-wallet-core/src/operations/reward.ts +++ b/packages/taler-wallet-core/src/operations/reward.ts @@ -108,7 +108,7 @@ export function computeRewardTransactionStatus( major: TransactionMajorState.Dialog, minor: TransactionMinorState.Proposed, }; - case RewardRecordStatus.SuspendidPickup: + case RewardRecordStatus.SuspendedPickup: return { major: TransactionMajorState.Pending, minor: TransactionMinorState.Pickup, @@ -128,7 +128,7 @@ export function computeTipTransactionActions( return [TransactionAction.Delete]; case RewardRecordStatus.PendingPickup: return [TransactionAction.Suspend, TransactionAction.Fail]; - case RewardRecordStatus.SuspendidPickup: + case RewardRecordStatus.SuspendedPickup: return [TransactionAction.Resume, TransactionAction.Fail]; case RewardRecordStatus.DialogAccept: return [TransactionAction.Abort]; @@ -255,7 +255,7 @@ export async function processTip( case RewardRecordStatus.Aborted: case RewardRecordStatus.DialogAccept: case RewardRecordStatus.Done: - case RewardRecordStatus.SuspendidPickup: + case RewardRecordStatus.SuspendedPickup: return TaskRunResult.finished(); } @@ -496,12 +496,12 @@ export async function suspendRewardTransaction( let newStatus: RewardRecordStatus | undefined = undefined; switch (tipRec.status) { case RewardRecordStatus.Done: - case RewardRecordStatus.SuspendidPickup: + case RewardRecordStatus.SuspendedPickup: case RewardRecordStatus.Aborted: case RewardRecordStatus.DialogAccept: break; case RewardRecordStatus.PendingPickup: - newStatus = RewardRecordStatus.SuspendidPickup; + newStatus = RewardRecordStatus.SuspendedPickup; break; default: @@ -551,7 +551,7 @@ export async function resumeTipTransaction( case RewardRecordStatus.Aborted: case RewardRecordStatus.DialogAccept: break; - case RewardRecordStatus.SuspendidPickup: + case RewardRecordStatus.SuspendedPickup: newStatus = RewardRecordStatus.PendingPickup; break; default: @@ -608,7 +608,7 @@ export async function abortTipTransaction( case RewardRecordStatus.PendingPickup: case RewardRecordStatus.DialogAccept: break; - case RewardRecordStatus.SuspendidPickup: + case RewardRecordStatus.SuspendedPickup: newStatus = RewardRecordStatus.Aborted; break; default: diff --git a/packages/taler-wallet-core/src/operations/testing.ts b/packages/taler-wallet-core/src/operations/testing.ts index 1962c965c..f71d842c7 100644 --- a/packages/taler-wallet-core/src/operations/testing.ts +++ b/packages/taler-wallet-core/src/operations/testing.ts @@ -700,7 +700,7 @@ export async function runIntegrationTest2( }); await confirmPeerPushCredit(ws, { - peerPushPaymentIncomingId: peerPushCredit.peerPushPaymentIncomingId, + peerPushCreditId: peerPushCredit.peerPushCreditId, }); const peerPullInit = await initiatePeerPullPayment(ws, { @@ -723,7 +723,7 @@ export async function runIntegrationTest2( }); await confirmPeerPullDebit(ws, { - peerPullPaymentIncomingId: peerPullInc.peerPullPaymentIncomingId, + peerPullDebitId: peerPullInc.peerPullDebitId, }); await waitUntilDone(ws); diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index 7f5302b25..7bdb9af5b 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -50,10 +50,10 @@ import { OperationRetryRecord, PeerPullPaymentIncomingRecord, PeerPullDebitRecordStatus, - PeerPullPaymentInitiationRecord, + PeerPullCreditRecord, PeerPushPaymentIncomingRecord, - PeerPushPaymentIncomingStatus, - PeerPushPaymentInitiationRecord, + PeerPushCreditStatus, + PeerPushDebitRecord, PurchaseRecord, PurchaseStatus, RefreshGroupRecord, @@ -154,7 +154,18 @@ import { resumePeerPushDebitTransaction, abortPeerPushDebitTransaction, } from "./pay-peer-push-debit.js"; -import { iterRecordsForDeposit, iterRecordsForPeerPullDebit, iterRecordsForPeerPullInitiation, iterRecordsForPeerPushCredit, iterRecordsForPeerPushInitiation, iterRecordsForPurchase, iterRecordsForRefresh, iterRecordsForRefund, iterRecordsForReward, iterRecordsForWithdrawal } from "./pending.js"; +import { + iterRecordsForDeposit, + iterRecordsForPeerPullDebit, + iterRecordsForPeerPullInitiation, + iterRecordsForPeerPushCredit, + iterRecordsForPeerPushInitiation, + iterRecordsForPurchase, + iterRecordsForRefresh, + iterRecordsForRefund, + iterRecordsForReward, + iterRecordsForWithdrawal, +} from "./pending.js"; const logger = new Logger("taler-wallet-core:transactions.ts"); @@ -335,11 +346,9 @@ export async function getTransactionById( } case TransactionType.PeerPullDebit: { return await ws.db - .mktx((x) => [x.peerPullPaymentIncoming]) + .mktx((x) => [x.peerPullDebit]) .runReadWrite(async (tx) => { - const debit = await tx.peerPullPaymentIncoming.get( - parsedTx.peerPullPaymentIncomingId, - ); + const debit = await tx.peerPullDebit.get(parsedTx.peerPullDebitId); if (!debit) throw Error("not found"); return buildTransactionForPullPaymentDebit(debit); }); @@ -347,11 +356,9 @@ export async function getTransactionById( case TransactionType.PeerPushDebit: { return await ws.db - .mktx((x) => [x.peerPushPaymentInitiations, x.contractTerms]) + .mktx((x) => [x.peerPushDebit, x.contractTerms]) .runReadWrite(async (tx) => { - const debit = await tx.peerPushPaymentInitiations.get( - parsedTx.pursePub, - ); + const debit = await tx.peerPushDebit.get(parsedTx.pursePub); if (!debit) throw Error("not found"); const ct = await tx.contractTerms.get(debit.contractTermsHash); checkDbInvariant(!!ct); @@ -363,18 +370,16 @@ export async function getTransactionById( } case TransactionType.PeerPushCredit: { - const peerPushPaymentIncomingId = parsedTx.peerPushPaymentIncomingId; + const peerPushCreditId = parsedTx.peerPushCreditId; return await ws.db .mktx((x) => [ - x.peerPushPaymentIncoming, + x.peerPushCredit, x.contractTerms, x.withdrawalGroups, x.operationRetries, ]) .runReadWrite(async (tx) => { - const pushInc = await tx.peerPushPaymentIncoming.get( - peerPushPaymentIncomingId, - ); + const pushInc = await tx.peerPushCredit.get(peerPushCreditId); if (!pushInc) throw Error("not found"); const ct = await tx.contractTerms.get(pushInc.contractTermsHash); checkDbInvariant(!!ct); @@ -405,13 +410,13 @@ export async function getTransactionById( const pursePub = parsedTx.pursePub; return await ws.db .mktx((x) => [ - x.peerPullPaymentInitiations, + x.peerPullCredit, x.contractTerms, x.withdrawalGroups, x.operationRetries, ]) .runReadWrite(async (tx) => { - const pushInc = await tx.peerPullPaymentInitiations.get(pursePub); + const pushInc = await tx.peerPullCredit.get(pursePub); if (!pushInc) throw Error("not found"); const ct = await tx.contractTerms.get(pushInc.contractTermsHash); checkDbInvariant(!!ct); @@ -442,7 +447,7 @@ export async function getTransactionById( } function buildTransactionForPushPaymentDebit( - pi: PeerPushPaymentInitiationRecord, + pi: PeerPushDebitRecord, contractTerms: PeerContractTerms, ort?: OperationRetryRecord, ): Transaction { @@ -490,14 +495,14 @@ function buildTransactionForPullPaymentDebit( timestamp: pi.timestampCreated, transactionId: constructTransactionIdentifier({ tag: TransactionType.PeerPullDebit, - peerPullPaymentIncomingId: pi.peerPullPaymentIncomingId, + peerPullDebitId: pi.peerPullDebitId, }), ...(ort?.lastError ? { error: ort.lastError } : {}), }; } function buildTransactionForPeerPullCredit( - pullCredit: PeerPullPaymentInitiationRecord, + pullCredit: PeerPullCreditRecord, pullCreditOrt: OperationRetryRecord | undefined, peerContractTerms: PeerContractTerms, wsr: WithdrawalGroupRecord | undefined, @@ -532,8 +537,8 @@ function buildTransactionForPeerPullCredit( // Old transactions don't have it! timestamp: pullCredit.mergeTimestamp ?? TalerPreciseTimestamp.now(), info: { - expiration: wsr.wgInfo.contractTerms.purse_expiration, - summary: wsr.wgInfo.contractTerms.summary, + expiration: peerContractTerms.purse_expiration, + summary: peerContractTerms.summary, }, talerUri: stringifyPayPullUri({ exchangeBaseUrl: wsr.exchangeBaseUrl, @@ -600,13 +605,13 @@ function buildTransactionForPeerPushCredit( amountRaw: Amounts.stringify(wsr.instructedAmount), exchangeBaseUrl: wsr.exchangeBaseUrl, info: { - expiration: wsr.wgInfo.contractTerms.purse_expiration, - summary: wsr.wgInfo.contractTerms.summary, + expiration: peerContractTerms.purse_expiration, + summary: peerContractTerms.summary, }, timestamp: wsr.timestampStart, transactionId: constructTransactionIdentifier({ tag: TransactionType.PeerPushCredit, - peerPushPaymentIncomingId: pushInc.peerPushPaymentIncomingId, + peerPushCreditId: pushInc.peerPushCreditId, }), kycUrl: pushInc.kycUrl, ...(wsrOrt?.lastError ? { error: wsrOrt.lastError } : {}), @@ -629,7 +634,7 @@ function buildTransactionForPeerPushCredit( timestamp: pushInc.timestamp, transactionId: constructTransactionIdentifier({ tag: TransactionType.PeerPushCredit, - peerPushPaymentIncomingId: pushInc.peerPushPaymentIncomingId, + peerPushCreditId: pushInc.peerPushCreditId, }), ...(pushOrt?.lastError ? { error: pushOrt.lastError } : {}), }; @@ -654,7 +659,7 @@ function buildTransactionForBankIntegratedWithdraw( reservePub: wgRecord.reservePub, bankConfirmationUrl: wgRecord.wgInfo.bankInfo.confirmUrl, reserveIsReady: - wgRecord.status === WithdrawalGroupStatus.Finished || + wgRecord.status === WithdrawalGroupStatus.Done || wgRecord.status === WithdrawalGroupStatus.PendingReady, }, kycUrl: wgRecord.kycUrl, @@ -698,7 +703,7 @@ function buildTransactionForManualWithdraw( reservePub: withdrawalGroup.reservePub, exchangePaytoUris, reserveIsReady: - withdrawalGroup.status === WithdrawalGroupStatus.Finished || + withdrawalGroup.status === WithdrawalGroupStatus.Done || withdrawalGroup.status === WithdrawalGroupStatus.PendingReady, }, kycUrl: withdrawalGroup.kycUrl, @@ -755,7 +760,7 @@ function buildTransactionForRefresh( ).amount; const outputAmount = Amounts.sumOrZero( refreshGroupRecord.currency, - refreshGroupRecord.estimatedOutputPerCoin, + refreshGroupRecord.expectedOutputPerCoin, ).amount; return { type: TransactionType.Refresh, @@ -786,8 +791,8 @@ function buildTransactionForDeposit( ort?: OperationRetryRecord, ): Transaction { let deposited = true; - for (const d of dg.depositedPerCoin) { - if (!d) { + for (const d of dg.statusPerCoin) { + if (d == DepositElementStatus.DepositPending) { deposited = false; } } @@ -796,7 +801,7 @@ function buildTransactionForDeposit( type: TransactionType.Deposit, txState: computeDepositTransactionStatus(dg), txActions: computeDepositTransactionActions(dg), - amountRaw: Amounts.stringify(dg.effectiveDepositAmount), + amountRaw: Amounts.stringify(dg.counterpartyEffectiveDepositAmount), amountEffective: Amounts.stringify(dg.totalPayCost), timestamp: dg.timestampCreated, targetPaytoUri: dg.wire.payto_uri, @@ -807,11 +812,11 @@ function buildTransactionForDeposit( }), wireTransferProgress: (100 * - dg.transactionPerCoin.reduce( + dg.statusPerCoin.reduce( (prev, cur) => prev + (cur === DepositElementStatus.Wired ? 1 : 0), 0, )) / - dg.transactionPerCoin.length, + dg.statusPerCoin.length, depositGroupId: dg.depositGroupId, trackingState: Object.values(dg.trackingState ?? {}), deposited, @@ -879,7 +884,6 @@ async function buildTransactionForPurchase( const info: OrderShortInfo = { merchant: contractData.merchant, orderId: contractData.orderId, - products: contractData.products, summary: contractData.summary, summary_i18n: contractData.summaryI18n, contractTermsHash: contractData.contractTermsHash, @@ -944,10 +948,10 @@ export async function getTransactions( x.exchangeDetails, x.exchanges, x.operationRetries, - x.peerPullPaymentIncoming, - x.peerPushPaymentInitiations, - x.peerPushPaymentIncoming, - x.peerPullPaymentInitiations, + x.peerPullDebit, + x.peerPushDebit, + x.peerPushCredit, + x.peerPullCredit, x.planchets, x.purchases, x.contractTerms, @@ -985,7 +989,7 @@ export async function getTransactions( } if ( pi.status !== PeerPullDebitRecordStatus.PendingDeposit && - pi.status !== PeerPullDebitRecordStatus.DonePaid + pi.status !== PeerPullDebitRecordStatus.Done ) { return; } @@ -1004,7 +1008,7 @@ export async function getTransactions( if (shouldSkipSearch(transactionsRequest, [])) { return; } - if (pi.status === PeerPushPaymentIncomingStatus.DialogProposed) { + if (pi.status === PeerPushCreditStatus.DialogProposed) { // We don't report proposed push credit transactions, user needs // to scan URI again and confirm to see it. return; @@ -1033,7 +1037,7 @@ export async function getTransactions( ), ); }); - + await iterRecordsForPeerPullInitiation(tx, filter, async (pi) => { const currency = Amounts.currencyOf(pi.amount); if (shouldSkipCurrency(transactionsRequest, currency)) { @@ -1078,7 +1082,7 @@ export async function getTransactions( ); transactions.push(buildTransactionForRefund(refundGroup, contractData)); }); - + await iterRecordsForRefresh(tx, filter, async (rg) => { if (shouldSkipCurrency(transactionsRequest, rg.currency)) { return; @@ -1099,7 +1103,7 @@ export async function getTransactions( } }); - await iterRecordsForWithdrawal(tx, filter ,async (wsr) => { + await iterRecordsForWithdrawal(tx, filter, async (wsr) => { if ( shouldSkipCurrency( transactionsRequest, @@ -1268,9 +1272,9 @@ export async function getTransactions( export type ParsedTransactionIdentifier = | { tag: TransactionType.Deposit; depositGroupId: string } | { tag: TransactionType.Payment; proposalId: string } - | { tag: TransactionType.PeerPullDebit; peerPullPaymentIncomingId: string } + | { tag: TransactionType.PeerPullDebit; peerPullDebitId: string } | { tag: TransactionType.PeerPullCredit; pursePub: string } - | { tag: TransactionType.PeerPushCredit; peerPushPaymentIncomingId: string } + | { tag: TransactionType.PeerPushCredit; peerPushCreditId: string } | { tag: TransactionType.PeerPushDebit; pursePub: string } | { tag: TransactionType.Refresh; refreshGroupId: string } | { tag: TransactionType.Refund; refundGroupId: string } @@ -1289,9 +1293,9 @@ export function constructTransactionIdentifier( case TransactionType.PeerPullCredit: return `txn:${pTxId.tag}:${pTxId.pursePub}` as TransactionIdStr; case TransactionType.PeerPullDebit: - return `txn:${pTxId.tag}:${pTxId.peerPullPaymentIncomingId}` as TransactionIdStr; + return `txn:${pTxId.tag}:${pTxId.peerPullDebitId}` as TransactionIdStr; case TransactionType.PeerPushCredit: - return `txn:${pTxId.tag}:${pTxId.peerPushPaymentIncomingId}` as TransactionIdStr; + return `txn:${pTxId.tag}:${pTxId.peerPushCreditId}` as TransactionIdStr; case TransactionType.PeerPushDebit: return `txn:${pTxId.tag}:${pTxId.pursePub}` as TransactionIdStr; case TransactionType.Refresh: @@ -1337,12 +1341,12 @@ export function parseTransactionIdentifier( case TransactionType.PeerPullDebit: return { tag: TransactionType.PeerPullDebit, - peerPullPaymentIncomingId: rest[0], + peerPullDebitId: rest[0], }; case TransactionType.PeerPushCredit: return { tag: TransactionType.PeerPushCredit, - peerPushPaymentIncomingId: rest[0], + peerPushCreditId: rest[0], }; case TransactionType.PeerPushDebit: return { tag: TransactionType.PeerPushDebit, pursePub: rest[0] }; @@ -1455,7 +1459,7 @@ export async function retryTransaction( case TransactionType.PeerPullDebit: { const taskId = constructTaskIdentifier({ tag: PendingTaskType.PeerPullDebit, - peerPullPaymentIncomingId: parsedTx.peerPullPaymentIncomingId, + peerPullDebitId: parsedTx.peerPullDebitId, }); await resetPendingTaskTimeout(ws, taskId); stopLongpolling(ws, taskId); @@ -1464,7 +1468,7 @@ export async function retryTransaction( case TransactionType.PeerPushCredit: { const taskId = constructTaskIdentifier({ tag: PendingTaskType.PeerPushCredit, - peerPushPaymentIncomingId: parsedTx.peerPushPaymentIncomingId, + peerPushCreditId: parsedTx.peerPushCreditId, }); await resetPendingTaskTimeout(ws, taskId); stopLongpolling(ws, taskId); @@ -1522,10 +1526,10 @@ export async function suspendTransaction( await suspendPeerPushDebitTransaction(ws, tx.pursePub); break; case TransactionType.PeerPullDebit: - await suspendPeerPullDebitTransaction(ws, tx.peerPullPaymentIncomingId); + await suspendPeerPullDebitTransaction(ws, tx.peerPullDebitId); break; case TransactionType.PeerPushCredit: - await suspendPeerPushCreditTransaction(ws, tx.peerPushPaymentIncomingId); + await suspendPeerPushCreditTransaction(ws, tx.peerPushCreditId); break; case TransactionType.Refund: throw Error("refund transactions can't be suspended or resumed"); @@ -1568,10 +1572,10 @@ export async function failTransaction( await failPeerPullCreditTransaction(ws, tx.pursePub); return; case TransactionType.PeerPullDebit: - await failPeerPullDebitTransaction(ws, tx.peerPullPaymentIncomingId); + await failPeerPullDebitTransaction(ws, tx.peerPullDebitId); return; case TransactionType.PeerPushCredit: - await failPeerPushCreditTransaction(ws, tx.peerPushPaymentIncomingId); + await failPeerPushCreditTransaction(ws, tx.peerPushCreditId); return; case TransactionType.PeerPushDebit: await failPeerPushDebitTransaction(ws, tx.pursePub); @@ -1613,10 +1617,10 @@ export async function resumeTransaction( await resumePeerPushDebitTransaction(ws, tx.pursePub); break; case TransactionType.PeerPullDebit: - await resumePeerPullDebitTransaction(ws, tx.peerPullPaymentIncomingId); + await resumePeerPullDebitTransaction(ws, tx.peerPullDebitId); break; case TransactionType.PeerPushCredit: - await resumePeerPushCreditTransaction(ws, tx.peerPushPaymentIncomingId); + await resumePeerPushCreditTransaction(ws, tx.peerPushCreditId); break; case TransactionType.Refund: throw Error("refund transactions can't be suspended or resumed"); @@ -1641,17 +1645,11 @@ export async function deleteTransaction( switch (parsedTx.tag) { case TransactionType.PeerPushCredit: { - const peerPushPaymentIncomingId = parsedTx.peerPushPaymentIncomingId; + const peerPushCreditId = parsedTx.peerPushCreditId; await ws.db - .mktx((x) => [ - x.withdrawalGroups, - x.peerPushPaymentIncoming, - x.tombstones, - ]) + .mktx((x) => [x.withdrawalGroups, x.peerPushCredit, x.tombstones]) .runReadWrite(async (tx) => { - const pushInc = await tx.peerPushPaymentIncoming.get( - peerPushPaymentIncomingId, - ); + const pushInc = await tx.peerPushCredit.get(peerPushCreditId); if (!pushInc) { return; } @@ -1668,12 +1666,9 @@ export async function deleteTransaction( }); } } - await tx.peerPushPaymentIncoming.delete(peerPushPaymentIncomingId); + await tx.peerPushCredit.delete(peerPushCreditId); await tx.tombstones.put({ - id: - TombstoneTag.DeletePeerPushCredit + - ":" + - peerPushPaymentIncomingId, + id: TombstoneTag.DeletePeerPushCredit + ":" + peerPushCreditId, }); }); return; @@ -1682,13 +1677,9 @@ export async function deleteTransaction( case TransactionType.PeerPullCredit: { const pursePub = parsedTx.pursePub; await ws.db - .mktx((x) => [ - x.withdrawalGroups, - x.peerPullPaymentInitiations, - x.tombstones, - ]) + .mktx((x) => [x.withdrawalGroups, x.peerPullCredit, x.tombstones]) .runReadWrite(async (tx) => { - const pullIni = await tx.peerPullPaymentInitiations.get(pursePub); + const pullIni = await tx.peerPullCredit.get(pursePub); if (!pullIni) { return; } @@ -1705,7 +1696,7 @@ export async function deleteTransaction( }); } } - await tx.peerPullPaymentInitiations.delete(pursePub); + await tx.peerPullCredit.delete(pursePub); await tx.tombstones.put({ id: TombstoneTag.DeletePeerPullCredit + ":" + pursePub, }); @@ -1795,7 +1786,7 @@ export async function deleteTransaction( case TransactionType.Refund: { const refundGroupId = parsedTx.refundGroupId; await ws.db - .mktx((x) => [x.refundGroups, x.tombstones, x.refundItems]) + .mktx((x) => [x.refundGroups, x.tombstones]) .runReadWrite(async (tx) => { const refundRecord = await tx.refundGroups.get(refundGroupId); if (!refundRecord) { @@ -1809,15 +1800,13 @@ export async function deleteTransaction( } case TransactionType.PeerPullDebit: { - const peerPullPaymentIncomingId = parsedTx.peerPullPaymentIncomingId; + const peerPullDebitId = parsedTx.peerPullDebitId; await ws.db - .mktx((x) => [x.peerPullPaymentIncoming, x.tombstones]) + .mktx((x) => [x.peerPullDebit, x.tombstones]) .runReadWrite(async (tx) => { - const debit = await tx.peerPullPaymentIncoming.get( - peerPullPaymentIncomingId, - ); + const debit = await tx.peerPullDebit.get(peerPullDebitId); if (debit) { - await tx.peerPullPaymentIncoming.delete(peerPullPaymentIncomingId); + await tx.peerPullDebit.delete(peerPullDebitId); await tx.tombstones.put({ id: transactionId }); } }); @@ -1828,11 +1817,11 @@ export async function deleteTransaction( case TransactionType.PeerPushDebit: { const pursePub = parsedTx.pursePub; await ws.db - .mktx((x) => [x.peerPushPaymentInitiations, x.tombstones]) + .mktx((x) => [x.peerPushDebit, x.tombstones]) .runReadWrite(async (tx) => { - const debit = await tx.peerPushPaymentInitiations.get(pursePub); + const debit = await tx.peerPushDebit.get(pursePub); if (debit) { - await tx.peerPushPaymentInitiations.delete(pursePub); + await tx.peerPushDebit.delete(pursePub); await tx.tombstones.put({ id: transactionId }); } }); @@ -1875,10 +1864,10 @@ export async function abortTransaction( await abortPeerPullCreditTransaction(ws, txId.pursePub); break; case TransactionType.PeerPullDebit: - await abortPeerPullDebitTransaction(ws, txId.peerPullPaymentIncomingId); + await abortPeerPullDebitTransaction(ws, txId.peerPullDebitId); break; case TransactionType.PeerPushCredit: - await abortPeerPushCreditTransaction(ws, txId.peerPushPaymentIncomingId); + await abortPeerPushCreditTransaction(ws, txId.peerPushCreditId); break; case TransactionType.PeerPushDebit: await abortPeerPushDebitTransaction(ws, txId.pursePub); diff --git a/packages/taler-wallet-core/src/operations/withdraw.test.ts b/packages/taler-wallet-core/src/operations/withdraw.test.ts index 5a557b5de..2d9286610 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.test.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.test.ts @@ -78,8 +78,7 @@ test("withdrawal selection bug repro", (t) => { }, verificationStatus: DenominationVerificationStatus.Unverified, currency: "KUDOS", - amountFrac: 0, - amountVal: 1000, + value: "KUDOS:1000", listIssueDate: { t_s: 0 }, }, { @@ -133,8 +132,7 @@ test("withdrawal selection bug repro", (t) => { t_s: 1585229388, }, verificationStatus: DenominationVerificationStatus.Unverified, - amountFrac: 0, - amountVal: 10, + value: "KUDOS:10", currency: "KUDOS", listIssueDate: { t_s: 0 }, }, @@ -188,8 +186,7 @@ test("withdrawal selection bug repro", (t) => { t_s: 1585229388, }, verificationStatus: DenominationVerificationStatus.Unverified, - amountFrac: 0, - amountVal: 5, + value: "KUDOS:5", currency: "KUDOS", listIssueDate: { t_s: 0 }, }, @@ -244,8 +241,7 @@ test("withdrawal selection bug repro", (t) => { t_s: 1585229388, }, verificationStatus: DenominationVerificationStatus.Unverified, - amountFrac: 0, - amountVal: 1, + value: "KUDOS:1", currency: "KUDOS", listIssueDate: { t_s: 0 }, }, @@ -299,8 +295,11 @@ test("withdrawal selection bug repro", (t) => { t_s: 1585229388, }, verificationStatus: DenominationVerificationStatus.Unverified, - amountFrac: 10000000, - amountVal: 0, + value: Amounts.stringify({ + currency: "KUDOS", + fraction: 10000000, + value: 0, + }), currency: "KUDOS", listIssueDate: { t_s: 0 }, }, @@ -354,8 +353,7 @@ test("withdrawal selection bug repro", (t) => { t_s: 1585229388, }, verificationStatus: DenominationVerificationStatus.Unverified, - amountFrac: 0, - amountVal: 2, + value: "KUDOS:2", currency: "KUDOS", listIssueDate: { t_s: 0 }, }, diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index a3d95fb5c..bae348dc1 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -123,7 +123,6 @@ import { import { getExchangeDetails, getExchangePaytoUri, - getExchangeTrust, updateExchangeFromUrl, } from "./exchanges.js"; import { @@ -320,7 +319,7 @@ export async function abortWithdrawalTransaction( case WithdrawalGroupStatus.AbortingBank: // No transition needed, but not an error break; - case WithdrawalGroupStatus.Finished: + case WithdrawalGroupStatus.Done: case WithdrawalGroupStatus.FailedBankAborted: case WithdrawalGroupStatus.AbortedExchange: case WithdrawalGroupStatus.AbortedBank: @@ -400,7 +399,7 @@ export function computeWithdrawalTransactionStatus( return { major: TransactionMajorState.Aborted, }; - case WithdrawalGroupStatus.Finished: + case WithdrawalGroupStatus.Done: return { major: TransactionMajorState.Done, }; @@ -504,7 +503,7 @@ export function computeWithdrawalTransactionActions( switch (wgRecord.status) { case WithdrawalGroupStatus.FailedBankAborted: return [TransactionAction.Delete]; - case WithdrawalGroupStatus.Finished: + case WithdrawalGroupStatus.Done: return [TransactionAction.Delete]; case WithdrawalGroupStatus.PendingRegisteringBank: return [TransactionAction.Suspend, TransactionAction.Abort]; @@ -726,7 +725,9 @@ interface WithdrawalBatchResult { batchResp: ExchangeWithdrawBatchResponse; } enum AmlStatus { - normal= 0, pending = 1, fronzen = 2, + normal = 0, + pending = 1, + fronzen = 2, } async function handleKycRequired( @@ -773,7 +774,9 @@ async function handleKycRequired( const kycStatus = await kycStatusRes.json(); logger.info(`kyc status: ${j2s(kycStatus)}`); kycUrl = kycStatus.kyc_url; - } else if (kycStatusRes.status === HttpStatusCode.UnavailableForLegalReasons) { + } else if ( + kycStatusRes.status === HttpStatusCode.UnavailableForLegalReasons + ) { const kycStatus = await kycStatusRes.json(); logger.info(`aml status: ${j2s(kycStatus)}`); amlStatus = kycStatus.aml_status; @@ -809,11 +812,15 @@ async function handleKycRequired( requirementRow: uuidResp.requirement_row, }; wg2.kycUrl = kycUrl; - wg2.status = amlStatus === AmlStatus.normal || amlStatus === undefined ? WithdrawalGroupStatus.PendingKyc : - amlStatus === AmlStatus.pending ? WithdrawalGroupStatus.PendingAml : - amlStatus === AmlStatus.fronzen ? WithdrawalGroupStatus.SuspendedAml : - assertUnreachable(amlStatus); - + wg2.status = + amlStatus === AmlStatus.normal || amlStatus === undefined + ? WithdrawalGroupStatus.PendingKyc + : amlStatus === AmlStatus.pending + ? WithdrawalGroupStatus.PendingAml + : amlStatus === AmlStatus.fronzen + ? WithdrawalGroupStatus.SuspendedAml + : assertUnreachable(amlStatus); + await tx.withdrawalGroups.put(wg2); const newTxState = computeWithdrawalTransactionStatus(wg2); return { @@ -924,31 +931,31 @@ async function processPlanchetExchangeBatchRequest( // FIXME: handle individual error codes better! - const reqUrl = new URL( - `reserves/${withdrawalGroup.reservePub}/batch-withdraw`, - withdrawalGroup.exchangeBaseUrl, - ).href; + const reqUrl = new URL( + `reserves/${withdrawalGroup.reservePub}/batch-withdraw`, + withdrawalGroup.exchangeBaseUrl, + ).href; - try { - const resp = await ws.http.postJson(reqUrl, batchReq); - if (resp.status === HttpStatusCode.UnavailableForLegalReasons) { - await handleKycRequired(ws, withdrawalGroup, resp, 0, requestCoinIdxs); - } - const r = await readSuccessResponseJsonOrThrow( - resp, - codecForWithdrawBatchResponse(), - ); - return { - coinIdxs: requestCoinIdxs, - batchResp: r, - }; - } catch (e) { - await storeCoinError(e, requestCoinIdxs[0]); - return { - batchResp: { ev_sigs: [] }, - coinIdxs: [], - }; + try { + const resp = await ws.http.postJson(reqUrl, batchReq); + if (resp.status === HttpStatusCode.UnavailableForLegalReasons) { + await handleKycRequired(ws, withdrawalGroup, resp, 0, requestCoinIdxs); } + const r = await readSuccessResponseJsonOrThrow( + resp, + codecForWithdrawBatchResponse(), + ); + return { + coinIdxs: requestCoinIdxs, + batchResp: r, + }; + } catch (e) { + await storeCoinError(e, requestCoinIdxs[0]); + return { + batchResp: { ev_sigs: [] }, + coinIdxs: [], + }; + } } async function processPlanchetVerifyAndStoreCoin( @@ -1415,10 +1422,12 @@ async function processWithdrawalGroupPendingKyc( logger.info(`kyc status: ${j2s(kycStatus)}`); // FIXME: do we need to update the KYC url, or does it always stay constant? return { ready: false }; - } else if (kycStatusRes.status === HttpStatusCode.UnavailableForLegalReasons) { + } else if ( + kycStatusRes.status === HttpStatusCode.UnavailableForLegalReasons + ) { const kycStatus = await kycStatusRes.json(); logger.info(`aml status: ${j2s(kycStatus)}`); - return {ready : false} + return { ready: false }; } else { throw Error( `unexpected response from kyc-check (${kycStatusRes.status})`, @@ -1453,7 +1462,7 @@ async function processWithdrawalGroupPendingReady( return undefined; } const txStatusOld = computeWithdrawalTransactionStatus(wg); - wg.status = WithdrawalGroupStatus.Finished; + wg.status = WithdrawalGroupStatus.Done; wg.timestampFinish = TalerPreciseTimestamp.now(); const txStatusNew = computeWithdrawalTransactionStatus(wg); await tx.withdrawalGroups.put(wg); @@ -1551,7 +1560,7 @@ async function processWithdrawalGroupPendingReady( logger.info(`now withdrawn ${numFinished} of ${numTotalCoins} coins`); if (wg.timestampFinish === undefined && numFinished === numTotalCoins) { wg.timestampFinish = TalerPreciseTimestamp.now(); - wg.status = WithdrawalGroupStatus.Finished; + wg.status = WithdrawalGroupStatus.Done; await makeCoinsVisible(ws, tx, transactionId); } @@ -1643,7 +1652,7 @@ export async function processWithdrawalGroup( } break; } - case WithdrawalGroupStatus.Finished: + case WithdrawalGroupStatus.Done: case WithdrawalGroupStatus.FailedBankAborted: { // FIXME return TaskRunResult.pending(); @@ -1714,11 +1723,6 @@ export async function getExchangeWithdrawalInfo( exchangeWireAccounts.push(account.payto_uri); } - const { isTrusted, isAudited } = await ws.exchangeOps.getExchangeTrust( - ws, - exchange, - ); - let hasDenomWithAgeRestriction = false; logger.trace("computing earliest deposit expiration"); @@ -1797,8 +1801,6 @@ export async function getExchangeWithdrawalInfo( exchangePaytoUris: paytoUris, exchangeWireAccounts, exchangeVersion: exchangeDetails.protocolVersionRange || "unknown", - isAudited, - isTrusted, numOfferedDenoms: possibleDenoms.length, selectedDenoms, // FIXME: delete this field / replace by something we can display to the user @@ -1859,7 +1861,6 @@ export async function getWithdrawalDetailsForUri( .mktx((x) => [ x.exchanges, x.exchangeDetails, - x.exchangeTos, x.denominations, x.operationRetries, ]) @@ -2215,8 +2216,6 @@ export interface PrepareCreateWithdrawalGroupResult { withdrawalGroup: WithdrawalGroupRecord; transactionId: string; creationInfo?: { - isTrusted: boolean; - isAudited: boolean; amount: AmountJson; canonExchange: string; exchangeDetails: ExchangeDetailsRecord; @@ -2311,10 +2310,6 @@ export async function internalPrepareCreateWithdrawalGroup( logger.trace(exchangeDetails); throw Error("exchange not updated"); } - const { isAudited, isTrusted } = await getExchangeTrust( - ws, - exchangeInfo.exchange, - ); const transactionId = constructTransactionIdentifier({ tag: TransactionType.Withdrawal, withdrawalGroupId: withdrawalGroup.withdrawalGroupId, @@ -2324,8 +2319,6 @@ export async function internalPrepareCreateWithdrawalGroup( withdrawalGroup, transactionId, creationInfo: { - isAudited, - isTrusted, canonExchange, amount, exchangeDetails, @@ -2344,7 +2337,6 @@ export async function internalPerformCreateWithdrawalGroup( withdrawalGroups: typeof WalletStoresV1.withdrawalGroups; reserves: typeof WalletStoresV1.reserves; exchanges: typeof WalletStoresV1.exchanges; - exchangeTrust: typeof WalletStoresV1.exchangeTrust; }>, prep: PrepareCreateWithdrawalGroupResult, ): Promise<PerformCreateWithdrawalGroupResult> { @@ -2352,7 +2344,7 @@ export async function internalPerformCreateWithdrawalGroup( if (!prep.creationInfo) { return { withdrawalGroup, transitionInfo: undefined }; } - const { isAudited, isTrusted, amount, canonExchange, exchangeDetails } = + const { amount, canonExchange, exchangeDetails } = prep.creationInfo; await tx.withdrawalGroups.add(withdrawalGroup); @@ -2368,15 +2360,6 @@ export async function internalPerformCreateWithdrawalGroup( await tx.exchanges.put(exchange); } - if (!isAudited && !isTrusted) { - await tx.exchangeTrust.put({ - currency: amount.currency, - exchangeBaseUrl: canonExchange, - exchangeMasterPub: exchangeDetails.masterPublicKey, - uids: [encodeCrock(getRandomBytes(32))], - }); - } - const oldTxState = { major: TransactionMajorState.None, minor: undefined, @@ -2422,7 +2405,6 @@ export async function internalCreateWithdrawalGroup( x.reserves, x.exchanges, x.exchangeDetails, - x.exchangeTrust, ]) .runReadWrite(async (tx) => { return await internalPerformCreateWithdrawalGroup(ws, tx, prep); @@ -2568,7 +2550,6 @@ export async function createManualWithdrawal( x.withdrawalGroups, x.exchanges, x.exchangeDetails, - x.exchangeTrust, ]) .runReadOnly(async (tx) => { return await getFundingPaytoUris(tx, withdrawalGroup.withdrawalGroupId); diff --git a/packages/taler-wallet-core/src/pending-types.ts b/packages/taler-wallet-core/src/pending-types.ts index 82eb542a7..627888b4d 100644 --- a/packages/taler-wallet-core/src/pending-types.ts +++ b/packages/taler-wallet-core/src/pending-types.ts @@ -99,14 +99,14 @@ export interface PendingPeerPullInitiationTask { */ export interface PendingPeerPullDebitTask { type: PendingTaskType.PeerPullDebit; - peerPullPaymentIncomingId: string; + peerPullDebitId: string; } /** */ export interface PendingPeerPushCreditTask { type: PendingTaskType.PeerPushCredit; - peerPushPaymentIncomingId: string; + peerPushCreditId: string; } /** diff --git a/packages/taler-wallet-core/src/remote.ts b/packages/taler-wallet-core/src/remote.ts index 89348698e..164f7cfe9 100644 --- a/packages/taler-wallet-core/src/remote.ts +++ b/packages/taler-wallet-core/src/remote.ts @@ -65,6 +65,7 @@ export async function createRemoteWallet( const ctx: RemoteWallet = { makeCoreApiRequest(operation, payload) { const id = `req-${nextRequestId}`; + nextRequestId += 1; const req: CoreApiRequestEnvelope = { operation, id, diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts b/packages/taler-wallet-core/src/util/coinSelection.ts index daba2ead5..ef2f85789 100644 --- a/packages/taler-wallet-core/src/util/coinSelection.ts +++ b/packages/taler-wallet-core/src/util/coinSelection.ts @@ -588,22 +588,22 @@ async function selectPayMerchantCandidates( for (const acc of exchangeDetails.wireInfo.accounts) { const pp = parsePaytoUri(acc.payto_uri); checkLogicInvariant(!!pp); - if (pp.targetType === req.wireMethod) { - // also check that wire method is supported now - const wireFeeStr = exchangeDetails.wireInfo.feesForType[ - req.wireMethod - ]?.find((x) => { - return AbsoluteTime.isBetween( - AbsoluteTime.now(), - AbsoluteTime.fromProtocolTimestamp(x.startStamp), - AbsoluteTime.fromProtocolTimestamp(x.endStamp), - ); - })?.wireFee; - if (wireFeeStr) { - wireMethodFee = wireFeeStr; - } - break; + if (pp.targetType !== req.wireMethod) { + continue; + } + const wireFeeStr = exchangeDetails.wireInfo.feesForType[ + req.wireMethod + ]?.find((x) => { + return AbsoluteTime.isBetween( + AbsoluteTime.now(), + AbsoluteTime.fromProtocolTimestamp(x.startStamp), + AbsoluteTime.fromProtocolTimestamp(x.endStamp), + ); + })?.wireFee; + if (wireFeeStr) { + wireMethodFee = wireFeeStr; } + break; } if (!wireMethodFee) { break; @@ -699,25 +699,17 @@ export function selectWithdrawalDenominations( let totalWithdrawCost = Amounts.zeroOfCurrency(amountAvailable.currency); denoms = denoms.filter((d) => isWithdrawableDenom(d, denomselAllowLate)); - denoms.sort((d1, d2) => - Amounts.cmp( - DenominationRecord.getValue(d2), - DenominationRecord.getValue(d1), - ), - ); + denoms.sort((d1, d2) => Amounts.cmp(d2.value, d1.value)); for (const d of denoms) { - const cost = Amounts.add( - DenominationRecord.getValue(d), - d.fees.feeWithdraw, - ).amount; + const cost = Amounts.add(d.value, d.fees.feeWithdraw).amount; const res = Amounts.divmod(remaining, cost); const count = res.quotient; remaining = Amounts.sub(remaining, Amounts.mult(cost, count).amount).amount; if (count > 0) { totalCoinValue = Amounts.add( totalCoinValue, - Amounts.mult(DenominationRecord.getValue(d), count).amount, + Amounts.mult(d.value, count).amount, ).amount; totalWithdrawCost = Amounts.add( totalWithdrawCost, @@ -766,30 +758,22 @@ export function selectForcedWithdrawalDenominations( let totalWithdrawCost = Amounts.zeroOfCurrency(amountAvailable.currency); denoms = denoms.filter((d) => isWithdrawableDenom(d, denomselAllowLate)); - denoms.sort((d1, d2) => - Amounts.cmp( - DenominationRecord.getValue(d2), - DenominationRecord.getValue(d1), - ), - ); + denoms.sort((d1, d2) => Amounts.cmp(d2.value, d1.value)); for (const fds of forcedDenomSel.denoms) { const count = fds.count; const denom = denoms.find((x) => { - return Amounts.cmp(DenominationRecord.getValue(x), fds.value) == 0; + return Amounts.cmp(x.value, fds.value) == 0; }); if (!denom) { throw Error( `unable to find denom for forced selection (value ${fds.value})`, ); } - const cost = Amounts.add( - DenominationRecord.getValue(denom), - denom.fees.feeWithdraw, - ).amount; + const cost = Amounts.add(denom.value, denom.fees.feeWithdraw).amount; totalCoinValue = Amounts.add( totalCoinValue, - Amounts.mult(DenominationRecord.getValue(denom), count).amount, + Amounts.mult(denom.value, count).amount, ).amount; totalWithdrawCost = Amounts.add( totalWithdrawCost, @@ -1002,7 +986,7 @@ export async function selectPeerCoins( x.coinAvailability, x.denominations, x.refreshGroups, - x.peerPushPaymentInitiations, + x.peerPushDebit, ]) .runReadWrite(async (tx) => { const exchanges = await tx.exchanges.iter().toArray(); diff --git a/packages/taler-wallet-core/src/util/instructedAmountConversion.ts b/packages/taler-wallet-core/src/util/instructedAmountConversion.ts index bd02e7b22..54c08eee4 100644 --- a/packages/taler-wallet-core/src/util/instructedAmountConversion.ts +++ b/packages/taler-wallet-core/src/util/instructedAmountConversion.ts @@ -321,7 +321,7 @@ function buildCoinInfoFromDenom( AbsoluteTime.fromProtocolTimestamp(denom.stampExpireDeposit), ), totalAvailable: total, - value: DenominationRecord.getValue(denom), + value: Amounts.parseOrThrow(denom.value), maxAge, }; } diff --git a/packages/taler-wallet-core/src/util/query.ts b/packages/taler-wallet-core/src/util/query.ts index 1c3ff6a2a..309c17a43 100644 --- a/packages/taler-wallet-core/src/util/query.ts +++ b/packages/taler-wallet-core/src/util/query.ts @@ -35,7 +35,7 @@ import { IDBKeyPath, IDBKeyRange, } from "@gnu-taler/idb-bridge"; -import { Logger, j2s } from "@gnu-taler/taler-util"; +import { Codec, Logger, j2s } from "@gnu-taler/taler-util"; const logger = new Logger("query.ts"); @@ -387,11 +387,11 @@ export interface StoreReadWriteAccessor<RecordType, IndexMap> { export interface StoreWithIndexes< StoreName extends string, - SD extends StoreDescriptor<unknown>, + RecordType, IndexMap, > { storeName: StoreName; - store: SD; + store: StoreDescriptor<RecordType>; indexMap: IndexMap; /** @@ -401,19 +401,13 @@ export interface StoreWithIndexes< mark: Symbol; } -export type GetRecordType<T> = T extends StoreDescriptor<infer X> ? X : unknown; - const storeWithIndexesSymbol = Symbol("StoreWithIndexesMark"); -export function describeStore< - StoreName extends string, - SD extends StoreDescriptor<unknown>, - IndexMap, ->( +export function describeStore<StoreName extends string, RecordType, IndexMap>( name: StoreName, - s: SD, + s: StoreDescriptor<RecordType>, m: IndexMap, -): StoreWithIndexes<StoreName, SD, IndexMap> { +): StoreWithIndexes<StoreName, RecordType, IndexMap> { return { storeName: name, store: s, @@ -422,13 +416,72 @@ export function describeStore< }; } +export function describeStoreV2< + StoreName extends string, + RecordType, + IndexMap extends { [x: string]: IndexDescriptor } = {}, +>(args: { + storeName: StoreName; + recordCodec: Codec<RecordType>; + keyPath?: IDBKeyPath | IDBKeyPath[]; + autoIncrement?: boolean; + /** + * Database version that this store was added in, or + * undefined if added in the first version. + */ + versionAdded?: number; + indexes?: IndexMap; +}): StoreWithIndexes<StoreName, RecordType, IndexMap> { + return { + storeName: args.storeName, + store: { + _dummy: undefined as any, + autoIncrement: args.autoIncrement, + keyPath: args.keyPath, + versionAdded: args.versionAdded, + }, + indexMap: args.indexes ?? ({} as IndexMap), + mark: storeWithIndexesSymbol, + }; +} + +type KeyPathComponents = string | number; + +/** + * Follow a key path (dot-separated) in an object. + */ +type DerefKeyPath<T, P> = P extends `${infer PX extends keyof T & + KeyPathComponents}` + ? T[PX] + : P extends `${infer P0 extends keyof T & KeyPathComponents}.${infer Rest}` + ? DerefKeyPath<T[P0], Rest> + : unknown; + +/** + * Return a path if it is a valid dot-separate path to an object. + * Otherwise, return "never". + */ +type ValidateKeyPath<T, P> = P extends `${infer PX extends keyof T & + KeyPathComponents}` + ? PX + : P extends `${infer P0 extends keyof T & KeyPathComponents}.${infer Rest}` + ? `${P0}.${ValidateKeyPath<T[P0], Rest>}` + : never; + +// function foo<T, P>( +// x: T, +// p: P extends ValidateKeyPath<T, P> ? P : never, +// ): void {} + +// foo({x: [0,1,2]}, "x.0"); + export type GetReadOnlyAccess<BoundStores> = { [P in keyof BoundStores]: BoundStores[P] extends StoreWithIndexes< - infer SN, - infer SD, - infer IM + infer StoreName, + infer RecordType, + infer IndexMap > - ? StoreReadOnlyAccessor<GetRecordType<SD>, IM> + ? StoreReadOnlyAccessor<RecordType, IndexMap> : unknown; }; @@ -446,11 +499,11 @@ export type DbReadOnlyTransaction< } ? { [P in Stores]: StoreMap[P] extends StoreWithIndexes< - infer SN, - infer SD, - infer IM + infer StoreName, + infer RecordType, + infer IndexMap > - ? StoreReadOnlyAccessor<GetRecordType<SD>, IM> + ? StoreReadOnlyAccessor<RecordType, IndexMap> : unknown; } : unknown; @@ -463,22 +516,22 @@ export type DbReadWriteTransaction< } ? { [P in Stores]: StoreMap[P] extends StoreWithIndexes< - infer SN, - infer SD, - infer IM + infer StoreName, + infer RecordType, + infer IndexMap > - ? StoreReadWriteAccessor<GetRecordType<SD>, IM> + ? StoreReadWriteAccessor<RecordType, IndexMap> : unknown; } : unknown; export type GetReadWriteAccess<BoundStores> = { [P in keyof BoundStores]: BoundStores[P] extends StoreWithIndexes< - infer SN, - infer SD, - infer IM + infer StoreName, + infer RecordType, + infer IndexMap > - ? StoreReadWriteAccessor<GetRecordType<SD>, IM> + ? StoreReadWriteAccessor<RecordType, IndexMap> : unknown; }; diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts index 4d9d40c43..d89ad257a 100644 --- a/packages/taler-wallet-core/src/wallet-api-types.ts +++ b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -119,8 +119,9 @@ import { CreateStoredBackupResponse, RecoverStoredBackupRequest, DeleteStoredBackupRequest, + TestingSetTimetravelRequest, } from "@gnu-taler/taler-util"; -import { AuditorTrustRecord, WalletContractData } from "./db.js"; +import { WalletContractData } from "./db.js"; import { AddBackupProviderRequest, AddBackupProviderResponse, @@ -215,6 +216,7 @@ export enum WalletApiOperation { ValidateIban = "validateIban", TestingWaitTransactionsFinal = "testingWaitTransactionsFinal", TestingWaitRefreshesFinal = "testingWaitRefreshesFinal", + TestingSetTimetravel = "testingSetTimetravel", GetScopedCurrencyInfo = "getScopedCurrencyInfo", ListStoredBackups = "listStoredBackups", CreateStoredBackup = "createStoredBackup", @@ -258,7 +260,6 @@ export interface WalletConfig { */ builtin: { exchanges: string[]; - auditors: AuditorTrustRecord[]; }; /** @@ -990,6 +991,15 @@ export type DumpCoinsOp = { }; /** + * Add an offset to the wallet's internal time. + */ +export type TestingSetTimetravelOp = { + op: WalletApiOperation.TestingSetTimetravel; + request: TestingSetTimetravelRequest; + response: EmptyObject; +}; + +/** * Wait until all transactions are in a final state. */ export type TestingWaitTransactionsFinal = { @@ -1111,6 +1121,7 @@ export type WalletOperations = { [WalletApiOperation.ValidateIban]: ValidateIbanOp; [WalletApiOperation.TestingWaitTransactionsFinal]: TestingWaitTransactionsFinal; [WalletApiOperation.TestingWaitRefreshesFinal]: TestingWaitRefreshesFinal; + [WalletApiOperation.TestingSetTimetravel]: TestingSetTimetravelOp; [WalletApiOperation.GetScopedCurrencyInfo]: GetScopedCurrencyInfoOp; [WalletApiOperation.CreateStoredBackup]: CreateStoredBackupsOp; [WalletApiOperation.ListStoredBackups]: ListStoredBackupsOp; diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 626409dd6..1a60b148c 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -34,7 +34,6 @@ import { Duration, ExchangeDetailedResponse, ExchangeListItem, - ExchangeTosStatusDetails, ExchangesListResponse, FeeDescription, GetExchangeTosResult, @@ -121,11 +120,16 @@ import { GetCurrencyInfoResponse, codecForGetCurrencyInfoRequest, CreateStoredBackupResponse, + StoredBackupList, + codecForDeleteStoredBackupRequest, + DeleteStoredBackupRequest, + RecoverStoredBackupRequest, + codecForRecoverStoredBackupRequest, + codecForTestingSetTimetravelRequest, + setDangerousTimetravel, } from "@gnu-taler/taler-util"; -import { - HttpRequestLibrary, - readSuccessResponseJsonOrThrow, -} from "@gnu-taler/taler-util/http"; +import type { HttpRequestLibrary } from "@gnu-taler/taler-util/http"; +import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js"; import { CryptoDispatcher, @@ -194,11 +198,9 @@ import { downloadTosFromAcceptedFormat, getExchangeDetails, getExchangeRequestTimeout, - getExchangeTrust, provideExchangeRecordInTx, updateExchangeFromUrl, updateExchangeFromUrlHandler, - updateExchangeTermsOfService, } from "./operations/exchanges.js"; import { getMerchantInfo } from "./operations/merchants.js"; import { @@ -352,9 +354,9 @@ async function callOperationHandler( case PendingTaskType.PeerPullCredit: return await processPeerPullCredit(ws, pending.pursePub); case PendingTaskType.PeerPullDebit: - return await processPeerPullDebit(ws, pending.peerPullPaymentIncomingId); + return await processPeerPullDebit(ws, pending.peerPullDebitId); case PendingTaskType.PeerPushCredit: - return await processPeerPushCredit(ws, pending.peerPushPaymentIncomingId); + return await processPeerPushCredit(ws, pending.peerPushCreditId); default: return assertUnreachable(pending); } @@ -522,7 +524,7 @@ async function runTaskLoop( */ async function fillDefaults(ws: InternalWalletState): Promise<void> { await ws.db - .mktx((x) => [x.config, x.auditorTrust, x.exchanges, x.exchangeDetails]) + .mktx((x) => [x.config, x.exchanges, x.exchangeDetails]) .runReadWrite(async (tx) => { const appliedRec = await tx.config.get("currencyDefaultsApplied"); let alreadyApplied = appliedRec ? !!appliedRec.value : false; @@ -530,10 +532,6 @@ async function fillDefaults(ws: InternalWalletState): Promise<void> { logger.trace("defaults already applied"); return; } - logger.info("importing default exchanges and auditors"); - for (const c of ws.config.builtin.auditors) { - await tx.auditorTrust.put(c); - } for (const baseUrl of ws.config.builtin.exchanges) { await addPresetExchangeEntry(tx, baseUrl); const now = AbsoluteTime.now(); @@ -557,33 +555,6 @@ async function getExchangeTos( ): Promise<GetExchangeTosResult> { // FIXME: download ToS in acceptable format if passed! const { exchangeDetails } = await updateExchangeFromUrl(ws, exchangeBaseUrl); - const tosDetails = await ws.db - .mktx((x) => [x.exchangeTos]) - .runReadOnly(async (tx) => { - return await getExchangeTosStatusDetails(tx, exchangeDetails); - }); - const content = tosDetails.content; - const currentEtag = tosDetails.currentVersion; - const contentType = tosDetails.contentType; - if ( - content === undefined || - currentEtag === undefined || - contentType === undefined - ) { - throw Error("exchange is in invalid state"); - } - if ( - acceptedFormat && - acceptedFormat.findIndex((f) => f === contentType) !== -1 - ) { - return { - acceptedEtag: exchangeDetails.tosAccepted?.etag, - currentEtag, - content, - contentType, - tosStatus: getExchangeTosStatus(exchangeDetails), - }; - } const tosDownload = await downloadTosFromAcceptedFormat( ws, @@ -592,17 +563,15 @@ async function getExchangeTos( acceptedFormat, ); - if (tosDownload.tosContentType === contentType) { - return { - acceptedEtag: exchangeDetails.tosAccepted?.etag, - currentEtag, - content, - contentType, - tosStatus: getExchangeTosStatus(exchangeDetails), - }; - } - - await updateExchangeTermsOfService(ws, exchangeBaseUrl, tosDownload); + await ws.db + .mktx((x) => [x.exchanges, x.exchangeDetails]) + .runReadWrite(async (tx) => { + const d = await getExchangeDetails(tx, exchangeBaseUrl); + if (d) { + d.tosCurrentEtag = tosDownload.tosEtag; + await tx.exchangeDetails.put(d); + } + }); return { acceptedEtag: exchangeDetails.tosAccepted?.etag, @@ -683,32 +652,6 @@ async function forgetKnownBankAccounts( return; } -async function getExchangeTosStatusDetails( - tx: GetReadOnlyAccess<{ exchangeTos: typeof WalletStoresV1.exchangeTos }>, - exchangeDetails: ExchangeDetailsRecord, -): Promise<ExchangeTosStatusDetails> { - let exchangeTos = await tx.exchangeTos.get([ - exchangeDetails.exchangeBaseUrl, - exchangeDetails.tosCurrentEtag, - ]); - - if (!exchangeTos) { - exchangeTos = { - etag: "not-available", - termsOfServiceContentType: "text/plain", - termsOfServiceText: "terms of service unavailable", - exchangeBaseUrl: exchangeDetails.exchangeBaseUrl, - }; - } - - return { - acceptedVersion: exchangeDetails.tosAccepted?.etag, - content: exchangeTos.termsOfServiceText, - contentType: exchangeTos.termsOfServiceContentType, - currentVersion: exchangeTos.etag, - }; -} - async function getExchanges( ws: InternalWalletState, ): Promise<ExchangesListResponse> { @@ -717,7 +660,6 @@ async function getExchanges( .mktx((x) => [ x.exchanges, x.exchangeDetails, - x.exchangeTos, x.denominations, x.operationRetries, ]) @@ -742,12 +684,7 @@ async function getExchangeDetailedInfo( ): Promise<ExchangeDetailedResponse> { //TODO: should we use the forceUpdate parameter? const exchange = await ws.db - .mktx((x) => [ - x.exchanges, - x.exchangeTos, - x.exchangeDetails, - x.denominations, - ]) + .mktx((x) => [x.exchanges, x.exchangeDetails, x.denominations]) .runReadOnly(async (tx) => { const ex = await tx.exchanges.get(exchangeBaseurl); const dp = ex?.detailsPointer; @@ -769,8 +706,6 @@ async function getExchangeDetailedInfo( return; } - const tos = await getExchangeTosStatusDetails(tx, exchangeDetails); - const denominations: DenominationInfo[] = denominationRecords.map((x) => DenominationRecord.toDenomInfo(x), ); @@ -779,7 +714,6 @@ async function getExchangeDetailedInfo( info: { exchangeBaseUrl: ex.baseUrl, currency, - tos, paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri), auditors: exchangeDetails.auditors, wireInfo: exchangeDetails.wireInfo, @@ -981,11 +915,7 @@ async function dumpCoins(ws: InternalWalletState): Promise<CoinDumpJson> { coin_pub: c.coinPub, denom_pub: denomInfo.denomPub, denom_pub_hash: c.denomPubHash, - denom_value: Amounts.stringify({ - value: denom.amountVal, - currency: denom.currency, - fraction: denom.amountFrac, - }), + denom_value: denom.value, exchange_base_url: c.exchangeBaseUrl, refresh_parent_coin_pub: refreshParentCoinPub, withdrawal_reserve_pub: withdrawalReservePub, @@ -1041,6 +971,57 @@ async function createStoredBackup( }; } +async function listStoredBackups( + ws: InternalWalletState, +): Promise<StoredBackupList> { + const storedBackups: StoredBackupList = { + storedBackups: [], + }; + const backupsDb = await openStoredBackupsDatabase(ws.idb); + await backupsDb.mktxAll().runReadWrite(async (tx) => { + await tx.backupMeta.iter().forEach((x) => { + storedBackups.storedBackups.push({ + name: x.name, + }); + }); + }); + return storedBackups; +} + +async function deleteStoredBackup( + ws: InternalWalletState, + req: DeleteStoredBackupRequest, +): Promise<void> { + const backupsDb = await openStoredBackupsDatabase(ws.idb); + await backupsDb.mktxAll().runReadWrite(async (tx) => { + await tx.backupData.delete(req.name); + await tx.backupMeta.delete(req.name); + }); +} + +async function recoverStoredBackup( + ws: InternalWalletState, + req: RecoverStoredBackupRequest, +): Promise<void> { + logger.info(`Recovering stored backup ${req.name}`); + const { name } = req; + const backupsDb = await openStoredBackupsDatabase(ws.idb); + const bd = await backupsDb.mktxAll().runReadWrite(async (tx) => { + const backupMeta = tx.backupMeta.get(name); + if (!backupMeta) { + throw Error("backup not found"); + } + const backupData = await tx.backupData.get(name); + if (!backupData) { + throw Error("no backup data (DB corrupt)"); + } + return backupData; + }); + logger.info(`backup found, now importing`); + await importDb(ws.db.idbHandle(), bd); + logger.info(`import done`); +} + /** * Implementation of the "wallet-core" API. */ @@ -1059,12 +1040,18 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>( switch (operation) { case WalletApiOperation.CreateStoredBackup: return createStoredBackup(ws); - case WalletApiOperation.DeleteStoredBackup: + case WalletApiOperation.DeleteStoredBackup: { + const req = codecForDeleteStoredBackupRequest().decode(payload); + await deleteStoredBackup(ws, req); return {}; + } case WalletApiOperation.ListStoredBackups: + return listStoredBackups(ws); + case WalletApiOperation.RecoverStoredBackup: { + const req = codecForRecoverStoredBackupRequest().decode(payload); + await recoverStoredBackup(ws, req); return {}; - case WalletApiOperation.RecoverStoredBackup: - return {}; + } case WalletApiOperation.InitWallet: { logger.trace("initializing wallet"); ws.initCalled = true; @@ -1496,24 +1483,11 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>( return {}; } case WalletApiOperation.ListCurrencies: { - return await ws.db - .mktx((x) => [x.auditorTrust, x.exchangeTrust]) - .runReadOnly(async (tx) => { - const trustedAuditors = await tx.auditorTrust.iter().toArray(); - const trustedExchanges = await tx.exchangeTrust.iter().toArray(); - return { - trustedAuditors: trustedAuditors.map((x) => ({ - currency: x.currency, - auditorBaseUrl: x.auditorBaseUrl, - auditorPub: x.auditorPub, - })), - trustedExchanges: trustedExchanges.map((x) => ({ - currency: x.currency, - exchangeBaseUrl: x.exchangeBaseUrl, - exchangeMasterPub: x.exchangeMasterPub, - })), - }; - }); + // FIXME: Remove / change to scoped currency approach. + return { + trustedAuditors: [], + trustedExchanges: [], + }; } case WalletApiOperation.WithdrawFakebank: { const req = codecForWithdrawFakebankRequest().decode(payload); @@ -1612,6 +1586,12 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>( return await waitUntilDone(ws); case WalletApiOperation.TestingWaitRefreshesFinal: return await waitUntilRefreshesDone(ws); + case WalletApiOperation.TestingSetTimetravel: { + const req = codecForTestingSetTimetravelRequest().decode(payload); + setDangerousTimetravel(req.offsetMs); + ws.workAvailable.trigger(); + return {}; + } // default: // assertUnreachable(operation); } @@ -1712,14 +1692,6 @@ export class Wallet { public static defaultConfig: Readonly<WalletConfig> = { builtin: { exchanges: ["https://exchange.demo.taler.net/"], - auditors: [ - { - currency: "KUDOS", - auditorPub: "BW9DC48PHQY4NH011SHHX36DZZ3Q22Y6X7FZ1VD1CMZ2PTFZ6PN0", - auditorBaseUrl: "https://auditor.demo.taler.net/", - uids: ["5P25XF8TVQP9AW6VYGY2KV47WT5Y3ZXFSJAA570GJPX5SVJXKBVG"], - }, - ], }, features: { allowHttp: false, @@ -1792,7 +1764,6 @@ class InternalWalletStateImpl implements InternalWalletState { exchangeOps: ExchangeOperations = { getExchangeDetails, - getExchangeTrust, updateExchangeFromUrl, }; @@ -1901,35 +1872,27 @@ class InternalWalletStateImpl implements InternalWalletState { return computeRefundTransactionState(rec); } case TransactionType.PeerPullCredit: - const rec = await tx.peerPullPaymentInitiations.get( - parsedTxId.pursePub, - ); + const rec = await tx.peerPullCredit.get(parsedTxId.pursePub); if (!rec) { return undefined; } return computePeerPullCreditTransactionState(rec); case TransactionType.PeerPullDebit: { - const rec = await tx.peerPullPaymentIncoming.get( - parsedTxId.peerPullPaymentIncomingId, - ); + const rec = await tx.peerPullDebit.get(parsedTxId.peerPullDebitId); if (!rec) { return undefined; } return computePeerPullDebitTransactionState(rec); } case TransactionType.PeerPushCredit: { - const rec = await tx.peerPushPaymentIncoming.get( - parsedTxId.peerPushPaymentIncomingId, - ); + const rec = await tx.peerPushCredit.get(parsedTxId.peerPushCreditId); if (!rec) { return undefined; } return computePeerPushCreditTransactionState(rec); } case TransactionType.PeerPushDebit: { - const rec = await tx.peerPushPaymentInitiations.get( - parsedTxId.pursePub, - ); + const rec = await tx.peerPushDebit.get(parsedTxId.pursePub); if (!rec) { return undefined; } diff --git a/packages/taler-wallet-core/tsconfig.json b/packages/taler-wallet-core/tsconfig.json index 8de28b3d8..663a4dd98 100644 --- a/packages/taler-wallet-core/tsconfig.json +++ b/packages/taler-wallet-core/tsconfig.json @@ -4,12 +4,12 @@ "composite": true, "declaration": true, "declarationMap": false, - "target": "ES2017", - "module": "ESNext", + "target": "ES2020", + "module": "Node16", "moduleResolution": "Node16", "resolveJsonModule": true, "sourceMap": true, - "lib": ["es6"], + "lib": ["ES2020"], "resolvePackageJsonImports": true, "types": ["node"], "noImplicitReturns": true, @@ -33,8 +33,5 @@ "path": "../taler-util/" } ], - "include": [ - "src/**/*", - "src/*.json" - ] + "include": ["src/**/*", "src/*.json", "../taler-util/src/bank-api-client.ts"] } diff --git a/packages/taler-wallet-embedded/tsconfig.json b/packages/taler-wallet-embedded/tsconfig.json index 7b27ca6b7..e8b265fb9 100644 --- a/packages/taler-wallet-embedded/tsconfig.json +++ b/packages/taler-wallet-embedded/tsconfig.json @@ -5,10 +5,10 @@ "declaration": true, "declarationMap": true, "target": "ES6", - "module": "ESNext", + "module": "Node16", "moduleResolution": "Node16", "sourceMap": true, - "lib": ["es6"], + "lib": ["ES2020"], "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "strict": true, diff --git a/packages/taler-wallet-webextension/package.json b/packages/taler-wallet-webextension/package.json index 040878529..f442b1509 100644 --- a/packages/taler-wallet-webextension/package.json +++ b/packages/taler-wallet-webextension/package.json @@ -64,7 +64,7 @@ "preact-cli": "^3.3.5", "preact-render-to-string": "^5.1.19", "rimraf": "^3.0.2", - "typescript": "5.1.3" + "typescript": "5.2.2" }, "nyc": { "include": [ diff --git a/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx index 6cf863519..74c92cbc6 100644 --- a/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx +++ b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx @@ -73,11 +73,11 @@ const cd: WalletContractData = { country: "ar", }, }, - products: [], + // products: [], autoRefund: undefined, summaryI18n: undefined, - deliveryDate: undefined, - deliveryLocation: undefined, + // deliveryDate: undefined, + // deliveryLocation: undefined, }; export const ShowingSimpleOrder = tests.createExample(ShowView, { diff --git a/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx index 392a7d0e8..db9b6ebcd 100644 --- a/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx +++ b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx @@ -259,7 +259,7 @@ export function ShowView({ contractTerms, hideHandler }: States.Show): VNode { </span> </td> </tr> - <tr> + {/* <tr> <td> <i18n.Translate>Delivery date</i18n.Translate> </td> @@ -291,7 +291,7 @@ export function ShowView({ contractTerms, hideHandler }: States.Show): VNode { .map((p) => `${p.description} x ${p.quantity}`) .join(", ")} </td> - </tr> + </tr> */} <tr> <td> <i18n.Translate>Created at</i18n.Translate> @@ -334,8 +334,8 @@ export function ShowView({ contractTerms, hideHandler }: States.Show): VNode { !contractTerms.autoRefund ? Duration.getZero() : Duration.fromTalerProtocolDuration( - contractTerms.autoRefund, - ), + contractTerms.autoRefund, + ), )} format="dd MMMM yyyy, HH:mm" /> diff --git a/packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx b/packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx index 214c4d792..f6c176550 100644 --- a/packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx +++ b/packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx @@ -99,7 +99,7 @@ export function ShowButtonsNonAcceptedTosView({ </WarningText> </section> )} */} - {terms.status === ExchangeTosStatus.Pending && ( + {terms.status === ExchangeTosStatus.Accepted && ( <section style={{ justifyContent: "space-around", display: "flex" }}> <Button variant="contained" @@ -181,7 +181,7 @@ export function ShowTosContentView({ </LinkSuccess> </section> )} - {termsAccepted && terms.status !== ExchangeTosStatus.Proposed && ( + {termsAccepted && terms.status !== ExchangeTosStatus.Accepted && ( <section style={{ justifyContent: "space-around", display: "flex" }}> <CheckboxOutlined name="terms" diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts b/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts index 098f18921..8bae9470f 100644 --- a/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts +++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts @@ -78,7 +78,7 @@ export function useComponentState({ const { contractTerms, - peerPullPaymentIncomingId, + peerPullDebitId, amountEffective, amountRaw, } = hook.response.p2p; @@ -155,7 +155,7 @@ export function useComponentState({ const resp = await api.wallet.call( WalletApiOperation.ConfirmPeerPullDebit, { - peerPullPaymentIncomingId, + peerPullDebitId, }, ); onSuccess(resp.transactionId); diff --git a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx index 0bdadef0e..c00e570f9 100644 --- a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx +++ b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx @@ -76,7 +76,7 @@ export function BaseView(state: SupportedStates): VNode { ...contractTerms, orderId: contractTerms.order_id, contractTermsHash: "", - products: contractTerms.products!, + // products: contractTerms.products!, }} proposalId={state.payStatus.proposalId} /> diff --git a/packages/taler-wallet-webextension/src/cta/Refund/index.ts b/packages/taler-wallet-webextension/src/cta/Refund/index.ts index bbb72c328..42e9cc534 100644 --- a/packages/taler-wallet-webextension/src/cta/Refund/index.ts +++ b/packages/taler-wallet-webextension/src/cta/Refund/index.ts @@ -49,7 +49,7 @@ export namespace State { interface BaseInfo { merchantName: string; - products: Product[] | undefined; + // products: Product[] | undefined; amount: AmountJson; // awaitingAmount: AmountJson; // granted: AmountJson; diff --git a/packages/taler-wallet-webextension/src/cta/Refund/state.ts b/packages/taler-wallet-webextension/src/cta/Refund/state.ts index 0ba77a19d..6c0f37471 100644 --- a/packages/taler-wallet-webextension/src/cta/Refund/state.ts +++ b/packages/taler-wallet-webextension/src/cta/Refund/state.ts @@ -106,7 +106,7 @@ export function useComponentState({ // granted: Amounts.parseOrThrow(info.response.refund.granted), // awaitingAmount: Amounts.parseOrThrow(refund.awaiting), merchantName: purchase.info.merchant.name, - products: purchase.info.products, + // products: purchase.info.products, error: undefined, }; diff --git a/packages/taler-wallet-webextension/src/cta/Refund/stories.tsx b/packages/taler-wallet-webextension/src/cta/Refund/stories.tsx index ef1f76033..03d55ee91 100644 --- a/packages/taler-wallet-webextension/src/cta/Refund/stories.tsx +++ b/packages/taler-wallet-webextension/src/cta/Refund/stories.tsx @@ -47,7 +47,7 @@ export const Ready = tests.createExample(ReadyView, { // awaitingAmount: Amounts.parseOrThrow("USD:1"), // granted: Amounts.parseOrThrow("USD:0"), merchantName: "the merchant", - products: [], + // products: [], orderId: "abcdef", }); @@ -60,18 +60,18 @@ export const WithAProductList = tests.createExample(ReadyView, { // awaitingAmount: Amounts.parseOrThrow("USD:1"), // granted: Amounts.parseOrThrow("USD:0"), merchantName: "the merchant", - products: [ - { - description: "beer", - image: beer, - quantity: 2, - }, - { - description: "t-shirt", - price: "EUR:1", - quantity: 5, - }, - ], + // products: [ + // { + // description: "beer", + // image: beer, + // quantity: 2, + // }, + // { + // description: "t-shirt", + // price: "EUR:1", + // quantity: 5, + // }, + // ], orderId: "abcdef", }); diff --git a/packages/taler-wallet-webextension/src/cta/Refund/views.tsx b/packages/taler-wallet-webextension/src/cta/Refund/views.tsx index accdab0c3..b6638cf8b 100644 --- a/packages/taler-wallet-webextension/src/cta/Refund/views.tsx +++ b/packages/taler-wallet-webextension/src/cta/Refund/views.tsx @@ -104,11 +104,11 @@ export function ReadyView(state: State.Ready): VNode { kind="positive" /> */} </section> - {state.products && state.products.length ? ( + {/* {state.products && state.products.length ? ( <section> <ProductList products={state.products} /> </section> - ) : undefined} + ) : undefined} */} <section> <Button variant="contained" diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts b/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts index dcd41bcc1..77333e15c 100644 --- a/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts +++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts @@ -118,8 +118,8 @@ export function useComponentState({ subject === undefined ? undefined : !subject - ? "Can't be empty" - : undefined, + ? "Can't be empty" + : undefined, value: subject ?? "", onInput: pushAlertOnError(async (e) => setSubject(e)), }, @@ -172,6 +172,10 @@ async function checkPeerPushDebitAndCheckMax( //a good response that allow us to try again throw e; } + if (Amounts.cmp(newAmount, amount) === 1) { + //how can this happen? + throw e; + } return checkPeerPushDebitAndCheckMax(api, Amounts.stringify(newAmount)); } } diff --git a/packages/taler-wallet-webextension/src/cta/TransferPickup/state.ts b/packages/taler-wallet-webextension/src/cta/TransferPickup/state.ts index fb9acbe83..47c644736 100644 --- a/packages/taler-wallet-webextension/src/cta/TransferPickup/state.ts +++ b/packages/taler-wallet-webextension/src/cta/TransferPickup/state.ts @@ -58,7 +58,7 @@ export function useComponentState({ const { contractTerms, - peerPushPaymentIncomingId, + peerPushCreditId, amountEffective, amountRaw, } = hook.response; @@ -72,7 +72,7 @@ export function useComponentState({ const resp = await api.wallet.call( WalletApiOperation.ConfirmPeerPushCredit, { - peerPushPaymentIncomingId, + peerPushCreditId, }, ); onSuccess(resp.transactionId); diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts index 72cabe5a4..a3855c2f4 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts +++ b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts @@ -23,7 +23,6 @@ import { ExchangeTosStatus, TalerError, parseWithdrawExchangeUri, - stringifyWithdrawUri, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { useEffect, useState } from "preact/hooks"; diff --git a/packages/taler-wallet-webextension/src/hooks/useDiagnostics.ts b/packages/taler-wallet-webextension/src/hooks/useDiagnostics.ts deleted file mode 100644 index fcd31b3c6..000000000 --- a/packages/taler-wallet-webextension/src/hooks/useDiagnostics.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022 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 { WalletDiagnostics } from "@gnu-taler/taler-util"; -import { useEffect, useState } from "preact/hooks"; -import { useBackendContext } from "../context/backend.js"; - -export function useDiagnostics(): [WalletDiagnostics | undefined, boolean] { - const [timedOut, setTimedOut] = useState(false); - const api = useBackendContext(); - const [diagnostics, setDiagnostics] = useState<WalletDiagnostics | undefined>( - undefined, - ); - - useEffect(() => { - let gotDiagnostics = false; - setTimeout(() => { - if (!gotDiagnostics) { - console.error("timed out"); - setTimedOut(true); - } - }, 1000); - const doFetch = async (): Promise<void> => { - const d = await api.background.call("getDiagnostics", undefined); - gotDiagnostics = true; - setDiagnostics(d); - }; - doFetch(); - }, []); - return [diagnostics, timedOut]; -} diff --git a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx index c5e5c3c07..c972f0919 100644 --- a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx @@ -27,24 +27,21 @@ import { PendingTaskInfo, WalletApiOperation, } from "@gnu-taler/taler-wallet-core"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useEffect, useRef, useState } from "preact/hooks"; -import { Diagnostics } from "../components/Diagnostics.js"; import { SelectList } from "../components/SelectList.js"; -import { NotifyUpdateFadeOut } from "../components/styled/index.js"; import { Time } from "../components/Time.js"; +import { NotifyUpdateFadeOut } from "../components/styled/index.js"; import { useBackendContext } from "../context/backend.js"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; -import { useDiagnostics } from "../hooks/useDiagnostics.js"; import { Button } from "../mui/Button.js"; import { Grid } from "../mui/Grid.js"; import { Paper } from "../mui/Paper.js"; import { TextField } from "../mui/TextField.js"; export function DeveloperPage(): VNode { - const [status, timedOut] = useDiagnostics(); const listenAllEvents = Array.from<NotificationType>({ length: 1 }); @@ -73,13 +70,11 @@ export function DeveloperPage(): VNode { response === undefined ? nonResponse : response.hasError - ? nonResponse - : response.response; + ? nonResponse + : response.response; return ( <View - status={status} - timedOut={timedOut} operations={operations} coins={coins} exchanges={exchanges} @@ -108,8 +103,6 @@ type SplitedCoinInfo = { }; export interface Props { - status: any; - timedOut: boolean; operations: PendingTaskInfo[]; coins: CoinsInfo; exchanges: ExchangeListItem[]; @@ -121,8 +114,6 @@ function hashObjectId(o: any): string { } export function View({ - status, - timedOut, operations, coins, onDownloadDatabase, @@ -458,7 +449,6 @@ export function View({ ); })} <br /> - <Diagnostics diagnostics={status} timedOut={timedOut} /> {operations && operations.length > 0 && ( <Fragment> <p> diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx index b32eb831a..6ade0718a 100644 --- a/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx @@ -16,7 +16,6 @@ import { Amounts, - BackupBackupProviderTerms, canonicalizeBaseUrl, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; @@ -41,6 +40,12 @@ interface Props { onBack: () => Promise<void>; } +interface BackupBackupProviderTerms { + annual_fee: string; + storage_limit_in_megabytes: number; + supported_protocol_version: string; +} + export function ProviderAddPage({ onBack }: Props): VNode { const [verifying, setVerifying] = useState< | { url: string; name: string; provider: BackupBackupProviderTerms } diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx index 3ba3ac591..f1ae84ed3 100644 --- a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx @@ -378,9 +378,9 @@ export const PaymentWithDeliveryDate = tests.createExample(TestedComponent, { amountRaw: "KUDOS:12", info: { ...exampleData.payment.info, - delivery_date: { - t_s: new Date().getTime() / 1000, - }, + // delivery_date: { + // t_s: new Date().getTime() / 1000, + // }, }, }, }); @@ -391,12 +391,12 @@ export const PaymentWithDeliveryAddr = tests.createExample(TestedComponent, { amountRaw: "KUDOS:12", info: { ...exampleData.payment.info, - delivery_location: { - country: "Argentina", - street: "Elm Street", - district: "CABA", - post_code: "1101", - }, + // delivery_location: { + // country: "Argentina", + // street: "Elm Street", + // district: "CABA", + // post_code: "1101", + // }, }, }, }); @@ -407,15 +407,15 @@ export const PaymentWithDeliveryFull = tests.createExample(TestedComponent, { amountRaw: "KUDOS:12", info: { ...exampleData.payment.info, - delivery_date: { - t_s: new Date().getTime() / 1000, - }, - delivery_location: { - country: "Argentina", - street: "Elm Street", - district: "CABA", - post_code: "1101", - }, + // delivery_date: { + // t_s: new Date().getTime() / 1000, + // }, + // delivery_location: { + // country: "Argentina", + // street: "Elm Street", + // district: "CABA", + // post_code: "1101", + // }, }, }, }); diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx index e54137016..3b76558ce 100644 --- a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx @@ -237,8 +237,8 @@ function TransactionTemplate({ <Fragment> <section style={{ padding: 8, textAlign: "center" }}> {transaction?.error && - // FIXME: wallet core should stop sending this error on KYC - transaction.error.code !== + // FIXME: wallet core should stop sending this error on KYC + transaction.error.code !== TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED ? ( <ErrorAlertView error={alertFromError( @@ -464,7 +464,7 @@ export function TransactionView({ {/**FIXME: DD37 check if this holds */} {transaction.txState.major !== - TransactionMajorState.Pending ? undefined : transaction + TransactionMajorState.Pending ? undefined : transaction .withdrawalDetails.type === WithdrawalType.ManualTransfer ? ( //manual withdrawal <BankDetailsByPaytoType @@ -1073,126 +1073,126 @@ export function MerchantDetails({ ); } -function DeliveryDetails({ - date, - location, -}: { - date: TalerProtocolTimestamp | undefined; - location: Location | undefined; -}): VNode { - const { i18n } = useTranslationContext(); - return ( - <PurchaseDetailsTable> - {location && ( - <Fragment> - {location.country && ( - <tr> - <td> - <i18n.Translate>Country</i18n.Translate> - </td> - <td>{location.country}</td> - </tr> - )} - {location.address_lines && ( - <tr> - <td> - <i18n.Translate>Address lines</i18n.Translate> - </td> - <td>{location.address_lines}</td> - </tr> - )} - {location.building_number && ( - <tr> - <td> - <i18n.Translate>Building number</i18n.Translate> - </td> - <td>{location.building_number}</td> - </tr> - )} - {location.building_name && ( - <tr> - <td> - <i18n.Translate>Building name</i18n.Translate> - </td> - <td>{location.building_name}</td> - </tr> - )} - {location.street && ( - <tr> - <td> - <i18n.Translate>Street</i18n.Translate> - </td> - <td>{location.street}</td> - </tr> - )} - {location.post_code && ( - <tr> - <td> - <i18n.Translate>Post code</i18n.Translate> - </td> - <td>{location.post_code}</td> - </tr> - )} - {location.town_location && ( - <tr> - <td> - <i18n.Translate>Town location</i18n.Translate> - </td> - <td>{location.town_location}</td> - </tr> - )} - {location.town && ( - <tr> - <td> - <i18n.Translate>Town</i18n.Translate> - </td> - <td>{location.town}</td> - </tr> - )} - {location.district && ( - <tr> - <td> - <i18n.Translate>District</i18n.Translate> - </td> - <td>{location.district}</td> - </tr> - )} - {location.country_subdivision && ( - <tr> - <td> - <i18n.Translate>Country subdivision</i18n.Translate> - </td> - <td>{location.country_subdivision}</td> - </tr> - )} - </Fragment> - )} - - {!location || !date ? undefined : ( - <tr> - <td colSpan={2}> - <hr /> - </td> - </tr> - )} - {date && ( - <Fragment> - <tr> - <td> - <i18n.Translate>Date</i18n.Translate> - </td> - <td> - <Time - timestamp={AbsoluteTime.fromProtocolTimestamp(date)} - format="dd MMMM yyyy, HH:mm" - /> - </td> - </tr> - </Fragment> - )} - </PurchaseDetailsTable> - ); -} +// function DeliveryDetails({ +// date, +// location, +// }: { +// date: TalerProtocolTimestamp | undefined; +// location: Location | undefined; +// }): VNode { +// const { i18n } = useTranslationContext(); +// return ( +// <PurchaseDetailsTable> +// {location && ( +// <Fragment> +// {location.country && ( +// <tr> +// <td> +// <i18n.Translate>Country</i18n.Translate> +// </td> +// <td>{location.country}</td> +// </tr> +// )} +// {location.address_lines && ( +// <tr> +// <td> +// <i18n.Translate>Address lines</i18n.Translate> +// </td> +// <td>{location.address_lines}</td> +// </tr> +// )} +// {location.building_number && ( +// <tr> +// <td> +// <i18n.Translate>Building number</i18n.Translate> +// </td> +// <td>{location.building_number}</td> +// </tr> +// )} +// {location.building_name && ( +// <tr> +// <td> +// <i18n.Translate>Building name</i18n.Translate> +// </td> +// <td>{location.building_name}</td> +// </tr> +// )} +// {location.street && ( +// <tr> +// <td> +// <i18n.Translate>Street</i18n.Translate> +// </td> +// <td>{location.street}</td> +// </tr> +// )} +// {location.post_code && ( +// <tr> +// <td> +// <i18n.Translate>Post code</i18n.Translate> +// </td> +// <td>{location.post_code}</td> +// </tr> +// )} +// {location.town_location && ( +// <tr> +// <td> +// <i18n.Translate>Town location</i18n.Translate> +// </td> +// <td>{location.town_location}</td> +// </tr> +// )} +// {location.town && ( +// <tr> +// <td> +// <i18n.Translate>Town</i18n.Translate> +// </td> +// <td>{location.town}</td> +// </tr> +// )} +// {location.district && ( +// <tr> +// <td> +// <i18n.Translate>District</i18n.Translate> +// </td> +// <td>{location.district}</td> +// </tr> +// )} +// {location.country_subdivision && ( +// <tr> +// <td> +// <i18n.Translate>Country subdivision</i18n.Translate> +// </td> +// <td>{location.country_subdivision}</td> +// </tr> +// )} +// </Fragment> +// )} + +// {!location || !date ? undefined : ( +// <tr> +// <td colSpan={2}> +// <hr /> +// </td> +// </tr> +// )} +// {date && ( +// <Fragment> +// <tr> +// <td> +// <i18n.Translate>Date</i18n.Translate> +// </td> +// <td> +// <Time +// timestamp={AbsoluteTime.fromProtocolTimestamp(date)} +// format="dd MMMM yyyy, HH:mm" +// /> +// </td> +// </tr> +// </Fragment> +// )} +// </PurchaseDetailsTable> +// ); +// } export function ExchangeDetails({ exchange }: { exchange: string }): VNode { return ( @@ -1475,10 +1475,10 @@ export function PurchaseDetails({ const total = Amounts.add(price.value, price.fee).amount; - const hasProducts = info.products && info.products.length > 0; + // const hasProducts = info.products && info.products.length > 0; - const hasShipping = - info.delivery_date !== undefined || info.delivery_location !== undefined; + // const hasShipping = + // info.delivery_date !== undefined || info.delivery_location !== undefined; const showLargePic = (): void => { return; @@ -1558,7 +1558,7 @@ export function PurchaseDetails({ </tr> </Fragment> )} - {hasProducts && ( + {/* {hasProducts && ( <tr> <td colSpan={2}> <PartCollapsible @@ -1586,8 +1586,8 @@ export function PurchaseDetails({ /> </td> </tr> - )} - {hasShipping && ( + )} */} + {/* {hasShipping && ( <tr> <td colSpan={2}> <PartCollapsible @@ -1602,7 +1602,7 @@ export function PurchaseDetails({ /> </td> </tr> - )} + )} */} <tr> <td> <ShowFullContractTermPopup proposalId={proposalId} /> @@ -2008,7 +2008,7 @@ function ShowWithdrawalDetailForBankIntegrated({ /> )} {!transaction.withdrawalDetails.confirmed && - transaction.withdrawalDetails.bankConfirmationUrl ? ( + transaction.withdrawalDetails.bankConfirmationUrl ? ( <InfoBox> <div style={{ display: "block" }}> <i18n.Translate> diff --git a/packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx index 2cf28b611..dfce1c14b 100644 --- a/packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx @@ -29,17 +29,9 @@ export default { export const Normal = tests.createExample(TestedComponent, { permissionToggle: { value: true, button: {} }, - diagnostics: { - errors: [], - walletManifestVersion: "1.0", - walletManifestDisplayVersion: "1.0", - firefoxIdbProblem: false, - dbOutdated: false, - }, }); export const TimedoutDiagnostics = tests.createExample(TestedComponent, { - timedOut: true, permissionToggle: { value: true, button: {} }, }); diff --git a/packages/taler-wallet-webextension/src/wallet/Welcome.tsx b/packages/taler-wallet-webextension/src/wallet/Welcome.tsx index 8d348ca9d..e19152be2 100644 --- a/packages/taler-wallet-webextension/src/wallet/Welcome.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Welcome.tsx @@ -25,7 +25,6 @@ import { Fragment, h, VNode } from "preact"; import { Checkbox } from "../components/Checkbox.js"; import { SubTitle, Title } from "../components/styled/index.js"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { useDiagnostics } from "../hooks/useDiagnostics.js"; import { useSettings } from "../hooks/useSettings.js"; import { ToggleHandler } from "../mui/handlers.js"; import { platform } from "../platform/foreground.js"; @@ -34,7 +33,6 @@ import { useAlertContext } from "../context/alert.js"; export function WelcomePage(): VNode { const [settings, updateSettings] = useSettings(); const { safely } = useAlertContext(); - const [diagnostics, timedOut] = useDiagnostics(); return ( <View permissionToggle={{ @@ -45,21 +43,15 @@ export function WelcomePage(): VNode { ), }, }} - diagnostics={diagnostics} - timedOut={timedOut} /> ); } export interface ViewProps { permissionToggle: ToggleHandler; - diagnostics: WalletDiagnostics | undefined; - timedOut: boolean; } export function View({ permissionToggle, - diagnostics, - timedOut, }: ViewProps): VNode { const { i18n } = useTranslationContext(); return ( diff --git a/packages/taler-wallet-webextension/src/wxApi.ts b/packages/taler-wallet-webextension/src/wxApi.ts index 46c9f1b2d..004faad5c 100644 --- a/packages/taler-wallet-webextension/src/wxApi.ts +++ b/packages/taler-wallet-webextension/src/wxApi.ts @@ -70,10 +70,6 @@ export interface BackgroundOperations { request: void; response: void; }; - getDiagnostics: { - request: void; - response: WalletDiagnostics; - }; runGarbageCollector: { request: void; response: void; diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts index e7385abe5..40b7077af 100644 --- a/packages/taler-wallet-webextension/src/wxBackend.ts +++ b/packages/taler-wallet-webextension/src/wxBackend.ts @@ -51,14 +51,11 @@ import { importDb, openPromise, } from "@gnu-taler/taler-wallet-core"; -import { - MessageFromBackend, - MessageFromFrontend, - MessageResponse, -} from "./platform/api.js"; +import { MessageFromFrontend, MessageResponse } from "./platform/api.js"; import { platform } from "./platform/background.js"; import { ExtensionOperations } from "./taler-wallet-interaction-loader.js"; import { BackgroundOperations } from "./wxApi.js"; +import { HttpRequestLibrary } from "@gnu-taler/taler-util/http"; /** * Currently active wallet instance. Might be unloaded and @@ -79,43 +76,6 @@ const walletInit: OpenedPromise<void> = openPromise<void>(); const logger = new Logger("wxBackend.ts"); -async function getDiagnostics(): Promise<WalletDiagnostics> { - const manifestData = platform.getWalletWebExVersion(); - const errors: string[] = []; - let firefoxIdbProblem = false; - let dbOutdated = false; - try { - await walletInit.promise; - } catch (e) { - errors.push("Error during wallet initialization: " + e); - if ( - currentDatabase === undefined && - outdatedDbVersion === undefined && - platform.isFirefox() - ) { - firefoxIdbProblem = true; - } - } - if (!currentWallet) { - errors.push("Could not create wallet backend."); - } - if (!currentDatabase) { - errors.push("Could not open database"); - } - if (outdatedDbVersion !== undefined) { - errors.push(`Outdated DB version: ${outdatedDbVersion}`); - dbOutdated = true; - } - const diagnostics: WalletDiagnostics = { - walletManifestDisplayVersion: manifestData.version_name || "(undefined)", - walletManifestVersion: manifestData.version, - errors, - firefoxIdbProblem, - dbOutdated, - }; - return diagnostics; -} - type BackendHandlerType = { [Op in keyof BackgroundOperations]: ( req: BackgroundOperations[Op]["request"], @@ -175,7 +135,6 @@ async function isInjectionEnabled(): Promise<boolean> { const backendHandlers: BackendHandlerType = { freeze, sum, - getDiagnostics, resetDb, runGarbageCollector, setLoggingLevel, @@ -297,7 +256,7 @@ async function reinitWallet(): Promise<void> { } currentDatabase = undefined; // setBadgeText({ text: "" }); - let httpLib; + let httpLib: HttpRequestLibrary; let cryptoWorker; let timer; @@ -318,7 +277,7 @@ async function reinitWallet(): Promise<void> { logger.info("Setting up wallet"); const wallet = await Wallet.create( indexedDB as any, - httpLib, + httpLib as any, timer, cryptoWorker, { diff --git a/packages/taler-wallet-webextension/tsconfig.json b/packages/taler-wallet-webextension/tsconfig.json index 773853302..08cbc871a 100644 --- a/packages/taler-wallet-webextension/tsconfig.json +++ b/packages/taler-wallet-webextension/tsconfig.json @@ -1,15 +1,12 @@ { "compilerOptions": { "composite": true, - "lib": [ - "es2021", - "DOM" - ], - "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ - "jsxFactory": "h", /* Specify the JSX factory function to use when targeting react JSX emit, e.g. React.createElement or h. */ + "lib": ["es2020", "DOM"], + "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, + "jsxFactory": "h" /* Specify the JSX factory function to use when targeting react JSX emit, e.g. React.createElement or h. */, "jsxFragmentFactory": "Fragment", // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#custom-jsx-factories "moduleResolution": "Node16", - "module": "ES2020", + "module": "Node16", "target": "ES6", "skipLibCheck": true, "preserveSymlinks": true, @@ -23,9 +20,7 @@ "esModuleInterop": true, "importHelpers": true, "rootDir": "./src", - "typeRoots": [ - "./node_modules/@types" - ] + "typeRoots": ["./node_modules/@types"] }, "references": [ { @@ -35,7 +30,5 @@ "path": "../taler-util/" } ], - "include": [ - "src/**/*" - ] + "include": ["src/**/*"] } diff --git a/packages/web-util/package.json b/packages/web-util/package.json index 6cfe19258..81eee949a 100644 --- a/packages/web-util/package.json +++ b/packages/web-util/package.json @@ -56,7 +56,7 @@ "sass": "1.56.1", "swr": "2.0.3", "tslib": "^2.5.3", - "typescript": "^5.1.3", + "typescript": "^5.2.2", "ws": "7.4.5" }, "dependencies": { diff --git a/packages/web-util/tsconfig.json b/packages/web-util/tsconfig.json index fc69cad06..a315dda1c 100644 --- a/packages/web-util/tsconfig.json +++ b/packages/web-util/tsconfig.json @@ -3,17 +3,14 @@ "composite": true, "declaration": true, "declarationMap": true, - "target": "ES6", - "module": "ESNext", + "target": "ES2020", + "module": "Node16", "jsx": "react", "jsxFactory": "h", "jsxFragmentFactory": "Fragment", "moduleResolution": "Node16", "sourceMap": true, - "lib": [ - "DOM", - "es6" - ], + "lib": ["DOM", "ES2020"], "outDir": "lib", "preserveSymlinks": true, "skipLibCheck": true, @@ -27,11 +24,7 @@ "esModuleInterop": true, "importHelpers": true, "rootDir": "./src", - "typeRoots": [ - "./node_modules/@types" - ] + "typeRoots": ["./node_modules/@types"] }, - "include": [ - "src/**/*" - ] + "include": ["src/**/*"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b733353b..347dbf0a5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: prettier: specifier: ^2.8.8 version: 2.8.8 + typescript: + specifier: ^5.2.2 + version: 5.2.2 packages/aml-backoffice-ui: dependencies: @@ -92,7 +95,7 @@ importers: version: 0.17.19 eslint-config-preact: specifier: ^1.2.0 - version: 1.3.0(@typescript-eslint/eslint-plugin@5.41.0)(eslint@8.42.0)(typescript@5.1.3) + version: 1.3.0(@typescript-eslint/eslint-plugin@5.41.0)(eslint@8.48.0)(typescript@5.2.2) mocha: specifier: ^9.2.0 version: 9.2.2 @@ -109,8 +112,8 @@ importers: specifier: ^3.3.2 version: 3.3.2 typescript: - specifier: 5.1.3 - version: 5.1.3 + specifier: 5.2.2 + version: 5.2.2 packages/anastasis-cli: dependencies: @@ -134,11 +137,11 @@ importers: specifier: ^3.0.2 version: 3.0.2 typedoc: - specifier: ^0.24.8 - version: 0.24.8(typescript@5.1.3) + specifier: ^0.25.1 + version: 0.25.1(typescript@5.2.2) typescript: - specifier: ^5.1.3 - version: 5.1.3 + specifier: ^5.2.2 + version: 5.2.2 packages/anastasis-core: dependencies: @@ -159,8 +162,8 @@ importers: specifier: ^3.0.2 version: 3.0.2 typescript: - specifier: ^5.1.3 - version: 5.1.3 + specifier: ^5.2.2 + version: 5.2.2 packages/anastasis-webui: dependencies: @@ -220,8 +223,8 @@ importers: specifier: 1.56.1 version: 1.56.1 typescript: - specifier: ^5.1.3 - version: 5.1.3 + specifier: ^5.2.2 + version: 5.2.2 packages/demobank-ui: dependencies: @@ -273,10 +276,10 @@ importers: version: 18.11.17 '@typescript-eslint/eslint-plugin': specifier: ^5.41.0 - version: 5.41.0(@typescript-eslint/parser@5.41.0)(eslint@8.42.0)(typescript@5.1.3) + version: 5.41.0(@typescript-eslint/parser@5.41.0)(eslint@8.48.0)(typescript@5.2.2) '@typescript-eslint/parser': specifier: ^5.41.0 - version: 5.41.0(eslint@8.42.0)(typescript@5.1.3) + version: 5.41.0(eslint@8.48.0)(typescript@5.2.2) bulma: specifier: ^0.9.4 version: 0.9.4 @@ -294,7 +297,7 @@ importers: version: 0.17.7 eslint-config-preact: specifier: ^1.2.0 - version: 1.3.0(@typescript-eslint/eslint-plugin@5.41.0)(eslint@8.42.0)(typescript@5.1.3) + version: 1.3.0(@typescript-eslint/eslint-plugin@5.41.0)(eslint@8.48.0)(typescript@5.2.2) mocha: specifier: ^9.2.0 version: 9.2.2 @@ -308,8 +311,8 @@ importers: specifier: 1.56.1 version: 1.56.1 typescript: - specifier: 5.1.3 - version: 5.1.3 + specifier: 5.2.2 + version: 5.2.2 packages/idb-bridge: dependencies: @@ -319,7 +322,7 @@ importers: optionalDependencies: better-sqlite3: specifier: ^8.4.0 - version: 8.4.0 + version: 8.6.0 devDependencies: '@types/better-sqlite3': specifier: ^7.6.4 @@ -337,8 +340,8 @@ importers: specifier: ^5.0.1 version: 5.0.1 typescript: - specifier: ^5.1.6 - version: 5.1.6 + specifier: ^5.2.2 + version: 5.2.2 packages/merchant-backend-ui: dependencies: @@ -381,10 +384,10 @@ importers: version: 4.2.1 '@typescript-eslint/eslint-plugin': specifier: ^4.22.0 - version: 4.33.0(@typescript-eslint/parser@4.33.0)(eslint@7.32.0)(typescript@5.1.3) + version: 4.33.0(@typescript-eslint/parser@4.33.0)(eslint@7.32.0)(typescript@5.2.2) '@typescript-eslint/parser': specifier: ^4.22.0 - version: 4.33.0(eslint@7.32.0)(typescript@5.1.3) + version: 4.33.0(eslint@7.32.0)(typescript@5.2.2) babel-loader: specifier: ^8.2.2 version: 8.2.5(@babel/core@7.18.9)(webpack@4.46.0) @@ -396,7 +399,7 @@ importers: version: 7.32.0 eslint-config-preact: specifier: ^1.1.4 - version: 1.3.0(@typescript-eslint/eslint-plugin@4.33.0)(eslint@7.32.0)(typescript@5.1.3) + version: 1.3.0(@typescript-eslint/eslint-plugin@4.33.0)(eslint@7.32.0)(typescript@5.2.2) eslint-plugin-header: specifier: ^3.1.1 version: 3.1.1(eslint@7.32.0) @@ -417,13 +420,13 @@ importers: version: 1.0.14 ts-node: specifier: ^10.9.1 - version: 10.9.1(@types/node@20.4.1)(typescript@5.1.3) + version: 10.9.1(@types/node@20.5.9)(typescript@5.2.2) tslib: specifier: 2.5.3 version: 2.5.3 typescript: - specifier: 5.1.3 - version: 5.1.3 + specifier: 5.2.2 + version: 5.2.2 packages/merchant-backoffice-ui: dependencies: @@ -452,8 +455,8 @@ importers: specifier: 1.4.4 version: 1.4.4 swr: - specifier: 1.3.0 - version: 1.3.0(react@18.2.0) + specifier: 2.2.2 + version: 2.2.2(react@18.2.0) yup: specifier: ^0.32.9 version: 0.32.11 @@ -478,10 +481,10 @@ importers: version: 18.11.17 '@typescript-eslint/eslint-plugin': specifier: ^4.22.0 - version: 4.33.0(@typescript-eslint/parser@4.33.0)(eslint@7.32.0)(typescript@5.1.3) + version: 4.33.0(@typescript-eslint/parser@4.33.0)(eslint@7.32.0)(typescript@5.2.2) '@typescript-eslint/parser': specifier: ^4.22.0 - version: 4.33.0(eslint@7.32.0)(typescript@5.1.3) + version: 4.33.0(eslint@7.32.0)(typescript@5.2.2) base64-inline-loader: specifier: ^1.1.1 version: 1.1.1(webpack@4.46.0) @@ -517,7 +520,7 @@ importers: version: 7.32.0 eslint-config-preact: specifier: ^1.1.4 - version: 1.3.0(@typescript-eslint/eslint-plugin@4.33.0)(eslint@7.32.0)(typescript@5.1.3) + version: 1.3.0(@typescript-eslint/eslint-plugin@4.33.0)(eslint@7.32.0)(typescript@5.2.2) eslint-plugin-header: specifier: ^3.1.1 version: 3.1.1(eslint@7.32.0) @@ -529,7 +532,7 @@ importers: version: 0.0.10 html-webpack-skip-assets-plugin: specifier: ^1.0.1 - version: 1.0.3(html-webpack-plugin@5.5.1)(webpack@4.46.0) + version: 1.0.3(html-webpack-plugin@5.5.3)(webpack@4.46.0) inline-chunk-html-plugin: specifier: ^1.1.1 version: 1.1.1 @@ -549,11 +552,11 @@ importers: specifier: ^0.5.21 version: 0.5.21 typedoc: - specifier: ^0.24.8 - version: 0.24.8(typescript@5.1.3) + specifier: ^0.25.1 + version: 0.25.1(typescript@5.2.2) typescript: - specifier: 5.1.3 - version: 5.1.3 + specifier: 5.2.2 + version: 5.2.2 packages/pogen: dependencies: @@ -568,8 +571,8 @@ importers: specifier: ^0.4.5 version: 0.4.5 typescript: - specifier: ^5.1.3 - version: 5.1.3 + specifier: ^5.2.2 + version: 5.2.2 packages/taler-harness: dependencies: @@ -596,8 +599,8 @@ importers: specifier: ^3.0.2 version: 3.0.2 typescript: - specifier: ^5.1.3 - version: 5.1.3 + specifier: ^5.2.2 + version: 5.2.2 packages/taler-util: dependencies: @@ -633,8 +636,8 @@ importers: specifier: ^3.0.2 version: 3.0.2 typescript: - specifier: ^5.1.3 - version: 5.1.3 + specifier: ^5.2.2 + version: 5.2.2 packages/taler-wallet-cli: dependencies: @@ -658,11 +661,11 @@ importers: specifier: ^3.0.2 version: 3.0.2 typedoc: - specifier: ^0.24.8 - version: 0.24.8(typescript@5.1.3) + specifier: ^0.25.1 + version: 0.25.1(typescript@5.2.2) typescript: - specifier: ^5.1.3 - version: 5.1.3 + specifier: ^5.2.2 + version: 5.2.2 packages/taler-wallet-core: dependencies: @@ -693,10 +696,10 @@ importers: version: link:../pogen '@typescript-eslint/eslint-plugin': specifier: ^5.36.1 - version: 5.41.0(@typescript-eslint/parser@5.41.0)(eslint@8.26.0)(typescript@5.1.3) + version: 5.41.0(@typescript-eslint/parser@5.41.0)(eslint@8.26.0)(typescript@5.2.2) '@typescript-eslint/parser': specifier: ^5.36.1 - version: 5.41.0(eslint@8.26.0)(typescript@5.1.3) + version: 5.41.0(eslint@8.26.0)(typescript@5.2.2) ava: specifier: ^4.3.3 version: 4.3.3(@ava/typescript@4.0.0) @@ -734,11 +737,11 @@ importers: specifier: ^3.0.2 version: 3.0.2 typedoc: - specifier: ^0.24.8 - version: 0.24.8(typescript@5.1.3) + specifier: ^0.25.1 + version: 0.25.1(typescript@5.2.2) typescript: - specifier: ^5.1.3 - version: 5.1.3 + specifier: ^5.2.2 + version: 5.2.2 packages/taler-wallet-embedded: dependencies: @@ -803,10 +806,10 @@ importers: devDependencies: '@babel/preset-react': specifier: ^7.22.3 - version: 7.22.3(@babel/core@7.18.9) + version: 7.22.3(@babel/core@7.22.15) '@babel/preset-typescript': specifier: 7.18.6 - version: 7.18.6(@babel/core@7.18.9) + version: 7.18.6(@babel/core@7.22.15) '@gnu-taler/pogen': specifier: workspace:* version: link:../pogen @@ -865,8 +868,8 @@ importers: specifier: ^3.0.2 version: 3.0.2 typescript: - specifier: 5.1.3 - version: 5.1.3 + specifier: 5.2.2 + version: 5.2.2 packages/web-util: dependencies: @@ -956,14 +959,19 @@ importers: specifier: ^2.5.3 version: 2.5.3 typescript: - specifier: ^5.1.3 - version: 5.1.3 + specifier: ^5.2.2 + version: 5.2.2 ws: specifier: 7.4.5 version: 7.4.5 packages: + /@aashutoshrathi/word-wrap@1.2.6: + resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} + engines: {node: '>=0.10.0'} + dev: true + /@alloc/quick-lru@5.2.0: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -975,6 +983,14 @@ packages: '@jridgewell/gen-mapping': 0.1.1 '@jridgewell/trace-mapping': 0.3.17 + /@ampproject/remapping@2.2.1: + resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.19 + dev: true + /@apideck/better-ajv-errors@0.3.6(ajv@8.11.0): resolution: {integrity: sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==} engines: {node: '>=10'} @@ -1014,6 +1030,14 @@ packages: dependencies: '@babel/highlight': 7.18.6 + /@babel/code-frame@7.22.13: + resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.22.13 + chalk: 2.4.2 + dev: true + /@babel/compat-data@7.19.4: resolution: {integrity: sha512-CHIGpJcUQ5lU9KrPHTjBMhVwQG6CQjxfg36fGXl3qk/Gik1WwWachaXFuo0uCWJT/mStOKtcbFJCaVLihC1CMw==} engines: {node: '>=6.9.0'} @@ -1028,6 +1052,11 @@ packages: engines: {node: '>=6.9.0'} dev: true + /@babel/compat-data@7.22.9: + resolution: {integrity: sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==} + engines: {node: '>=6.9.0'} + dev: true + /@babel/core@7.13.16: resolution: {integrity: sha512-sXHpixBiWWFti0AV2Zq7avpTasr6sIAu7Y396c608541qAU2ui4a193m0KSQmfPSKFZLnQ3cvlKDOm3XkuXm3Q==} engines: {node: '>=6.9.0'} @@ -1096,6 +1125,29 @@ packages: - supports-color dev: true + /@babel/core@7.22.15: + resolution: {integrity: sha512-PtZqMmgRrvj8ruoEOIwVA3yoF91O+Hgw9o7DAUTNBA6Mo2jpu31clx9a7Nz/9JznqetTR6zwfC4L3LAjKQXUwA==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.2.1 + '@babel/code-frame': 7.22.13 + '@babel/generator': 7.22.15 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-module-transforms': 7.22.15(@babel/core@7.22.15) + '@babel/helpers': 7.22.15 + '@babel/parser': 7.22.15 + '@babel/template': 7.22.15 + '@babel/traverse': 7.22.15 + '@babel/types': 7.22.15 + convert-source-map: 1.9.0 + debug: 4.3.4 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + /@babel/eslint-parser@7.19.1(@babel/core@7.18.9)(eslint@7.32.0): resolution: {integrity: sha512-AqNf2QWt1rtu2/1rLswy6CDP7H9Oh3mMhk177Y67Rg8d7RD9WfOLLv8CGn6tisFvS2htm86yIe1yLF6I1UDaGQ==} engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} @@ -1110,7 +1162,7 @@ packages: semver: 6.3.0 dev: true - /@babel/eslint-parser@7.19.1(@babel/core@7.18.9)(eslint@8.42.0): + /@babel/eslint-parser@7.19.1(@babel/core@7.18.9)(eslint@8.48.0): resolution: {integrity: sha512-AqNf2QWt1rtu2/1rLswy6CDP7H9Oh3mMhk177Y67Rg8d7RD9WfOLLv8CGn6tisFvS2htm86yIe1yLF6I1UDaGQ==} engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} peerDependencies: @@ -1119,7 +1171,7 @@ packages: dependencies: '@babel/core': 7.18.9 '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 - eslint: 8.42.0 + eslint: 8.48.0 eslint-visitor-keys: 2.1.0 semver: 6.3.0 dev: true @@ -1142,6 +1194,16 @@ packages: '@jridgewell/trace-mapping': 0.3.17 jsesc: 2.5.2 + /@babel/generator@7.22.15: + resolution: {integrity: sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.22.15 + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.19 + jsesc: 2.5.2 + dev: true + /@babel/generator@7.22.3: resolution: {integrity: sha512-C17MW4wlk//ES/CJDL51kPNwl+qiBQyN7b9SKyVp11BLGFeSPoVaHrv+MNt8jwQFhQWowW88z1eeBx3pFz9v8A==} engines: {node: '>=6.9.0'} @@ -1286,6 +1348,17 @@ packages: semver: 6.3.0 dev: true + /@babel/helper-compilation-targets@7.22.15: + resolution: {integrity: sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/compat-data': 7.22.9 + '@babel/helper-validator-option': 7.22.15 + browserslist: 4.21.10 + lru-cache: 5.1.1 + semver: 6.3.1 + dev: true + /@babel/helper-create-class-features-plugin@7.20.12(@babel/core@7.18.9): resolution: {integrity: sha512-9OunRkbT0JQcednL0UFvbfXpAsUXiGjUk0a7sN8fUXX7Mue79cUSMjHGDRRi/Vz9vYlpIhLV5fMD5dKoMhhsNQ==} engines: {node: '>=6.9.0'} @@ -1324,6 +1397,25 @@ packages: - supports-color dev: true + /@babel/helper-create-class-features-plugin@7.20.12(@babel/core@7.22.15): + resolution: {integrity: sha512-9OunRkbT0JQcednL0UFvbfXpAsUXiGjUk0a7sN8fUXX7Mue79cUSMjHGDRRi/Vz9vYlpIhLV5fMD5dKoMhhsNQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.22.15 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-function-name': 7.19.0 + '@babel/helper-member-expression-to-functions': 7.20.7 + '@babel/helper-optimise-call-expression': 7.18.6 + '@babel/helper-replace-supers': 7.20.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 + '@babel/helper-split-export-declaration': 7.18.6 + transitivePeerDependencies: + - supports-color + dev: true + /@babel/helper-create-class-features-plugin@7.22.1(@babel/core@7.18.9): resolution: {integrity: sha512-SowrZ9BWzYFgzUMwUmowbPSGu6CXL5MSuuCkG3bejahSpSymioPmuLdhPxNOc9MjuNGjy7M/HaXvJ8G82Lywlw==} engines: {node: '>=6.9.0'} @@ -1455,6 +1547,11 @@ packages: resolution: {integrity: sha512-Z2tgopurB/kTbidvzeBrc2To3PUP/9i5MUe+fU6QJCQDyPwSH2oRapkLw3KGECDYSjhQZCNxEvNvZlLw8JjGwA==} engines: {node: '>=6.9.0'} + /@babel/helper-environment-visitor@7.22.5: + resolution: {integrity: sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==} + engines: {node: '>=6.9.0'} + dev: true + /@babel/helper-explode-assignable-expression@7.18.6: resolution: {integrity: sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==} engines: {node: '>=6.9.0'} @@ -1477,12 +1574,27 @@ packages: '@babel/template': 7.21.9 '@babel/types': 7.22.4 + /@babel/helper-function-name@7.22.5: + resolution: {integrity: sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.22.15 + '@babel/types': 7.22.15 + dev: true + /@babel/helper-hoist-variables@7.18.6: resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==} engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.22.4 + /@babel/helper-hoist-variables@7.22.5: + resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.22.15 + dev: true + /@babel/helper-member-expression-to-functions@7.18.9: resolution: {integrity: sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg==} engines: {node: '>=6.9.0'} @@ -1517,6 +1629,13 @@ packages: dependencies: '@babel/types': 7.22.4 + /@babel/helper-module-imports@7.22.15: + resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.22.15 + dev: true + /@babel/helper-module-transforms@7.19.6: resolution: {integrity: sha512-fCmcfQo/KYr/VXXDIyd3CBGZ6AFhPFy1TfSEJ+PilGVlQT6jcbqtHAM4C1EciRqMza7/TpOUZliuSH+U6HAhJw==} engines: {node: '>=6.9.0'} @@ -1564,6 +1683,20 @@ packages: - supports-color dev: true + /@babel/helper-module-transforms@7.22.15(@babel/core@7.22.15): + resolution: {integrity: sha512-l1UiX4UyHSFsYt17iQ3Se5pQQZZHa22zyIXURmvkmLCD4t/aU+dvNWHatKac/D9Vm9UES7nvIqHs4jZqKviUmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.22.15 + '@babel/helper-environment-visitor': 7.22.5 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-validator-identifier': 7.22.15 + dev: true + /@babel/helper-optimise-call-expression@7.18.6: resolution: {integrity: sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==} engines: {node: '>=6.9.0'} @@ -1665,6 +1798,13 @@ packages: dependencies: '@babel/types': 7.22.4 + /@babel/helper-simple-access@7.22.5: + resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.22.15 + dev: true + /@babel/helper-skip-transparent-expression-wrappers@7.18.9: resolution: {integrity: sha512-imytd2gHi3cJPsybLRbmFrF7u5BIEuI2cNheyKi3/iOBC63kNn3q8Crn2xVuESli0aM4KYsyEqKyS7lFL8YVtw==} engines: {node: '>=6.9.0'} @@ -1685,6 +1825,13 @@ packages: dependencies: '@babel/types': 7.22.4 + /@babel/helper-split-export-declaration@7.22.6: + resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.22.15 + dev: true + /@babel/helper-string-parser@7.19.4: resolution: {integrity: sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==} engines: {node: '>=6.9.0'} @@ -1694,10 +1841,20 @@ packages: resolution: {integrity: sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==} engines: {node: '>=6.9.0'} + /@babel/helper-string-parser@7.22.5: + resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==} + engines: {node: '>=6.9.0'} + dev: true + /@babel/helper-validator-identifier@7.19.1: resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==} engines: {node: '>=6.9.0'} + /@babel/helper-validator-identifier@7.22.15: + resolution: {integrity: sha512-4E/F9IIEi8WR94324mbDUMo074YTheJmd7eZF5vITTeYchqAi6sYXRLHUVsmkdmY4QjfKTcB2jB7dVP3NaBElQ==} + engines: {node: '>=6.9.0'} + dev: true + /@babel/helper-validator-option@7.18.6: resolution: {integrity: sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==} engines: {node: '>=6.9.0'} @@ -1707,6 +1864,11 @@ packages: resolution: {integrity: sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==} engines: {node: '>=6.9.0'} + /@babel/helper-validator-option@7.22.15: + resolution: {integrity: sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==} + engines: {node: '>=6.9.0'} + dev: true + /@babel/helper-wrap-function@7.19.0: resolution: {integrity: sha512-txX8aN8CZyYGTwcLhlk87KRqncAzhh5TpQamZUa0/u3an36NtDpUP6bQgBCBcLeBs09R/OwQu3OjK0k/HwfNDg==} engines: {node: '>=6.9.0'} @@ -1740,6 +1902,17 @@ packages: transitivePeerDependencies: - supports-color + /@babel/helpers@7.22.15: + resolution: {integrity: sha512-7pAjK0aSdxOwR+CcYAqgWOGy5dcfvzsTIfFTb2odQqW47MDfv14UaJDY6eng8ylM2EaeKXdxaSWESbkmaQHTmw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.22.15 + '@babel/traverse': 7.22.15 + '@babel/types': 7.22.15 + transitivePeerDependencies: + - supports-color + dev: true + /@babel/helpers@7.22.3: resolution: {integrity: sha512-jBJ7jWblbgr7r6wYZHMdIqKc73ycaTcCaWRq4/2LpuPHcx7xMlZvpGQkOYc9HeSjn6rcx15CPlgVcBtZ4WZJ2w==} engines: {node: '>=6.9.0'} @@ -1759,6 +1932,15 @@ packages: chalk: 2.4.2 js-tokens: 4.0.0 + /@babel/highlight@7.22.13: + resolution: {integrity: sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.22.15 + chalk: 2.4.2 + js-tokens: 4.0.0 + dev: true + /@babel/parser@7.19.6: resolution: {integrity: sha512-h1IUp81s2JYJ3mRkdxJgs4UvmSsRvDrx5ICSJbPvtWYv5i1nTBGcBpnog+89rAFMwvvru6E5NUHdBe01UeSzYA==} engines: {node: '>=6.0.0'} @@ -1772,6 +1954,14 @@ packages: dependencies: '@babel/types': 7.21.5 + /@babel/parser@7.22.15: + resolution: {integrity: sha512-RWmQ/sklUN9BvGGpCDgSubhHWfAx24XDTDObup4ffvxaYsptOg2P3KG0j+1eWKLxpkX0j0uHxmpq2Z1SP/VhxA==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.22.15 + dev: true + /@babel/parser@7.22.4: resolution: {integrity: sha512-VLLsx06XkEYqBtE5YGPwfSGwfrjnyPP5oiGty3S8pQLFDFLaS8VwWSIxkTXpcvr5zeYLE6+MBNl2npl/YnfofA==} engines: {node: '>=6.0.0'} @@ -2432,6 +2622,16 @@ packages: '@babel/helper-plugin-utils': 7.21.5 dev: true + /@babel/plugin-syntax-jsx@7.21.4(@babel/core@7.22.15): + resolution: {integrity: sha512-5hewiLct5OKyh6PLKEYaFclcqtIgCb6bmELouxjF6up5q3Sov7rOayW4RwhbaBL0dit8rA80GNfY+UuDp2mBbQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.15 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.18.9): resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} peerDependencies: @@ -2580,23 +2780,23 @@ packages: '@babel/helper-plugin-utils': 7.21.5 dev: true - /@babel/plugin-syntax-typescript@7.20.0(@babel/core@7.18.9): + /@babel/plugin-syntax-typescript@7.20.0(@babel/core@7.22.1): resolution: {integrity: sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.18.9 + '@babel/core': 7.22.1 '@babel/helper-plugin-utils': 7.21.5 dev: true - /@babel/plugin-syntax-typescript@7.20.0(@babel/core@7.22.1): + /@babel/plugin-syntax-typescript@7.20.0(@babel/core@7.22.15): resolution: {integrity: sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.22.1 + '@babel/core': 7.22.15 '@babel/helper-plugin-utils': 7.21.5 dev: true @@ -3520,6 +3720,16 @@ packages: '@babel/helper-plugin-utils': 7.21.5 dev: true + /@babel/plugin-transform-react-display-name@7.18.6(@babel/core@7.22.15): + resolution: {integrity: sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.15 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + /@babel/plugin-transform-react-jsx-development@7.18.6(@babel/core@7.18.9): resolution: {integrity: sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==} engines: {node: '>=6.9.0'} @@ -3530,6 +3740,16 @@ packages: '@babel/plugin-transform-react-jsx': 7.22.3(@babel/core@7.18.9) dev: true + /@babel/plugin-transform-react-jsx-development@7.18.6(@babel/core@7.22.15): + resolution: {integrity: sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.15 + '@babel/plugin-transform-react-jsx': 7.22.3(@babel/core@7.22.15) + dev: true + /@babel/plugin-transform-react-jsx@7.19.0(@babel/core@7.22.1): resolution: {integrity: sha512-UVEvX3tXie3Szm3emi1+G63jyw1w5IcMY0FSKM+CRnKRI5Mr1YbCNgsSTwoTwKphQEG9P+QqmuRFneJPZuHNhg==} engines: {node: '>=6.9.0'} @@ -3558,6 +3778,20 @@ packages: '@babel/types': 7.22.4 dev: true + /@babel/plugin-transform-react-jsx@7.22.3(@babel/core@7.22.15): + resolution: {integrity: sha512-JEulRWG2f04a7L8VWaOngWiK6p+JOSpB+DAtwfJgOaej1qdbNxqtK7MwTBHjUA10NeFcszlFNqCdbRcirzh2uQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.15 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-module-imports': 7.21.4 + '@babel/helper-plugin-utils': 7.21.5 + '@babel/plugin-syntax-jsx': 7.21.4(@babel/core@7.22.15) + '@babel/types': 7.22.4 + dev: true + /@babel/plugin-transform-react-pure-annotations@7.18.6(@babel/core@7.18.9): resolution: {integrity: sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ==} engines: {node: '>=6.9.0'} @@ -3569,6 +3803,17 @@ packages: '@babel/helper-plugin-utils': 7.21.5 dev: true + /@babel/plugin-transform-react-pure-annotations@7.18.6(@babel/core@7.22.15): + resolution: {integrity: sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.15 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + /@babel/plugin-transform-regenerator@7.18.6(@babel/core@7.18.9): resolution: {integrity: sha512-poqRI2+qiSdeldcz4wTSTXBRryoq3Gc70ye7m7UD5Ww0nE29IXqMl6r7Nd15WBgRd74vloEMlShtH6CKxVzfmQ==} engines: {node: '>=6.9.0'} @@ -3769,30 +4014,30 @@ packages: '@babel/helper-plugin-utils': 7.21.5 dev: true - /@babel/plugin-transform-typescript@7.20.13(@babel/core@7.18.9): + /@babel/plugin-transform-typescript@7.20.13(@babel/core@7.22.1): resolution: {integrity: sha512-O7I/THxarGcDZxkgWKMUrk7NK1/WbHAg3Xx86gqS6x9MTrNL6AwIluuZ96ms4xeDe6AVx6rjHbWHP7x26EPQBA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.18.9 - '@babel/helper-create-class-features-plugin': 7.20.12(@babel/core@7.18.9) + '@babel/core': 7.22.1 + '@babel/helper-create-class-features-plugin': 7.20.12(@babel/core@7.22.1) '@babel/helper-plugin-utils': 7.21.5 - '@babel/plugin-syntax-typescript': 7.20.0(@babel/core@7.18.9) + '@babel/plugin-syntax-typescript': 7.20.0(@babel/core@7.22.1) transitivePeerDependencies: - supports-color dev: true - /@babel/plugin-transform-typescript@7.20.13(@babel/core@7.22.1): + /@babel/plugin-transform-typescript@7.20.13(@babel/core@7.22.15): resolution: {integrity: sha512-O7I/THxarGcDZxkgWKMUrk7NK1/WbHAg3Xx86gqS6x9MTrNL6AwIluuZ96ms4xeDe6AVx6rjHbWHP7x26EPQBA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.22.1 - '@babel/helper-create-class-features-plugin': 7.20.12(@babel/core@7.22.1) + '@babel/core': 7.22.15 + '@babel/helper-create-class-features-plugin': 7.20.12(@babel/core@7.22.15) '@babel/helper-plugin-utils': 7.21.5 - '@babel/plugin-syntax-typescript': 7.20.0(@babel/core@7.22.1) + '@babel/plugin-syntax-typescript': 7.20.0(@babel/core@7.22.15) transitivePeerDependencies: - supports-color dev: true @@ -4190,30 +4435,45 @@ packages: '@babel/plugin-transform-react-pure-annotations': 7.18.6(@babel/core@7.18.9) dev: true - /@babel/preset-typescript@7.18.6(@babel/core@7.18.9): + /@babel/preset-react@7.22.3(@babel/core@7.22.15): + resolution: {integrity: sha512-lxDz1mnZ9polqClBCVBjIVUypoB4qV3/tZUDb/IlYbW1kiiLaXaX+bInbRjl+lNQ/iUZraQ3+S8daEmoELMWug==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.15 + '@babel/helper-plugin-utils': 7.21.5 + '@babel/helper-validator-option': 7.21.0 + '@babel/plugin-transform-react-display-name': 7.18.6(@babel/core@7.22.15) + '@babel/plugin-transform-react-jsx': 7.22.3(@babel/core@7.22.15) + '@babel/plugin-transform-react-jsx-development': 7.18.6(@babel/core@7.22.15) + '@babel/plugin-transform-react-pure-annotations': 7.18.6(@babel/core@7.22.15) + dev: true + + /@babel/preset-typescript@7.18.6(@babel/core@7.22.1): resolution: {integrity: sha512-s9ik86kXBAnD760aybBucdpnLsAt0jK1xqJn2juOn9lkOvSHV60os5hxoVJsPzMQxvnUJFAlkont2DvvaYEBtQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.18.9 + '@babel/core': 7.22.1 '@babel/helper-plugin-utils': 7.19.0 '@babel/helper-validator-option': 7.18.6 - '@babel/plugin-transform-typescript': 7.20.13(@babel/core@7.18.9) + '@babel/plugin-transform-typescript': 7.20.13(@babel/core@7.22.1) transitivePeerDependencies: - supports-color dev: true - /@babel/preset-typescript@7.18.6(@babel/core@7.22.1): + /@babel/preset-typescript@7.18.6(@babel/core@7.22.15): resolution: {integrity: sha512-s9ik86kXBAnD760aybBucdpnLsAt0jK1xqJn2juOn9lkOvSHV60os5hxoVJsPzMQxvnUJFAlkont2DvvaYEBtQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.22.1 + '@babel/core': 7.22.15 '@babel/helper-plugin-utils': 7.19.0 '@babel/helper-validator-option': 7.18.6 - '@babel/plugin-transform-typescript': 7.20.13(@babel/core@7.22.1) + '@babel/plugin-transform-typescript': 7.20.13(@babel/core@7.22.15) transitivePeerDependencies: - supports-color dev: true @@ -4284,6 +4544,15 @@ packages: '@babel/parser': 7.22.4 '@babel/types': 7.22.4 + /@babel/template@7.22.15: + resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.22.13 + '@babel/parser': 7.22.15 + '@babel/types': 7.22.15 + dev: true + /@babel/traverse@7.19.6: resolution: {integrity: sha512-6l5HrUCzFM04mfbG09AagtYyR2P0B71B1wN7PfSPiksDPz2k5H9CBC1tcZpz2M8OxbKTPccByoOJ22rUKbpmQQ==} engines: {node: '>=6.9.0'} @@ -4319,6 +4588,24 @@ packages: transitivePeerDependencies: - supports-color + /@babel/traverse@7.22.15: + resolution: {integrity: sha512-DdHPwvJY0sEeN4xJU5uRLmZjgMMDIvMPniLuYzUVXj/GGzysPl0/fwt44JBkyUIzGJPV8QgHMcQdQ34XFuKTYQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.22.13 + '@babel/generator': 7.22.15 + '@babel/helper-environment-visitor': 7.22.5 + '@babel/helper-function-name': 7.22.5 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/parser': 7.22.15 + '@babel/types': 7.22.15 + debug: 4.3.4 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + /@babel/traverse@7.22.4: resolution: {integrity: sha512-Tn1pDsjIcI+JcLKq1AVlZEr4226gpuAQTsLMorsYg9tuS/kG7nuwwJ4AB8jfQuEgb/COBwR/DqJxmoiYFu5/rQ==} engines: {node: '>=6.9.0'} @@ -4353,6 +4640,15 @@ packages: '@babel/helper-validator-identifier': 7.19.1 to-fast-properties: 2.0.0 + /@babel/types@7.22.15: + resolution: {integrity: sha512-X+NLXr0N8XXmN5ZsaQdm9U2SSC3UbIYq/doL++sueHOTisgZHoKaQtZxGuV2cUPQHMfjKEfg/g6oy7Hm6SKFtA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.22.5 + '@babel/helper-validator-identifier': 7.22.15 + to-fast-properties: 2.0.0 + dev: true + /@babel/types@7.22.4: resolution: {integrity: sha512-Tx9x3UBHTTsMSW85WB2kphxYQVvrZ/t1FxD88IpSgIjiUJlCm9z+xWIDwyo1vffTwSqteqyznB8ZE9vYYk16zA==} engines: {node: '>=6.9.0'} @@ -4792,18 +5088,18 @@ packages: dev: true optional: true - /@eslint-community/eslint-utils@4.4.0(eslint@8.42.0): + /@eslint-community/eslint-utils@4.4.0(eslint@8.48.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 dependencies: - eslint: 8.42.0 - eslint-visitor-keys: 3.4.1 + eslint: 8.48.0 + eslint-visitor-keys: 3.4.3 dev: true - /@eslint-community/regexpp@4.5.1: - resolution: {integrity: sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==} + /@eslint-community/regexpp@4.8.0: + resolution: {integrity: sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} dev: true @@ -4841,14 +5137,14 @@ packages: - supports-color dev: true - /@eslint/eslintrc@2.0.3: - resolution: {integrity: sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==} + /@eslint/eslintrc@2.1.2: + resolution: {integrity: sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 debug: 4.3.4 - espree: 9.5.2 - globals: 13.20.0 + espree: 9.6.1 + globals: 13.21.0 ignore: 5.2.4 import-fresh: 3.3.0 js-yaml: 4.1.0 @@ -4858,8 +5154,8 @@ packages: - supports-color dev: true - /@eslint/js@8.42.0: - resolution: {integrity: sha512-6SWlXpWU5AvId8Ac7zjzmIOqMOba/JWY8XZ4A7q7Gn1Vlfg/SFFIlrtHXt9nPn4op9ZPAkl91Jao+QQv3r/ukw==} + /@eslint/js@8.48.0: + resolution: {integrity: sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true @@ -4887,8 +5183,8 @@ packages: react: 18.2.0 dev: false - /@humanwhocodes/config-array@0.11.10: - resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==} + /@humanwhocodes/config-array@0.11.11: + resolution: {integrity: sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==} engines: {node: '>=10.10.0'} dependencies: '@humanwhocodes/object-schema': 1.2.1 @@ -4978,13 +5274,18 @@ packages: dependencies: '@jridgewell/set-array': 1.1.2 '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.18 + '@jridgewell/trace-mapping': 0.3.19 dev: true /@jridgewell/resolve-uri@3.1.0: resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} engines: {node: '>=6.0.0'} + /@jridgewell/resolve-uri@3.1.1: + resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} + engines: {node: '>=6.0.0'} + dev: true + /@jridgewell/set-array@1.1.2: resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} engines: {node: '>=6.0.0'} @@ -4996,11 +5297,11 @@ packages: '@jridgewell/trace-mapping': 0.3.17 dev: true - /@jridgewell/source-map@0.3.3: - resolution: {integrity: sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==} + /@jridgewell/source-map@0.3.5: + resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==} dependencies: '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.18 + '@jridgewell/trace-mapping': 0.3.19 dev: true /@jridgewell/sourcemap-codec@1.4.14: @@ -5016,11 +5317,11 @@ packages: '@jridgewell/resolve-uri': 3.1.0 '@jridgewell/sourcemap-codec': 1.4.14 - /@jridgewell/trace-mapping@0.3.18: - resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==} + /@jridgewell/trace-mapping@0.3.19: + resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==} dependencies: - '@jridgewell/resolve-uri': 3.1.0 - '@jridgewell/sourcemap-codec': 1.4.14 + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 dev: true /@jridgewell/trace-mapping@0.3.9: @@ -5731,6 +6032,10 @@ packages: resolution: {integrity: sha512-JIzsAvJeA/5iY6Y/OxZbv1lUcc8dNSE77lb2gnBH+/PJ3lFR1Ccvgwl5JWnHAkNHcRsT0TbpVOsiMKZ1F/yyJg==} dev: true + /@types/node@20.5.9: + resolution: {integrity: sha512-PcGNd//40kHAS3sTlzKB9C9XL4K0sTup8nbG5lC14kzEteTNuAFh9u5nA0o5TWnSG2r/JNPRXFVcHJIIeRlmqQ==} + dev: true + /@types/parse-json@4.0.0: resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} dev: true @@ -5833,7 +6138,7 @@ packages: '@types/node': 18.11.17 dev: true - /@typescript-eslint/eslint-plugin@4.33.0(@typescript-eslint/parser@4.33.0)(eslint@7.32.0)(typescript@5.1.3): + /@typescript-eslint/eslint-plugin@4.33.0(@typescript-eslint/parser@4.33.0)(eslint@7.32.0)(typescript@5.2.2): resolution: {integrity: sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg==} engines: {node: ^10.12.0 || >=12.0.0} peerDependencies: @@ -5844,8 +6149,8 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/experimental-utils': 4.33.0(eslint@7.32.0)(typescript@5.1.3) - '@typescript-eslint/parser': 4.33.0(eslint@7.32.0)(typescript@5.1.3) + '@typescript-eslint/experimental-utils': 4.33.0(eslint@7.32.0)(typescript@5.2.2) + '@typescript-eslint/parser': 4.33.0(eslint@7.32.0)(typescript@5.2.2) '@typescript-eslint/scope-manager': 4.33.0 debug: 4.3.4 eslint: 7.32.0 @@ -5853,13 +6158,13 @@ packages: ignore: 5.2.0 regexpp: 3.2.0 semver: 7.3.8 - tsutils: 3.21.0(typescript@5.1.3) - typescript: 5.1.3 + tsutils: 3.21.0(typescript@5.2.2) + typescript: 5.2.2 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/eslint-plugin@5.41.0(@typescript-eslint/parser@5.41.0)(eslint@8.26.0)(typescript@5.1.3): + /@typescript-eslint/eslint-plugin@5.41.0(@typescript-eslint/parser@5.41.0)(eslint@8.26.0)(typescript@5.2.2): resolution: {integrity: sha512-DXUS22Y57/LAFSg3x7Vi6RNAuLpTXwxB9S2nIA7msBb/Zt8p7XqMwdpdc1IU7CkOQUPgAqR5fWvxuKCbneKGmA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -5870,22 +6175,22 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/parser': 5.41.0(eslint@8.26.0)(typescript@5.1.3) + '@typescript-eslint/parser': 5.41.0(eslint@8.26.0)(typescript@5.2.2) '@typescript-eslint/scope-manager': 5.41.0 - '@typescript-eslint/type-utils': 5.41.0(eslint@8.26.0)(typescript@5.1.3) - '@typescript-eslint/utils': 5.41.0(eslint@8.26.0)(typescript@5.1.3) + '@typescript-eslint/type-utils': 5.41.0(eslint@8.26.0)(typescript@5.2.2) + '@typescript-eslint/utils': 5.41.0(eslint@8.26.0)(typescript@5.2.2) debug: 4.3.4 eslint: 8.26.0 ignore: 5.2.0 regexpp: 3.2.0 semver: 7.3.8 - tsutils: 3.21.0(typescript@5.1.3) - typescript: 5.1.3 + tsutils: 3.21.0(typescript@5.2.2) + typescript: 5.2.2 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/eslint-plugin@5.41.0(@typescript-eslint/parser@5.41.0)(eslint@8.42.0)(typescript@5.1.3): + /@typescript-eslint/eslint-plugin@5.41.0(@typescript-eslint/parser@5.41.0)(eslint@8.48.0)(typescript@5.2.2): resolution: {integrity: sha512-DXUS22Y57/LAFSg3x7Vi6RNAuLpTXwxB9S2nIA7msBb/Zt8p7XqMwdpdc1IU7CkOQUPgAqR5fWvxuKCbneKGmA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -5896,22 +6201,22 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/parser': 5.41.0(eslint@8.42.0)(typescript@5.1.3) + '@typescript-eslint/parser': 5.41.0(eslint@8.48.0)(typescript@5.2.2) '@typescript-eslint/scope-manager': 5.41.0 - '@typescript-eslint/type-utils': 5.41.0(eslint@8.42.0)(typescript@5.1.3) - '@typescript-eslint/utils': 5.41.0(eslint@8.42.0)(typescript@5.1.3) + '@typescript-eslint/type-utils': 5.41.0(eslint@8.48.0)(typescript@5.2.2) + '@typescript-eslint/utils': 5.41.0(eslint@8.48.0)(typescript@5.2.2) debug: 4.3.4 - eslint: 8.42.0 + eslint: 8.48.0 ignore: 5.2.0 regexpp: 3.2.0 semver: 7.3.8 - tsutils: 3.21.0(typescript@5.1.3) - typescript: 5.1.3 + tsutils: 3.21.0(typescript@5.2.2) + typescript: 5.2.2 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/experimental-utils@4.33.0(eslint@7.32.0)(typescript@5.1.3): + /@typescript-eslint/experimental-utils@4.33.0(eslint@7.32.0)(typescript@5.2.2): resolution: {integrity: sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==} engines: {node: ^10.12.0 || >=12.0.0} peerDependencies: @@ -5920,7 +6225,7 @@ packages: '@types/json-schema': 7.0.11 '@typescript-eslint/scope-manager': 4.33.0 '@typescript-eslint/types': 4.33.0 - '@typescript-eslint/typescript-estree': 4.33.0(typescript@5.1.3) + '@typescript-eslint/typescript-estree': 4.33.0(typescript@5.2.2) eslint: 7.32.0 eslint-scope: 5.1.1 eslint-utils: 3.0.0(eslint@7.32.0) @@ -5929,33 +6234,33 @@ packages: - typescript dev: true - /@typescript-eslint/experimental-utils@5.41.0(eslint@7.32.0)(typescript@5.1.3): + /@typescript-eslint/experimental-utils@5.41.0(eslint@7.32.0)(typescript@5.2.2): resolution: {integrity: sha512-/qxT2Kd2q/A22JVIllvws4rvc00/3AT4rAo/0YgEN28y+HPhbJbk6X4+MAHEoZzpNyAOugIT7D/OLnKBW8FfhA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - '@typescript-eslint/utils': 5.41.0(eslint@7.32.0)(typescript@5.1.3) + '@typescript-eslint/utils': 5.41.0(eslint@7.32.0)(typescript@5.2.2) eslint: 7.32.0 transitivePeerDependencies: - supports-color - typescript dev: true - /@typescript-eslint/experimental-utils@5.41.0(eslint@8.42.0)(typescript@5.1.3): + /@typescript-eslint/experimental-utils@5.41.0(eslint@8.48.0)(typescript@5.2.2): resolution: {integrity: sha512-/qxT2Kd2q/A22JVIllvws4rvc00/3AT4rAo/0YgEN28y+HPhbJbk6X4+MAHEoZzpNyAOugIT7D/OLnKBW8FfhA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - '@typescript-eslint/utils': 5.41.0(eslint@8.42.0)(typescript@5.1.3) - eslint: 8.42.0 + '@typescript-eslint/utils': 5.41.0(eslint@8.48.0)(typescript@5.2.2) + eslint: 8.48.0 transitivePeerDependencies: - supports-color - typescript dev: true - /@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.1.3): + /@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.2.2): resolution: {integrity: sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==} engines: {node: ^10.12.0 || >=12.0.0} peerDependencies: @@ -5967,15 +6272,15 @@ packages: dependencies: '@typescript-eslint/scope-manager': 4.33.0 '@typescript-eslint/types': 4.33.0 - '@typescript-eslint/typescript-estree': 4.33.0(typescript@5.1.3) + '@typescript-eslint/typescript-estree': 4.33.0(typescript@5.2.2) debug: 4.3.4 eslint: 7.32.0 - typescript: 5.1.3 + typescript: 5.2.2 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/parser@5.41.0(eslint@8.26.0)(typescript@5.1.3): + /@typescript-eslint/parser@5.41.0(eslint@8.26.0)(typescript@5.2.2): resolution: {integrity: sha512-HQVfix4+RL5YRWZboMD1pUfFN8MpRH4laziWkkAzyO1fvNOY/uinZcvo3QiFJVS/siNHupV8E5+xSwQZrl6PZA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -5987,15 +6292,15 @@ packages: dependencies: '@typescript-eslint/scope-manager': 5.41.0 '@typescript-eslint/types': 5.41.0 - '@typescript-eslint/typescript-estree': 5.41.0(typescript@5.1.3) + '@typescript-eslint/typescript-estree': 5.41.0(typescript@5.2.2) debug: 4.3.4 eslint: 8.26.0 - typescript: 5.1.3 + typescript: 5.2.2 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/parser@5.41.0(eslint@8.42.0)(typescript@5.1.3): + /@typescript-eslint/parser@5.41.0(eslint@8.48.0)(typescript@5.2.2): resolution: {integrity: sha512-HQVfix4+RL5YRWZboMD1pUfFN8MpRH4laziWkkAzyO1fvNOY/uinZcvo3QiFJVS/siNHupV8E5+xSwQZrl6PZA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -6007,10 +6312,10 @@ packages: dependencies: '@typescript-eslint/scope-manager': 5.41.0 '@typescript-eslint/types': 5.41.0 - '@typescript-eslint/typescript-estree': 5.41.0(typescript@5.1.3) + '@typescript-eslint/typescript-estree': 5.41.0(typescript@5.2.2) debug: 4.3.4 - eslint: 8.42.0 - typescript: 5.1.3 + eslint: 8.48.0 + typescript: 5.2.2 transitivePeerDependencies: - supports-color dev: true @@ -6031,7 +6336,7 @@ packages: '@typescript-eslint/visitor-keys': 5.41.0 dev: true - /@typescript-eslint/type-utils@5.41.0(eslint@8.26.0)(typescript@5.1.3): + /@typescript-eslint/type-utils@5.41.0(eslint@8.26.0)(typescript@5.2.2): resolution: {integrity: sha512-L30HNvIG6A1Q0R58e4hu4h+fZqaO909UcnnPbwKiN6Rc3BUEx6ez2wgN7aC0cBfcAjZfwkzE+E2PQQ9nEuoqfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -6041,17 +6346,17 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 5.41.0(typescript@5.1.3) - '@typescript-eslint/utils': 5.41.0(eslint@8.26.0)(typescript@5.1.3) + '@typescript-eslint/typescript-estree': 5.41.0(typescript@5.2.2) + '@typescript-eslint/utils': 5.41.0(eslint@8.26.0)(typescript@5.2.2) debug: 4.3.4 eslint: 8.26.0 - tsutils: 3.21.0(typescript@5.1.3) - typescript: 5.1.3 + tsutils: 3.21.0(typescript@5.2.2) + typescript: 5.2.2 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/type-utils@5.41.0(eslint@8.42.0)(typescript@5.1.3): + /@typescript-eslint/type-utils@5.41.0(eslint@8.48.0)(typescript@5.2.2): resolution: {integrity: sha512-L30HNvIG6A1Q0R58e4hu4h+fZqaO909UcnnPbwKiN6Rc3BUEx6ez2wgN7aC0cBfcAjZfwkzE+E2PQQ9nEuoqfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -6061,12 +6366,12 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 5.41.0(typescript@5.1.3) - '@typescript-eslint/utils': 5.41.0(eslint@8.42.0)(typescript@5.1.3) + '@typescript-eslint/typescript-estree': 5.41.0(typescript@5.2.2) + '@typescript-eslint/utils': 5.41.0(eslint@8.48.0)(typescript@5.2.2) debug: 4.3.4 - eslint: 8.42.0 - tsutils: 3.21.0(typescript@5.1.3) - typescript: 5.1.3 + eslint: 8.48.0 + tsutils: 3.21.0(typescript@5.2.2) + typescript: 5.2.2 transitivePeerDependencies: - supports-color dev: true @@ -6081,7 +6386,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /@typescript-eslint/typescript-estree@4.33.0(typescript@5.1.3): + /@typescript-eslint/typescript-estree@4.33.0(typescript@5.2.2): resolution: {integrity: sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==} engines: {node: ^10.12.0 || >=12.0.0} peerDependencies: @@ -6096,13 +6401,13 @@ packages: globby: 11.1.0 is-glob: 4.0.3 semver: 7.3.8 - tsutils: 3.21.0(typescript@5.1.3) - typescript: 5.1.3 + tsutils: 3.21.0(typescript@5.2.2) + typescript: 5.2.2 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/typescript-estree@5.41.0(typescript@5.1.3): + /@typescript-eslint/typescript-estree@5.41.0(typescript@5.2.2): resolution: {integrity: sha512-SlzFYRwFSvswzDSQ/zPkIWcHv8O5y42YUskko9c4ki+fV6HATsTODUPbRbcGDFYP86gaJL5xohUEytvyNNcXWg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -6117,13 +6422,13 @@ packages: globby: 11.1.0 is-glob: 4.0.3 semver: 7.3.8 - tsutils: 3.21.0(typescript@5.1.3) - typescript: 5.1.3 + tsutils: 3.21.0(typescript@5.2.2) + typescript: 5.2.2 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/utils@5.41.0(eslint@7.32.0)(typescript@5.1.3): + /@typescript-eslint/utils@5.41.0(eslint@7.32.0)(typescript@5.2.2): resolution: {integrity: sha512-QlvfwaN9jaMga9EBazQ+5DDx/4sAdqDkcs05AsQHMaopluVCUyu1bTRUVKzXbgjDlrRAQrYVoi/sXJ9fmG+KLQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -6133,7 +6438,7 @@ packages: '@types/semver': 7.3.12 '@typescript-eslint/scope-manager': 5.41.0 '@typescript-eslint/types': 5.41.0 - '@typescript-eslint/typescript-estree': 5.41.0(typescript@5.1.3) + '@typescript-eslint/typescript-estree': 5.41.0(typescript@5.2.2) eslint: 7.32.0 eslint-scope: 5.1.1 eslint-utils: 3.0.0(eslint@7.32.0) @@ -6143,7 +6448,7 @@ packages: - typescript dev: true - /@typescript-eslint/utils@5.41.0(eslint@8.26.0)(typescript@5.1.3): + /@typescript-eslint/utils@5.41.0(eslint@8.26.0)(typescript@5.2.2): resolution: {integrity: sha512-QlvfwaN9jaMga9EBazQ+5DDx/4sAdqDkcs05AsQHMaopluVCUyu1bTRUVKzXbgjDlrRAQrYVoi/sXJ9fmG+KLQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -6153,7 +6458,7 @@ packages: '@types/semver': 7.3.12 '@typescript-eslint/scope-manager': 5.41.0 '@typescript-eslint/types': 5.41.0 - '@typescript-eslint/typescript-estree': 5.41.0(typescript@5.1.3) + '@typescript-eslint/typescript-estree': 5.41.0(typescript@5.2.2) eslint: 8.26.0 eslint-scope: 5.1.1 eslint-utils: 3.0.0(eslint@8.26.0) @@ -6163,7 +6468,7 @@ packages: - typescript dev: true - /@typescript-eslint/utils@5.41.0(eslint@8.42.0)(typescript@5.1.3): + /@typescript-eslint/utils@5.41.0(eslint@8.48.0)(typescript@5.2.2): resolution: {integrity: sha512-QlvfwaN9jaMga9EBazQ+5DDx/4sAdqDkcs05AsQHMaopluVCUyu1bTRUVKzXbgjDlrRAQrYVoi/sXJ9fmG+KLQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -6173,10 +6478,10 @@ packages: '@types/semver': 7.3.12 '@typescript-eslint/scope-manager': 5.41.0 '@typescript-eslint/types': 5.41.0 - '@typescript-eslint/typescript-estree': 5.41.0(typescript@5.1.3) - eslint: 8.42.0 + '@typescript-eslint/typescript-estree': 5.41.0(typescript@5.2.2) + eslint: 8.48.0 eslint-scope: 5.1.1 - eslint-utils: 3.0.0(eslint@8.42.0) + eslint-utils: 3.0.0(eslint@8.48.0) semver: 7.3.8 transitivePeerDependencies: - supports-color @@ -6383,20 +6688,20 @@ packages: acorn: 7.4.1 dev: true - /acorn-jsx@5.3.2(acorn@8.8.1): + /acorn-jsx@5.3.2(acorn@8.10.0): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - acorn: 8.8.1 + acorn: 8.10.0 dev: true - /acorn-jsx@5.3.2(acorn@8.8.2): + /acorn-jsx@5.3.2(acorn@8.8.1): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - acorn: 8.8.2 + acorn: 8.8.1 dev: true /acorn-walk@6.2.0: @@ -6420,6 +6725,12 @@ packages: engines: {node: '>=0.4.0'} dev: true + /acorn@8.10.0: + resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + /acorn@8.8.1: resolution: {integrity: sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==} engines: {node: '>=0.4.0'} @@ -6546,8 +6857,8 @@ packages: engines: {node: '>=12'} dev: true - /ansi-sequence-parser@1.1.0: - resolution: {integrity: sha512-lEm8mt52to2fT8GhciPCGeCXACSz2UwIN4X2e2LJSnZ5uAbn2/dsYdOmUXq0AtWS5cpAupysIneExOgH0Vd2TQ==} + /ansi-sequence-parser@1.1.1: + resolution: {integrity: sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==} dev: true /ansi-styles@1.0.0: @@ -6779,8 +7090,8 @@ packages: engines: {node: '>=8'} dev: true - /async-each@1.0.3: - resolution: {integrity: sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==} + /async-each@1.0.6: + resolution: {integrity: sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==} requiresBuild: true dev: true optional: true @@ -6805,6 +7116,7 @@ packages: /atob@2.1.2: resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} engines: {node: '>= 4.5.0'} + hasBin: true dev: true /autoprefixer@10.4.14(postcss@8.4.23): @@ -7187,8 +7499,8 @@ packages: tweetnacl: 0.14.5 dev: true - /better-sqlite3@8.4.0: - resolution: {integrity: sha512-NmsNW1CQvqMszu/CFAJ3pLct6NEFlNfuGM6vw72KHkjOD1UDnL96XNN1BMQc1hiHo8vE2GbOWQYIpZ+YM5wrZw==} + /better-sqlite3@8.6.0: + resolution: {integrity: sha512-jwAudeiTMTSyby+/SfbHDebShbmC2MCH8mU2+DXi0WJfv13ypEJm47cd3kljmy/H130CazEvkf2Li//ewcMJ1g==} requiresBuild: true dependencies: bindings: 1.5.0 @@ -7368,7 +7680,7 @@ packages: resolution: {integrity: sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==} dependencies: cipher-base: 1.0.4 - des.js: 1.0.1 + des.js: 1.1.0 inherits: 2.0.4 safe-buffer: 5.2.1 dev: true @@ -7390,7 +7702,7 @@ packages: elliptic: 6.5.4 inherits: 2.0.4 parse-asn1: 5.1.6 - readable-stream: 3.6.0 + readable-stream: 3.6.2 safe-buffer: 5.2.1 dev: true @@ -7400,6 +7712,17 @@ packages: pako: 1.0.11 dev: true + /browserslist@4.21.10: + resolution: {integrity: sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001527 + electron-to-chromium: 1.4.508 + node-releases: 2.0.13 + update-browserslist-db: 1.0.11(browserslist@4.21.10) + dev: true + /browserslist@4.21.4: resolution: {integrity: sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -7529,7 +7852,7 @@ packages: chownr: 1.1.4 figgy-pudding: 3.5.2 glob: 7.2.3 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 infer-owner: 1.0.4 lru-cache: 5.1.1 mississippi: 3.0.0 @@ -7653,7 +7976,7 @@ packages: resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} dependencies: pascal-case: 3.1.2 - tslib: 2.6.1 + tslib: 2.6.2 dev: true /camelcase-css@2.0.1: @@ -7682,6 +8005,10 @@ packages: /caniuse-lite@1.0.30001482: resolution: {integrity: sha512-F1ZInsg53cegyjroxLNW9DmrEQ1SuGRTO1QlpA0o2/6OpQ0gFeDRoq1yFmnr8Sakn9qwwt9DmbxHB6w167OSuQ==} + /caniuse-lite@1.0.30001527: + resolution: {integrity: sha512-YkJi7RwPgWtXVSgK4lG9AHH57nSzvvOp9MesgXmw4Q7n0C3H04L0foHqfxcmSAm5AcWb8dW9AYj2tR7/5GnddQ==} + dev: true + /caseless@0.12.0: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} dev: true @@ -7776,7 +8103,7 @@ packages: requiresBuild: true dependencies: anymatch: 2.0.0 - async-each: 1.0.3 + async-each: 1.0.6 braces: 2.3.2 glob-parent: 3.1.0 inherits: 2.0.4 @@ -7805,7 +8132,7 @@ packages: normalize-path: 3.0.0 readdirp: 3.6.0 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 /chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} @@ -8134,7 +8461,7 @@ packages: dependencies: buffer-from: 1.1.2 inherits: 2.0.4 - readable-stream: 2.3.7 + readable-stream: 2.3.8 typedarray: 0.0.6 dev: true @@ -8667,8 +8994,8 @@ packages: array-find-index: 1.0.2 dev: true - /cyclist@1.0.1: - resolution: {integrity: sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==} + /cyclist@1.0.2: + resolution: {integrity: sha512-0sVXIohTfLqVIW3kb/0n6IiWF3Ifj5nm2XaSrLq2DI6fKIGa2fYAZdk917rUneaeLVpYfFcyXE2ft0fe3remsA==} dev: true /damerau-levenshtein@1.0.8: @@ -8763,8 +9090,8 @@ packages: engines: {node: '>=10'} dev: true - /decode-uri-component@0.2.0: - resolution: {integrity: sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==} + /decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} engines: {node: '>=0.10'} dev: true @@ -8909,8 +9236,8 @@ packages: engines: {node: '>= 0.6.0'} dev: true - /des.js@1.0.1: - resolution: {integrity: sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==} + /des.js@1.1.0: + resolution: {integrity: sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==} dependencies: inherits: 2.0.4 minimalistic-assert: 1.0.1 @@ -9054,7 +9381,7 @@ packages: resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} dependencies: no-case: 3.0.4 - tslib: 2.6.1 + tslib: 2.6.2 dev: true /dot-prop@5.3.0: @@ -9092,7 +9419,7 @@ packages: dependencies: end-of-stream: 1.4.4 inherits: 2.0.4 - readable-stream: 2.3.7 + readable-stream: 2.3.8 stream-shift: 1.0.1 dev: true @@ -9128,6 +9455,10 @@ packages: /electron-to-chromium@1.4.284: resolution: {integrity: sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==} + /electron-to-chromium@1.4.508: + resolution: {integrity: sha512-FFa8QKjQK/A5QuFr2167myhMesGrhlOBD+3cYNxO9/S4XzHEXesyTD/1/xF644gC8buFPz3ca6G1LOQD0tZrrg==} + dev: true + /elliptic@6.5.4: resolution: {integrity: sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==} dependencies: @@ -9188,7 +9519,7 @@ packages: resolution: {integrity: sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==} engines: {node: '>=6.9.0'} dependencies: - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 memory-fs: 0.5.0 tapable: 1.1.3 dev: true @@ -9219,6 +9550,7 @@ packages: /errno@0.1.8: resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==} + hasBin: true dependencies: prr: 1.0.1 dev: true @@ -9412,14 +9744,14 @@ packages: eslint: ^7.32.0 || ^8.2.0 eslint-plugin-import: ^2.25.3 dependencies: - '@typescript-eslint/eslint-plugin': 5.41.0(@typescript-eslint/parser@5.41.0)(eslint@8.26.0)(typescript@5.1.3) - '@typescript-eslint/parser': 5.41.0(eslint@8.26.0)(typescript@5.1.3) + '@typescript-eslint/eslint-plugin': 5.41.0(@typescript-eslint/parser@5.41.0)(eslint@8.26.0)(typescript@5.2.2) + '@typescript-eslint/parser': 5.41.0(eslint@8.26.0)(typescript@5.2.2) eslint: 8.26.0 eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.26.0)(eslint@8.26.0) eslint-plugin-import: 2.26.0(@typescript-eslint/parser@5.41.0)(eslint@8.26.0) dev: true - /eslint-config-preact@1.3.0(@typescript-eslint/eslint-plugin@4.33.0)(eslint@7.32.0)(typescript@5.1.3): + /eslint-config-preact@1.3.0(@typescript-eslint/eslint-plugin@4.33.0)(eslint@7.32.0)(typescript@5.2.2): resolution: {integrity: sha512-yHYXg5qNzEJd3D/30AmsIW0W8MuY858KpApXp7xxBF08IYUljSKCOqMx+dVucXHQnAm7+11wOnMkgVHIBAechw==} peerDependencies: eslint: 6.x || 7.x || 8.x @@ -9431,7 +9763,7 @@ packages: '@babel/plugin-syntax-jsx': 7.21.4(@babel/core@7.18.9) eslint: 7.32.0 eslint-plugin-compat: 4.0.2(eslint@7.32.0) - eslint-plugin-jest: 25.7.0(@typescript-eslint/eslint-plugin@4.33.0)(eslint@7.32.0)(typescript@5.1.3) + eslint-plugin-jest: 25.7.0(@typescript-eslint/eslint-plugin@4.33.0)(eslint@7.32.0)(typescript@5.2.2) eslint-plugin-react: 7.31.10(eslint@7.32.0) eslint-plugin-react-hooks: 4.6.0(eslint@7.32.0) transitivePeerDependencies: @@ -9441,21 +9773,21 @@ packages: - typescript dev: true - /eslint-config-preact@1.3.0(@typescript-eslint/eslint-plugin@5.41.0)(eslint@8.42.0)(typescript@5.1.3): + /eslint-config-preact@1.3.0(@typescript-eslint/eslint-plugin@5.41.0)(eslint@8.48.0)(typescript@5.2.2): resolution: {integrity: sha512-yHYXg5qNzEJd3D/30AmsIW0W8MuY858KpApXp7xxBF08IYUljSKCOqMx+dVucXHQnAm7+11wOnMkgVHIBAechw==} peerDependencies: eslint: 6.x || 7.x || 8.x dependencies: '@babel/core': 7.18.9 - '@babel/eslint-parser': 7.19.1(@babel/core@7.18.9)(eslint@8.42.0) + '@babel/eslint-parser': 7.19.1(@babel/core@7.18.9)(eslint@8.48.0) '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.18.9) '@babel/plugin-syntax-decorators': 7.19.0(@babel/core@7.18.9) '@babel/plugin-syntax-jsx': 7.21.4(@babel/core@7.18.9) - eslint: 8.42.0 - eslint-plugin-compat: 4.0.2(eslint@8.42.0) - eslint-plugin-jest: 25.7.0(@typescript-eslint/eslint-plugin@5.41.0)(eslint@8.42.0)(typescript@5.1.3) - eslint-plugin-react: 7.31.10(eslint@8.42.0) - eslint-plugin-react-hooks: 4.6.0(eslint@8.42.0) + eslint: 8.48.0 + eslint-plugin-compat: 4.0.2(eslint@8.48.0) + eslint-plugin-jest: 25.7.0(@typescript-eslint/eslint-plugin@5.41.0)(eslint@8.48.0)(typescript@5.2.2) + eslint-plugin-react: 7.31.10(eslint@8.48.0) + eslint-plugin-react-hooks: 4.6.0(eslint@8.48.0) transitivePeerDependencies: - '@typescript-eslint/eslint-plugin' - jest @@ -9502,7 +9834,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.41.0(eslint@8.26.0)(typescript@5.1.3) + '@typescript-eslint/parser': 5.41.0(eslint@8.26.0)(typescript@5.2.2) debug: 3.2.7 eslint: 8.26.0 eslint-import-resolver-node: 0.3.6 @@ -9527,7 +9859,7 @@ packages: semver: 7.3.5 dev: true - /eslint-plugin-compat@4.0.2(eslint@8.42.0): + /eslint-plugin-compat@4.0.2(eslint@8.48.0): resolution: {integrity: sha512-xqvoO54CLTVaEYGMzhu35Wzwk/As7rCvz/2dqwnFiWi0OJccEtGIn+5qq3zqIu9nboXlpdBN579fZcItC73Ycg==} engines: {node: '>=9.x'} peerDependencies: @@ -9538,7 +9870,7 @@ packages: browserslist: 4.21.5 caniuse-lite: 1.0.30001482 core-js: 3.26.0 - eslint: 8.42.0 + eslint: 8.48.0 find-up: 5.0.0 lodash.memoize: 4.1.2 semver: 7.3.5 @@ -9562,7 +9894,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.41.0(eslint@8.26.0)(typescript@5.1.3) + '@typescript-eslint/parser': 5.41.0(eslint@8.26.0)(typescript@5.2.2) array-includes: 3.1.5 array.prototype.flat: 1.3.0 debug: 2.6.9 @@ -9583,7 +9915,7 @@ packages: - supports-color dev: true - /eslint-plugin-jest@25.7.0(@typescript-eslint/eslint-plugin@4.33.0)(eslint@7.32.0)(typescript@5.1.3): + /eslint-plugin-jest@25.7.0(@typescript-eslint/eslint-plugin@4.33.0)(eslint@7.32.0)(typescript@5.2.2): resolution: {integrity: sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} peerDependencies: @@ -9596,15 +9928,15 @@ packages: jest: optional: true dependencies: - '@typescript-eslint/eslint-plugin': 4.33.0(@typescript-eslint/parser@4.33.0)(eslint@7.32.0)(typescript@5.1.3) - '@typescript-eslint/experimental-utils': 5.41.0(eslint@7.32.0)(typescript@5.1.3) + '@typescript-eslint/eslint-plugin': 4.33.0(@typescript-eslint/parser@4.33.0)(eslint@7.32.0)(typescript@5.2.2) + '@typescript-eslint/experimental-utils': 5.41.0(eslint@7.32.0)(typescript@5.2.2) eslint: 7.32.0 transitivePeerDependencies: - supports-color - typescript dev: true - /eslint-plugin-jest@25.7.0(@typescript-eslint/eslint-plugin@5.41.0)(eslint@8.42.0)(typescript@5.1.3): + /eslint-plugin-jest@25.7.0(@typescript-eslint/eslint-plugin@5.41.0)(eslint@8.48.0)(typescript@5.2.2): resolution: {integrity: sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} peerDependencies: @@ -9617,9 +9949,9 @@ packages: jest: optional: true dependencies: - '@typescript-eslint/eslint-plugin': 5.41.0(@typescript-eslint/parser@5.41.0)(eslint@8.42.0)(typescript@5.1.3) - '@typescript-eslint/experimental-utils': 5.41.0(eslint@8.42.0)(typescript@5.1.3) - eslint: 8.42.0 + '@typescript-eslint/eslint-plugin': 5.41.0(@typescript-eslint/parser@5.41.0)(eslint@8.48.0)(typescript@5.2.2) + '@typescript-eslint/experimental-utils': 5.41.0(eslint@8.48.0)(typescript@5.2.2) + eslint: 8.48.0 transitivePeerDependencies: - supports-color - typescript @@ -9665,13 +9997,13 @@ packages: eslint: 8.26.0 dev: true - /eslint-plugin-react-hooks@4.6.0(eslint@8.42.0): + /eslint-plugin-react-hooks@4.6.0(eslint@8.48.0): resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} engines: {node: '>=10'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 dependencies: - eslint: 8.42.0 + eslint: 8.48.0 dev: true /eslint-plugin-react@7.31.10(eslint@7.32.0): @@ -9720,7 +10052,7 @@ packages: string.prototype.matchall: 4.0.7 dev: true - /eslint-plugin-react@7.31.10(eslint@8.42.0): + /eslint-plugin-react@7.31.10(eslint@8.48.0): resolution: {integrity: sha512-e4N/nc6AAlg4UKW/mXeYWd3R++qUano5/o+t+wnWxIf+bLsOaH3a4q74kX3nDjYym3VBN4HyO9nEn1GcAqgQOA==} engines: {node: '>=4'} peerDependencies: @@ -9729,7 +10061,7 @@ packages: array-includes: 3.1.5 array.prototype.flatmap: 1.3.0 doctrine: 2.1.0 - eslint: 8.42.0 + eslint: 8.48.0 estraverse: 5.3.0 jsx-ast-utils: 3.3.3 minimatch: 3.1.2 @@ -9767,8 +10099,8 @@ packages: estraverse: 5.3.0 dev: true - /eslint-scope@7.2.0: - resolution: {integrity: sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==} + /eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: esrecurse: 4.3.0 @@ -9812,13 +10144,13 @@ packages: eslint-visitor-keys: 2.1.0 dev: true - /eslint-utils@3.0.0(eslint@8.42.0): + /eslint-utils@3.0.0(eslint@8.48.0): resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} peerDependencies: eslint: '>=5' dependencies: - eslint: 8.42.0 + eslint: 8.48.0 eslint-visitor-keys: 2.1.0 dev: true @@ -9837,8 +10169,8 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /eslint-visitor-keys@3.4.1: - resolution: {integrity: sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==} + /eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true @@ -9984,16 +10316,16 @@ packages: - supports-color dev: true - /eslint@8.42.0: - resolution: {integrity: sha512-ulg9Ms6E1WPf67PHaEY4/6E2tEn5/f7FXGzr3t9cBMugOmf1INYvuUwwh1aXQN4MfJ6a5K2iNwP3w4AColvI9A==} + /eslint@8.48.0: + resolution: {integrity: sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} hasBin: true dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.42.0) - '@eslint-community/regexpp': 4.5.1 - '@eslint/eslintrc': 2.0.3 - '@eslint/js': 8.42.0 - '@humanwhocodes/config-array': 0.11.10 + '@eslint-community/eslint-utils': 4.4.0(eslint@8.48.0) + '@eslint-community/regexpp': 4.8.0 + '@eslint/eslintrc': 2.1.2 + '@eslint/js': 8.48.0 + '@humanwhocodes/config-array': 0.11.11 '@humanwhocodes/module-importer': 1.0.1 '@nodelib/fs.walk': 1.2.8 ajv: 6.12.6 @@ -10002,19 +10334,18 @@ packages: debug: 4.3.4 doctrine: 3.0.0 escape-string-regexp: 4.0.0 - eslint-scope: 7.2.0 - eslint-visitor-keys: 3.4.1 - espree: 9.5.2 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 esquery: 1.5.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 file-entry-cache: 6.0.1 find-up: 5.0.0 glob-parent: 6.0.2 - globals: 13.20.0 + globals: 13.21.0 graphemer: 1.4.0 ignore: 5.2.4 - import-fresh: 3.3.0 imurmurhash: 0.1.4 is-glob: 4.0.3 is-path-inside: 3.0.3 @@ -10024,9 +10355,8 @@ packages: lodash.merge: 4.6.2 minimatch: 3.1.2 natural-compare: 1.4.0 - optionator: 0.9.1 + optionator: 0.9.3 strip-ansi: 6.0.1 - strip-json-comments: 3.1.1 text-table: 0.2.0 transitivePeerDependencies: - supports-color @@ -10055,13 +10385,13 @@ packages: eslint-visitor-keys: 3.3.0 dev: true - /espree@9.5.2: - resolution: {integrity: sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==} + /espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - acorn: 8.8.2 - acorn-jsx: 5.3.2(acorn@8.8.2) - eslint-visitor-keys: 3.4.1 + acorn: 8.10.0 + acorn-jsx: 5.3.2(acorn@8.10.0) + eslint-visitor-keys: 3.4.3 dev: true /esprima@4.0.1: @@ -10492,7 +10822,7 @@ packages: resolution: {integrity: sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==} dependencies: inherits: 2.0.4 - readable-stream: 2.3.7 + readable-stream: 2.3.8 dev: true /follow-redirects@1.15.2: @@ -10607,7 +10937,7 @@ packages: resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==} dependencies: inherits: 2.0.4 - readable-stream: 2.3.7 + readable-stream: 2.3.8 dev: true /fromentries@1.3.2: @@ -10665,10 +10995,10 @@ packages: /fs-write-stream-atomic@1.0.10: resolution: {integrity: sha512-gehEzmPn2nAwr39eay+x3X34Ra+M2QlVUTLhkXPjWdeO8RF9kszk116avgBJM3ZyNHgHXBNx+VmPaFC36k0PzA==} dependencies: - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 iferr: 0.1.5 imurmurhash: 0.1.4 - readable-stream: 2.3.7 + readable-stream: 2.3.8 dev: true /fs.realpath@1.0.0: @@ -10686,8 +11016,8 @@ packages: dev: true optional: true - /fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] requiresBuild: true @@ -10921,8 +11251,8 @@ packages: type-fest: 0.20.2 dev: true - /globals@13.20.0: - resolution: {integrity: sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==} + /globals@13.21.0: + resolution: {integrity: sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==} engines: {node: '>=8'} dependencies: type-fest: 0.20.2 @@ -10985,6 +11315,10 @@ packages: resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} dev: true + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + dev: true + /grapheme-splitter@1.0.4: resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} dev: true @@ -11105,7 +11439,7 @@ packages: engines: {node: '>=4'} dependencies: inherits: 2.0.4 - readable-stream: 3.6.0 + readable-stream: 3.6.2 safe-buffer: 5.2.1 dev: true @@ -11201,7 +11535,7 @@ packages: he: 1.2.0 param-case: 3.0.4 relateurl: 0.2.7 - terser: 5.17.7 + terser: 5.19.4 dev: true /html-minifier@3.5.21: @@ -11253,8 +11587,8 @@ packages: webpack: 4.46.0 dev: true - /html-webpack-plugin@5.5.1(webpack@4.46.0): - resolution: {integrity: sha512-cTUzZ1+NqjGEKjmVgZKLMdiFg3m9MdRXkZW2OEe69WYVi5ONLMmlnSZdXzGGMOq0C8jGDrL6EWyEDDUioHO/pA==} + /html-webpack-plugin@5.5.3(webpack@4.46.0): + resolution: {integrity: sha512-6YrDKTuqaP/TquFH7h4srYWsZx+x6k6+FbsTm0ziCwGHDP78Unr1r9F/H4+sGmMbX08GQcJ+K64x55b+7VM/jg==} engines: {node: '>=10.13.0'} peerDependencies: webpack: ^5.20.0 @@ -11267,13 +11601,13 @@ packages: webpack: 4.46.0 dev: true - /html-webpack-skip-assets-plugin@1.0.3(html-webpack-plugin@5.5.1)(webpack@4.46.0): + /html-webpack-skip-assets-plugin@1.0.3(html-webpack-plugin@5.5.3)(webpack@4.46.0): resolution: {integrity: sha512-vpdh+JZGlE1Df3IftH2gw5P7b6yfTsahcOIJnkkkj5iJU9dUStXgzgALoXWwl8+17wWgFm3edcJzeYTJBYfMAw==} peerDependencies: html-webpack-plugin: '>=3.0.0' webpack: '>=3.0.0' dependencies: - html-webpack-plugin: 5.5.1(webpack@4.46.0) + html-webpack-plugin: 5.5.3(webpack@4.46.0) minimatch: 3.0.4 webpack: 4.46.0 dev: true @@ -12137,6 +12471,13 @@ packages: minimist: 1.2.7 dev: true + /json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + dependencies: + minimist: 1.2.8 + dev: true + /json5@2.2.1: resolution: {integrity: sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==} engines: {node: '>=6'} @@ -12319,6 +12660,15 @@ packages: json5: 1.0.1 dev: true + /loader-utils@1.4.2: + resolution: {integrity: sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==} + engines: {node: '>=4.0.0'} + dependencies: + big.js: 5.2.2 + emojis-list: 3.0.0 + json5: 1.0.2 + dev: true + /loader-utils@2.0.3: resolution: {integrity: sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A==} engines: {node: '>=8.9.0'} @@ -12431,7 +12781,7 @@ packages: /lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} dependencies: - tslib: 2.6.1 + tslib: 2.6.2 dev: true /lowercase-keys@1.0.1: @@ -12483,7 +12833,7 @@ packages: engines: {node: '>=6'} dependencies: pify: 4.0.1 - semver: 5.7.1 + semver: 5.7.2 dev: true /make-dir@3.1.0: @@ -12576,7 +12926,7 @@ packages: resolution: {integrity: sha512-cda4JKCxReDXFXRqOHPQscuIYg1PvxbE2S2GP45rnwfEK+vZaXC8C1OFvdHIbgw0DLzowXGVoxLaAmlgRy14GQ==} dependencies: errno: 0.1.8 - readable-stream: 2.3.7 + readable-stream: 2.3.8 dev: true /memory-fs@0.5.0: @@ -12584,7 +12934,7 @@ packages: engines: {node: '>=4.3.0 <5.0.0 || >=5.10'} dependencies: errno: 0.1.8 - readable-stream: 2.3.7 + readable-stream: 2.3.8 dev: true /merge-descriptors@1.0.1: @@ -12638,6 +12988,7 @@ packages: /miller-rabin@4.0.1: resolution: {integrity: sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==} + hasBin: true dependencies: bn.js: 4.12.0 brorand: 1.1.0 @@ -12744,9 +13095,20 @@ packages: brace-expansion: 2.0.1 dev: true + /minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + /minimist@1.2.7: resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==} + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: true + /minipass-collect@1.0.2: resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} engines: {node: '>= 8'} @@ -12833,8 +13195,9 @@ packages: /mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true dependencies: - minimist: 1.2.7 + minimist: 1.2.8 dev: true /mkdirp@1.0.4: @@ -12995,7 +13358,7 @@ packages: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} dependencies: lower-case: 2.0.2 - tslib: 2.6.1 + tslib: 2.6.2 dev: true /node-abi@3.45.0: @@ -13049,13 +13412,13 @@ packages: process: 0.11.10 punycode: 1.4.1 querystring-es3: 0.2.1 - readable-stream: 2.3.7 + readable-stream: 2.3.8 stream-browserify: 2.0.2 stream-http: 2.8.3 string_decoder: 1.3.0 timers-browserify: 2.0.12 tty-browserify: 0.0.0 - url: 0.11.0 + url: 0.11.1 util: 0.11.1 vm-browserify: 1.1.2 dev: true @@ -13070,6 +13433,10 @@ packages: /node-releases@2.0.10: resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==} + /node-releases@2.0.13: + resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} + dev: true + /nofilter@3.1.0: resolution: {integrity: sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==} engines: {node: '>=12.19'} @@ -13420,6 +13787,18 @@ packages: word-wrap: 1.2.3 dev: true + /optionator@0.9.3: + resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} + engines: {node: '>= 0.8.0'} + dependencies: + '@aashutoshrathi/word-wrap': 1.2.6 + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + /ora@5.4.1: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} @@ -13571,9 +13950,9 @@ packages: /parallel-transform@1.2.0: resolution: {integrity: sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==} dependencies: - cyclist: 1.0.1 + cyclist: 1.0.2 inherits: 2.0.4 - readable-stream: 2.3.7 + readable-stream: 2.3.8 dev: true /param-case@2.1.1: @@ -13586,7 +13965,7 @@ packages: resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} dependencies: dot-case: 3.0.4 - tslib: 2.6.1 + tslib: 2.6.2 dev: true /parent-module@1.0.1: @@ -13651,7 +14030,7 @@ packages: resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} dependencies: no-case: 3.0.4 - tslib: 2.6.1 + tslib: 2.6.2 dev: true /pascalcase@0.1.1: @@ -14895,10 +15274,6 @@ packages: pump: 2.0.1 dev: true - /punycode@1.3.2: - resolution: {integrity: sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==} - dev: true - /punycode@1.4.1: resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} dev: true @@ -14931,6 +15306,13 @@ packages: side-channel: 1.0.4 dev: true + /qs@6.11.2: + resolution: {integrity: sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.4 + dev: true + /qs@6.5.3: resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==} engines: {node: '>=0.6'} @@ -14941,11 +15323,6 @@ packages: engines: {node: '>=0.4.x'} dev: true - /querystring@0.2.0: - resolution: {integrity: sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==} - engines: {node: '>=0.4.x'} - dev: true - /querystring@0.2.1: resolution: {integrity: sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==} engines: {node: '>=0.4.x'} @@ -15050,6 +15427,18 @@ packages: util-deprecate: 1.0.2 dev: true + /readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + dev: true + /readable-stream@3.6.0: resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==} engines: {node: '>= 6'} @@ -15058,14 +15447,23 @@ packages: string_decoder: 1.3.0 util-deprecate: 1.0.2 + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: true + /readdirp@2.2.1: resolution: {integrity: sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==} engines: {node: '>=0.10'} requiresBuild: true dependencies: - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 micromatch: 3.1.10 - readable-stream: 2.3.7 + readable-stream: 2.3.8 transitivePeerDependencies: - supports-color dev: true @@ -15315,6 +15713,7 @@ packages: /resolve-url@0.2.1: resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==} + deprecated: https://github.com/lydell/resolve-url#deprecated dev: true /resolve@1.22.1: @@ -15420,7 +15819,7 @@ packages: resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==} engines: {node: '>=10.0.0'} optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true /run-parallel@1.2.0: @@ -15565,9 +15964,19 @@ packages: hasBin: true dev: true + /semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + dev: true + /semver@6.3.0: resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + dev: true + /semver@7.3.4: resolution: {integrity: sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==} engines: {node: '>=10'} @@ -15689,6 +16098,7 @@ packages: /sha.js@2.4.11: resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} + hasBin: true dependencies: inherits: 2.0.4 safe-buffer: 5.2.1 @@ -15725,10 +16135,10 @@ packages: engines: {node: '>=8'} dev: true - /shiki@0.14.2: - resolution: {integrity: sha512-ltSZlSLOuSY0M0Y75KA+ieRaZ0Trf5Wl3gutE7jzLuIcWxLp5i/uEnLoQWNvgKXQ5OMpGkJnVMRLAuzjc0LJ2A==} + /shiki@0.14.4: + resolution: {integrity: sha512-IXCRip2IQzKwxArNNq1S+On4KPML3Yyn8Zzs/xRgcgOWIr8ntIK3IKzjFPfjy/7kt9ZMjc+FItfqHRBg8b6tNQ==} dependencies: - ansi-sequence-parser: 1.1.0 + ansi-sequence-parser: 1.1.1 jsonc-parser: 3.2.0 vscode-oniguruma: 1.7.0 vscode-textmate: 8.0.0 @@ -15905,9 +16315,10 @@ packages: /source-map-resolve@0.5.3: resolution: {integrity: sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==} + deprecated: See https://github.com/lydell/source-map-resolve#deprecated dependencies: atob: 2.1.2 - decode-uri-component: 0.2.0 + decode-uri-component: 0.2.2 resolve-url: 0.2.1 source-map-url: 0.4.1 urix: 0.1.0 @@ -16076,7 +16487,7 @@ packages: resolution: {integrity: sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==} dependencies: inherits: 2.0.4 - readable-stream: 2.3.7 + readable-stream: 2.3.8 dev: true /stream-each@1.2.3: @@ -16091,7 +16502,7 @@ packages: dependencies: builtin-status-codes: 3.0.0 inherits: 2.0.4 - readable-stream: 2.3.7 + readable-stream: 2.3.8 to-arraybuffer: 1.0.1 xtend: 4.0.2 dev: true @@ -16350,22 +16761,24 @@ packages: stable: 0.1.8 dev: true - /swr@1.3.0(react@18.2.0): - resolution: {integrity: sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw==} + /swr@2.0.3(react@18.2.0): + resolution: {integrity: sha512-sGvQDok/AHEWTPfhUWXEHBVEXmgGnuahyhmRQbjl9XBYxT/MSlAzvXEKQpyM++bMPaI52vcWS2HiKNaW7+9OFw==} + engines: {pnpm: '7'} peerDependencies: react: ^16.11.0 || ^17.0.0 || ^18.0.0 dependencies: react: 18.2.0 - dev: false + use-sync-external-store: 1.2.0(react@18.2.0) - /swr@2.0.3(react@18.2.0): - resolution: {integrity: sha512-sGvQDok/AHEWTPfhUWXEHBVEXmgGnuahyhmRQbjl9XBYxT/MSlAzvXEKQpyM++bMPaI52vcWS2HiKNaW7+9OFw==} - engines: {pnpm: '7'} + /swr@2.2.2(react@18.2.0): + resolution: {integrity: sha512-CbR41AoMD4TQBQw9ic3GTXspgfM9Y8Mdhb5Ob4uIKXhWqnRLItwA5fpGvB7SmSw3+zEjb0PdhiEumtUvYoQ+bQ==} peerDependencies: react: ^16.11.0 || ^17.0.0 || ^18.0.0 dependencies: + client-only: 0.0.1 react: 18.2.0 use-sync-external-store: 1.2.0(react@18.2.0) + dev: false /symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -16529,8 +16942,9 @@ packages: /terser@4.8.1: resolution: {integrity: sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==} engines: {node: '>=6.0.0'} + hasBin: true dependencies: - acorn: 8.8.2 + acorn: 8.10.0 commander: 2.20.3 source-map: 0.6.1 source-map-support: 0.5.21 @@ -16546,13 +16960,13 @@ packages: source-map-support: 0.5.21 dev: true - /terser@5.17.7: - resolution: {integrity: sha512-/bi0Zm2C6VAexlGgLlVxA0P2lru/sdLyfCVaRMfKVo9nWxbmz7f/sD8VPybPeSUJaJcwmCJis9pBIhcVcG1QcQ==} + /terser@5.19.4: + resolution: {integrity: sha512-6p1DjHeuluwxDXcuT9VR8p64klWJKo1ILiy19s6C9+0Bh2+NWTX6nD9EPppiER4ICkHDVB1RkVpin/YW2nQn/g==} engines: {node: '>=10'} hasBin: true dependencies: - '@jridgewell/source-map': 0.3.3 - acorn: 8.8.2 + '@jridgewell/source-map': 0.3.5 + acorn: 8.10.0 commander: 2.20.3 source-map-support: 0.5.21 dev: true @@ -16588,7 +17002,7 @@ packages: /through2@2.0.5: resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} dependencies: - readable-stream: 2.3.7 + readable-stream: 2.3.8 xtend: 4.0.2 dev: true @@ -16726,7 +17140,7 @@ packages: tslib: 2.6.1 dev: true - /ts-node@10.9.1(@types/node@20.4.1)(typescript@5.1.3): + /ts-node@10.9.1(@types/node@20.5.9)(typescript@5.2.2): resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true peerDependencies: @@ -16745,14 +17159,14 @@ packages: '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.3 - '@types/node': 20.4.1 + '@types/node': 20.5.9 acorn: 8.8.1 acorn-walk: 8.2.0 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.1.3 + typescript: 5.2.2 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 dev: true @@ -16797,14 +17211,18 @@ packages: resolution: {integrity: sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==} dev: true - /tsutils@3.21.0(typescript@5.1.3): + /tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + dev: true + + /tsutils@3.21.0(typescript@5.2.2): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' dependencies: tslib: 1.14.1 - typescript: 5.1.3 + typescript: 5.2.2 dev: true /tty-browserify@0.0.0: @@ -16877,18 +17295,18 @@ packages: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} dev: true - /typedoc@0.24.8(typescript@5.1.3): - resolution: {integrity: sha512-ahJ6Cpcvxwaxfu4KtjA8qZNqS43wYt6JL27wYiIgl1vd38WW/KWX11YuAeZhuz9v+ttrutSsgK+XO1CjL1kA3w==} - engines: {node: '>= 14.14'} + /typedoc@0.25.1(typescript@5.2.2): + resolution: {integrity: sha512-c2ye3YUtGIadxN2O6YwPEXgrZcvhlZ6HlhWZ8jQRNzwLPn2ylhdGqdR8HbyDRyALP8J6lmSANILCkkIdNPFxqA==} + engines: {node: '>= 16'} hasBin: true peerDependencies: - typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x + typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x dependencies: lunr: 2.3.9 marked: 4.3.0 - minimatch: 9.0.1 - shiki: 0.14.2 - typescript: 5.1.3 + minimatch: 9.0.3 + shiki: 0.14.4 + typescript: 5.2.2 dev: true /typescript@4.6.4: @@ -16897,13 +17315,8 @@ packages: hasBin: true dev: true - /typescript@5.1.3: - resolution: {integrity: sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==} - engines: {node: '>=14.17'} - dev: true - - /typescript@5.1.6: - resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==} + /typescript@5.2.2: + resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} engines: {node: '>=14.17'} hasBin: true dev: true @@ -17023,6 +17436,7 @@ packages: /upath@1.2.0: resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==} engines: {node: '>=4'} + requiresBuild: true dev: true /update-browserslist-db@1.0.10(browserslist@4.21.4): @@ -17046,6 +17460,17 @@ packages: escalade: 3.1.1 picocolors: 1.0.0 + /update-browserslist-db@1.0.11(browserslist@4.21.10): + resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.21.10 + escalade: 3.1.1 + picocolors: 1.0.0 + dev: true + /update-notifier@5.1.0: resolution: {integrity: sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==} engines: {node: '>=10'} @@ -17078,6 +17503,7 @@ packages: /urix@0.1.0: resolution: {integrity: sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==} + deprecated: Please see https://github.com/lydell/urix#deprecated dev: true /url-loader@4.1.1(file-loader@6.2.0)(webpack@4.46.0): @@ -17104,11 +17530,11 @@ packages: prepend-http: 2.0.0 dev: true - /url@0.11.0: - resolution: {integrity: sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==} + /url@0.11.1: + resolution: {integrity: sha512-rWS3H04/+mzzJkv0eZ7vEDGiQbgquI1fGfOad6zKvgYQi1SzMmhl7c/DdRGxhaWrVH6z0qWITo8rpnxK/RfEhA==} dependencies: - punycode: 1.3.2 - querystring: 0.2.0 + punycode: 1.4.1 + qs: 6.11.2 dev: true /use-sync-external-store@1.2.0(react@18.2.0): @@ -17258,7 +17684,7 @@ packages: /watchpack@1.7.5: resolution: {integrity: sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==} dependencies: - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 neo-async: 2.6.2 optionalDependencies: chokidar: 3.5.3 @@ -17443,7 +17869,7 @@ packages: eslint-scope: 4.0.3 json-parse-better-errors: 1.0.2 loader-runner: 2.4.0 - loader-utils: 1.4.0 + loader-utils: 1.4.2 memory-fs: 0.4.1 micromatch: 3.1.10 mkdirp: 0.5.6 |