Compare commits

..

No commits in common. "70fca92e781696a057089bc8bc48adebdf6e017e" and "0b606028339d8256643ce60f11e72a090a301b58" have entirely different histories.

119 changed files with 969 additions and 4195 deletions

View File

@ -20,7 +20,6 @@ dist:
$(git-archive-all) \ $(git-archive-all) \
--include ./configure \ --include ./configure \
--include ./packages/taler-wallet-cli/configure \ --include ./packages/taler-wallet-cli/configure \
--include ./packages/anastasis-cli/configure \
--include ./packages/demobank-ui/configure \ --include ./packages/demobank-ui/configure \
--include ./packages/taler-harness/configure \ --include ./packages/taler-harness/configure \
--include ./packages/merchant-backoffice-ui/configure \ --include ./packages/merchant-backoffice-ui/configure \
@ -122,17 +121,15 @@ install:
pnpm install --frozen-lockfile pnpm install --frozen-lockfile
pnpm run compile pnpm run compile
make -C packages/taler-wallet-cli TOPLEVEL=yes install-nodeps make -C packages/taler-wallet-cli TOPLEVEL=yes install-nodeps
make -C packages/anastasis-cli TOPLEVEL=yes install-nodeps
make -C packages/taler-harness TOPLEVEL=yes install-nodeps make -C packages/taler-harness TOPLEVEL=yes install-nodeps
make -C packages/demobank-ui TOPLEVEL=yes install-nodeps make -C packages/demobank-ui TOPLEVEL=yes install-nodeps
make -C packages/merchant-backoffice-ui TOPLEVEL=yes install-nodeps make -C packages/merchant-backoffice-ui TOPLEVEL=yes install-nodeps
make -C packages/aml-backoffice-ui TOPLEVEL=yes install-nodeps make -C packages/aml-backoffice-ui TOPLEVEL=yes install-nodeps
.PHONY: install-tools .PHONY: install-tools
# Install taler-wallet-cli, anastasis-cli and taler-harness # Install taler-wallet-cli and taler-harness
install-tools: install-tools:
pnpm install --frozen-lockfile --filter @gnu-taler/taler-wallet-cli... --filter @gnu-taler/anastasis-cli... --filter @gnu-taler/taler-harness... pnpm install --frozen-lockfile --filter @gnu-taler/taler-wallet-cli... --filter @gnu-taler/taler-harness...
pnpm run --filter @gnu-taler/taler-wallet-cli... --filter @gnu-taler/anastasis-cli... --filter @gnu-taler/taler-harness... compile pnpm run --filter @gnu-taler/taler-wallet-cli... --filter @gnu-taler/taler-harness... compile
make -C packages/taler-wallet-cli TOPLEVEL=yes install-nodeps make -C packages/taler-wallet-cli TOPLEVEL=yes install-nodeps
make -C packages/anastasis-cli TOPLEVEL=yes install-nodeps
make -C packages/taler-harness TOPLEVEL=yes install-nodeps make -C packages/taler-harness TOPLEVEL=yes install-nodeps

View File

@ -27,7 +27,6 @@ copy_configure() {
our_configure=build-system/taler-build-scripts/configure our_configure=build-system/taler-build-scripts/configure
copy_configure "$our_configure" ./configure copy_configure "$our_configure" ./configure
copy_configure "$our_configure" ./packages/taler-wallet-cli/configure copy_configure "$our_configure" ./packages/taler-wallet-cli/configure
copy_configure "$our_configure" ./packages/anastasis-cli/configure
copy_configure "$our_configure" ./packages/demobank-ui/configure copy_configure "$our_configure" ./packages/demobank-ui/configure
copy_configure "$our_configure" ./packages/merchant-backoffice-ui/configure copy_configure "$our_configure" ./packages/merchant-backoffice-ui/configure
copy_configure "$our_configure" ./packages/taler-harness/configure copy_configure "$our_configure" ./packages/taler-harness/configure

View File

@ -61,20 +61,17 @@ export function buildQuerySignature(key: SigningKey): string {
return encodeCrock(eddsaSign(sigBlob, key)); return encodeCrock(eddsaSign(sigBlob, key));
} }
export function buildDecisionSignature( export function buildDecisionSignature(
key: SigningKey, key: SigningKey,
decision: AmlExchangeBackend.AmlDecision, decision: AmlExchangeBackend.AmlDecision,
): string { ): string {
const zero = new Uint8Array(new ArrayBuffer(64))
const sigBlob = buildSigPS(TalerSignaturePurpose.TALER_SIGNATURE_AML_DECISION) const sigBlob = buildSigPS(TalerSignaturePurpose.TALER_SIGNATURE_AML_DECISION)
//TODO: new need the null terminator, also in the exchange .put(hash(stringToBytes(decision.justification)))
.put(hash(stringToBytes(decision.justification)))//check null // .put(timestampRoundedToBuffer(decision.decision_time))
.put(timestampRoundedToBuffer(decision.decision_time))
.put(amountToBuffer(decision.new_threshold)) .put(amountToBuffer(decision.new_threshold))
.put(decodeCrock(decision.h_payto)) .put(decodeCrock(decision.h_payto))
.put(zero) //kyc_requirement // .put(hash(stringToBytes(decision.kyc_requirements)))
.put(bufferForUint32(decision.new_state)) .put(bufferForUint32(decision.new_state))
.build(); .build();

View File

@ -85,6 +85,7 @@ export function useCases(
const records = !afterData const records = !afterData
? [] ? []
: ((afterData ?? lastAfter).data ?? { records: [] }).records; : ((afterData ?? lastAfter).data ?? { records: [] }).records;
console.log("afterdata", afterData, lastAfter, records)
if (loadingAfter) return { loading: true, data: { records } }; if (loadingAfter) return { loading: true, data: { records } };
if (afterData) { if (afterData) {
return { ok: true, data: { records }, ...pagination }; return { ok: true, data: { records }, ...pagination };

View File

@ -38,7 +38,7 @@ export function NewFormEntry({
fullName: "loggedIn_user_fullname", fullName: "loggedIn_user_fullname",
when: AbsoluteTime.now(), when: AbsoluteTime.now(),
state: AmlExchangeBackend.AmlState.pending, state: AmlExchangeBackend.AmlState.pending,
threshold: Amounts.parseOrThrow("KUDOS:1000"), threshold: Amounts.parseOrThrow("ARS:1000"),
}; };
const api = useAmlCasesAPI() const api = useAmlCasesAPI()

View File

@ -1,41 +0,0 @@
# This Makefile has been placed in the public domain.
ifeq ($(TOPLEVEL), yes)
$(info top-level build)
-include ../../.config.mk
else
$(info package-level build)
-include ../../.config.mk
-include .config.mk
endif
$(info prefix is $(prefix))
all:
@echo use 'make install' to build and install anastasis-cli
ifndef prefix
.PHONY: warn-noprefix install
warn-noprefix:
@echo "no prefix configured, did you run ./configure?"
install: warn-noprefix
else
install_target = $(prefix)/lib/anastasis-cli
.PHONY: install install-nodeps deps
install-nodeps:
./build-node.mjs
install -d $(prefix)/bin
install -d $(install_target)/bin
install -d $(install_target)/node_modules/anastasis-cli
install -d $(install_target)/node_modules/anastasis-cli/bin
install -d $(install_target)/node_modules/anastasis-cli/dist
install ./dist/anastasis-cli-bundled.cjs $(install_target)/node_modules/anastasis-cli/dist/
install ./dist/anastasis-cli-bundled.cjs.map $(install_target)/node_modules/anastasis-cli/dist/
install ./bin/anastasis-cli.mjs $(install_target)/node_modules/anastasis-cli/bin/
ln -sf $(install_target)/node_modules/anastasis-cli/bin/anastasis-cli.mjs $(prefix)/bin/anastasis-cli
deps:
pnpm install --frozen-lockfile --filter @gnu-taler/anastasis-cli...
install:
$(MAKE) deps
$(MAKE) install-nodeps
endif

View File

@ -1,4 +0,0 @@
# anastasis-cli
This package provides `anastasis-cli`, the command-line interface for the
Anastasis backup system.

View File

@ -1,20 +0,0 @@
#!/usr/bin/env node
/*
This file is part of GNU Taler
(C) 2022 Taler Systems SA
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.
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
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { reducerCliMain } from '../dist/anastasis-cli-bundled.cjs';
reducerCliMain();

View File

@ -1,70 +0,0 @@
#!/usr/bin/env node
/*
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 esbuild from "esbuild";
import path from "path";
import fs from "fs";
const BASE = process.cwd();
let GIT_ROOT = BASE;
while (!fs.existsSync(path.join(GIT_ROOT, ".git")) && GIT_ROOT !== "/") {
GIT_ROOT = path.join(GIT_ROOT, "../");
}
if (GIT_ROOT === "/") {
console.log("not found");
process.exit(1);
}
const GIT_HASH = GIT_ROOT === "/" ? undefined : git_hash();
let _package = JSON.parse(fs.readFileSync(path.join(BASE, "package.json")));
function git_hash() {
const rev = fs
.readFileSync(path.join(GIT_ROOT, ".git", "HEAD"))
.toString()
.trim()
.split(/.*[: ]/)
.slice(-1)[0];
if (rev.indexOf("/") === -1) {
return rev;
} else {
return fs.readFileSync(path.join(GIT_ROOT, ".git", rev)).toString().trim();
}
}
export const buildConfig = {
entryPoints: ["src/index.ts"],
outfile: "dist/anastasis-cli-bundled.cjs",
bundle: true,
minify: false,
target: ["es2020"],
format: "cjs",
platform: "node",
sourcemap: true,
inject: ["src/import-meta-url.js"],
define: {
__VERSION__: `"${_package.version}"`,
__GIT_HASH__: `"${GIT_HASH}"`,
["import.meta.url"]: "import_meta_url",
},
};
esbuild.build(buildConfig).catch((e) => {
console.log(e);
process.exit(1);
});

View File

@ -1,44 +0,0 @@
{
"name": "@gnu-taler/anastasis-cli",
"version": "0.0.1",
"description": "",
"engines": {
"node": ">=0.18.0"
},
"repository": {
"type": "git",
"url": "git://git.taler.net/wallet-core.git"
},
"author": "Florian Dold",
"license": "GPL-3.0",
"bin": {
"anastasis-cli": "./bin/anastasis-cli.mjs"
},
"type": "module",
"scripts": {
"compile": "tsc --build && ./build-node.mjs",
"test": "tsc",
"clean": "rimraf lib dist tsconfig.tsbuildinfo",
"pretty": "prettier --write src"
},
"files": [
"AUTHORS",
"README",
"COPYING",
"bin/",
"dist/node",
"src/"
],
"devDependencies": {
"@types/node": "^18.11.17",
"prettier": "^2.8.8",
"rimraf": "^3.0.2",
"typedoc": "^0.24.8",
"typescript": "^5.1.3"
},
"dependencies": {
"@gnu-taler/taler-util": "workspace:*",
"@gnu-taler/anastasis-core": "workspace:*",
"tslib": "^2.5.3"
}
}

View File

@ -1,2 +0,0 @@
// Helper to make 'import.meta.url' available in esbuild-bundled code as well.
export const import_meta_url = require("url").pathToFileURL(__filename);

View File

@ -1,87 +0,0 @@
import { clk } from "@gnu-taler/taler-util/clk";
import {
discoverPolicies,
getBackupStartState,
getRecoveryStartState,
reduceAction,
} from "@gnu-taler/anastasis-core";
import fs from "fs";
import { j2s } from "@gnu-taler/taler-util";
export const reducerCli = clk.program("anastasis-cli", {
help: "Command line interface for Anastasis.",
});
reducerCli
.subcommand("reducer", "reduce", {
help: "Run the anastasis reducer",
})
.flag("initBackup", ["-b", "--backup"])
.flag("initRecovery", ["-r", "--restore"])
.maybeOption("argumentsJson", ["-a", "--arguments"], clk.STRING)
.maybeArgument("action", clk.STRING)
.maybeArgument("stateFile", clk.STRING)
.action(async (x) => {
if (x.reducer.initBackup) {
console.log(JSON.stringify(await getBackupStartState()));
return;
} else if (x.reducer.initRecovery) {
console.log(JSON.stringify(await getRecoveryStartState()));
return;
}
const action = x.reducer.action;
if (!action) {
console.log("action required");
return;
}
let lastState: any;
if (x.reducer.stateFile) {
const s = fs.readFileSync(x.reducer.stateFile, { encoding: "utf-8" });
lastState = JSON.parse(s);
} else {
const s = await read(process.stdin);
lastState = JSON.parse(s);
}
let args: any;
if (x.reducer.argumentsJson) {
args = JSON.parse(x.reducer.argumentsJson);
} else {
args = {};
}
const nextState = await reduceAction(lastState, action, args);
console.log(JSON.stringify(nextState));
});
reducerCli
.subcommand("discover", "discover", {
help: "Run the anastasis reducer",
})
.maybeArgument("stateFile", clk.STRING)
.action(async (args) => {
let lastState: any;
if (args.discover.stateFile) {
const s = fs.readFileSync(args.discover.stateFile, { encoding: "utf-8" });
lastState = JSON.parse(s);
} else {
const s = await read(process.stdin);
lastState = JSON.parse(s);
}
const res = await discoverPolicies(lastState);
console.log(j2s(res));
});
async function read(stream: NodeJS.ReadStream): Promise<string> {
const chunks = [];
for await (const chunk of stream) {
chunks.push(chunk);
}
return Buffer.concat(chunks).toString("utf8");
}
export function reducerCliMain() {
reducerCli.run();
}

View File

@ -1,33 +0,0 @@
{
"compileOnSave": true,
"compilerOptions": {
"composite": true,
"target": "ES2018",
"module": "ESNext",
"moduleResolution": "Node16",
"sourceMap": true,
"lib": ["es6"],
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"strict": true,
"strictPropertyInitialization": false,
"outDir": "lib",
"noImplicitAny": true,
"noImplicitThis": true,
"incremental": true,
"esModuleInterop": true,
"importHelpers": true,
"rootDir": "src",
"baseUrl": "./src",
"typeRoots": ["./node_modules/@types"]
},
"include": ["src/**/*"],
"references": [
{
"path": "../anastasis-core/"
},
{
"path": "../taler-util/"
}
]
}

View File

@ -23,6 +23,7 @@
"dependencies": { "dependencies": {
"@gnu-taler/taler-util": "workspace:*", "@gnu-taler/taler-util": "workspace:*",
"fflate": "^0.7.4", "fflate": "^0.7.4",
"hash-wasm": "^4.9.0",
"tslib": "^2.5.3" "tslib": "^2.5.3"
}, },
"ava": { "ava": {

View File

@ -0,0 +1,7 @@
import { reducerCliMain } from "./cli.js";
async function r() {
reducerCliMain();
}
r();

View File

@ -0,0 +1,64 @@
import { clk } from "@gnu-taler/taler-util/clk";
import {
getBackupStartState,
getRecoveryStartState,
reduceAction,
} from "./index.js";
import fs from "fs";
export const reducerCli = clk
.program("reducer", {
help: "Command line interface for Anastasis.",
})
.flag("initBackup", ["-b", "--backup"])
.flag("initRecovery", ["-r", "--restore"])
.maybeOption("argumentsJson", ["-a", "--arguments"], clk.STRING)
.maybeArgument("action", clk.STRING)
.maybeArgument("stateFile", clk.STRING);
async function read(stream: NodeJS.ReadStream): Promise<string> {
const chunks = [];
for await (const chunk of stream) {
chunks.push(chunk);
}
return Buffer.concat(chunks).toString("utf8");
}
reducerCli.action(async (x) => {
if (x.reducer.initBackup) {
console.log(JSON.stringify(await getBackupStartState()));
return;
} else if (x.reducer.initRecovery) {
console.log(JSON.stringify(await getRecoveryStartState()));
return;
}
const action = x.reducer.action;
if (!action) {
console.log("action required");
return;
}
let lastState: any;
if (x.reducer.stateFile) {
const s = fs.readFileSync(x.reducer.stateFile, { encoding: "utf-8" });
lastState = JSON.parse(s);
} else {
const s = await read(process.stdin);
lastState = JSON.parse(s);
}
let args: any;
if (x.reducer.argumentsJson) {
args = JSON.parse(x.reducer.argumentsJson);
} else {
args = {};
}
const nextState = await reduceAction(lastState, action, args);
console.log(JSON.stringify(nextState));
});
export function reducerCliMain() {
reducerCli.run();
}

View File

@ -26,8 +26,8 @@ import {
secretbox_open, secretbox_open,
hash, hash,
bytesToString, bytesToString,
hashArgon2id,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { argon2id } from "hash-wasm";
export type Flavor<T, FlavorT extends string> = T & { export type Flavor<T, FlavorT extends string> = T & {
_flavor?: `anastasis.${FlavorT}`; _flavor?: `anastasis.${FlavorT}`;
@ -71,13 +71,15 @@ export async function userIdentifierDerive(
): Promise<UserIdentifier> { ): Promise<UserIdentifier> {
const canonIdData = canonicalJson(idData); const canonIdData = canonicalJson(idData);
const hashInput = stringToBytes(canonIdData); const hashInput = stringToBytes(canonIdData);
const result = await hashArgon2id( const result = await argon2id({
hashInput, // password hashLength: 64,
decodeCrock(serverSalt), // salt iterations: 3,
3, // iterations memorySize: 1024 /* kibibytes */,
1024, // memoryLimit (kibibytes) parallelism: 1,
64, // hashLength password: hashInput,
); salt: decodeCrock(serverSalt),
outputType: "binary",
});
return encodeCrock(result); return encodeCrock(result);
} }
@ -151,11 +153,7 @@ export async function decryptPolicyMetadata(
userId: UserIdentifier, userId: UserIdentifier,
metadataEnc: OpaqueData, metadataEnc: OpaqueData,
): Promise<PolicyMetadata> { ): Promise<PolicyMetadata> {
// @ts-ignore
console.log("metadataEnc", metadataEnc);
const plain = await anastasisDecrypt(asOpaque(userId), metadataEnc, "rmd"); const plain = await anastasisDecrypt(asOpaque(userId), metadataEnc, "rmd");
// @ts-ignore
console.log("plain:", plain);
const metadataBytes = decodeCrock(plain); const metadataBytes = decodeCrock(plain);
const policyHash = encodeCrock(metadataBytes.slice(0, 64)); const policyHash = encodeCrock(metadataBytes.slice(0, 64));
const secretName = bytesToString(metadataBytes.slice(64)); const secretName = bytesToString(metadataBytes.slice(64));
@ -345,13 +343,15 @@ export async function secureAnswerHash(
truthUuid: TruthUuid, truthUuid: TruthUuid,
questionSalt: TruthSalt, questionSalt: TruthSalt,
): Promise<SecureAnswerHash> { ): Promise<SecureAnswerHash> {
const powResult = await hashArgon2id( const powResult = await argon2id({
stringToBytes(answer), // password hashLength: 64,
decodeCrock(questionSalt), // salt iterations: 3,
3, // iterations memorySize: 1024 /* kibibytes */,
1024, // memorySize (kibibytes) parallelism: 1,
64, // hashLength password: stringToBytes(answer),
); salt: decodeCrock(questionSalt),
outputType: "binary",
});
const kdfResult = kdfKw({ const kdfResult = kdfKw({
outputLength: 64, outputLength: 64,
salt: decodeCrock(truthUuid), salt: decodeCrock(truthUuid),

View File

@ -0,0 +1,2 @@
export * from "./index.js";
export { reducerCliMain } from "./cli.js";

View File

@ -2,11 +2,11 @@
"compileOnSave": true, "compileOnSave": true,
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"target": "ES2020", "target": "ES2018",
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Node16", "moduleResolution": "Node16",
"sourceMap": true, "sourceMap": true,
"lib": ["ES2020"], "lib": ["es6"],
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"strict": true, "strict": true,

View File

@ -136,7 +136,6 @@ export interface DiscoveryUiState {
export interface AnastasisReducerApi { export interface AnastasisReducerApi {
currentReducerState: ReducerState | undefined; currentReducerState: ReducerState | undefined;
// FIXME: Explain better!
currentError: any; currentError: any;
discoveryState: DiscoveryUiState; discoveryState: DiscoveryUiState;
dismissError: () => void; dismissError: () => void;

View File

@ -46,9 +46,8 @@ export function ContinentSelectionScreen(): VNode {
// const cc = reducer.currentReducerState.selected_country || ""; // const cc = reducer.currentReducerState.selected_country || "";
const theCountry = countryList.find((c) => c.code === countryCode); const theCountry = countryList.find((c) => c.code === countryCode);
const selectCountryAction = async () => { const selectCountryAction = async () => {
// selection should be when the select box changes it value //selection should be when the select box changes it value
if (!theCountry) return; if (!theCountry) return;
// FIXME: Why is there no await?
reducer.transition("select_country", { reducer.transition("select_country", {
country_code: countryCode, country_code: countryCode,
}); });
@ -57,7 +56,6 @@ export function ContinentSelectionScreen(): VNode {
// const step1 = reducer.currentReducerState.backup_state === BackupStates.ContinentSelecting || // const step1 = reducer.currentReducerState.backup_state === BackupStates.ContinentSelecting ||
// reducer.currentReducerState.recovery_state === RecoveryStates.ContinentSelecting; // reducer.currentReducerState.recovery_state === RecoveryStates.ContinentSelecting;
// FIXME: i18n
const errors = !theCountry ? "Select a country" : undefined; const errors = !theCountry ? "Select a country" : undefined;
const handleBack = async () => { const handleBack = async () => {

View File

@ -228,8 +228,6 @@ function AnastasisClientImpl(): VNode {
return <StartScreen />; return <StartScreen />;
} }
// FIXME: Use switch statements here!
if ( if (
(state.reducer_type === "backup" && (state.reducer_type === "backup" &&
state.backup_state === BackupStates.ContinentSelecting) || state.backup_state === BackupStates.ContinentSelecting) ||

View File

@ -21,11 +21,9 @@ export function Loading(): VNode {
<div <div
class="columns is-centered is-vcentered" class="columns is-centered is-vcentered"
style={{ style={{
height: "calc(100% - 3rem)",
position: "absolute",
width: "100%", width: "100%",
height: "200px",
display: "flex",
margin: "auto",
justifyContent: "center",
}} }}
> >
<Spinner /> <Spinner />
@ -35,7 +33,7 @@ export function Loading(): VNode {
export function Spinner(): VNode { export function Spinner(): VNode {
return ( return (
<div class="lds-ring" style={{margin:"auto"}}> <div class="lds-ring">
<div /> <div />
<div /> <div />
<div /> <div />

View File

@ -14,8 +14,8 @@ msgstr ""
"Project-Id-Version: Taler Wallet\n" "Project-Id-Version: Taler Wallet\n"
"Report-Msgid-Bugs-To: taler@gnu.org\n" "Report-Msgid-Bugs-To: taler@gnu.org\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n" "POT-Creation-Date: 2016-11-23 00:00+0100\n"
"PO-Revision-Date: 2023-08-15 07:28+0000\n" "PO-Revision-Date: 2022-12-26 23:30+0000\n"
"Last-Translator: Krystian Baran <kiszkot@murena.io>\n" "Last-Translator: Stefan Kügel <skuegel@web.de>\n"
"Language-Team: Italian <https://weblate.taler.net/projects/gnu-taler/" "Language-Team: Italian <https://weblate.taler.net/projects/gnu-taler/"
"taler-bank-spa/it/>\n" "taler-bank-spa/it/>\n"
"Language: it\n" "Language: it\n"
@ -199,9 +199,9 @@ msgid "Amount to withdraw:"
msgstr "Somma da ritirare" msgstr "Somma da ritirare"
#: src/pages/home/WalletWithdrawForm.tsx:84 #: src/pages/home/WalletWithdrawForm.tsx:84
#, c-format #, fuzzy, c-format
msgid "Withdraw" msgid "Withdraw"
msgstr "Prelevare" msgstr "Conferma il ritiro"
#: src/pages/home/WalletWithdrawForm.tsx:128 #: src/pages/home/WalletWithdrawForm.tsx:128
#, fuzzy, c-format #, fuzzy, c-format
@ -231,12 +231,12 @@ msgstr "Trasferisci fondi a un altro conto di questa banca:"
#: src/pages/home/Transactions.tsx:69 #: src/pages/home/Transactions.tsx:69
#, c-format #, c-format
msgid "Date" msgid "Date"
msgstr "Data" msgstr ""
#: src/pages/home/Transactions.tsx:70 #: src/pages/home/Transactions.tsx:70
#, c-format #, c-format
msgid "Amount" msgid "Amount"
msgstr "Importo" msgstr "Somma"
#: src/pages/home/Transactions.tsx:71 #: src/pages/home/Transactions.tsx:71
#, c-format #, c-format
@ -246,7 +246,7 @@ msgstr "Controparte"
#: src/pages/home/Transactions.tsx:72 #: src/pages/home/Transactions.tsx:72
#, c-format #, c-format
msgid "Subject" msgid "Subject"
msgstr "Soggetto" msgstr "Causale"
#: src/pages/home/QrCodeSection.tsx:41 #: src/pages/home/QrCodeSection.tsx:41
#, fuzzy, c-format #, fuzzy, c-format

View File

@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { Amounts, HttpStatusCode, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util"; import { Amounts, HttpStatusCode, parsePaytoUri } from "@gnu-taler/taler-util";
import { import {
ErrorType, ErrorType,
HttpResponsePaginated, HttpResponsePaginated,
@ -27,7 +27,6 @@ import { useAccountDetails } from "../hooks/access.js";
import { LoginForm } from "./LoginForm.js"; import { LoginForm } from "./LoginForm.js";
import { PaymentOptions } from "./PaymentOptions.js"; import { PaymentOptions } from "./PaymentOptions.js";
import { notifyError } from "../hooks/notification.js"; import { notifyError } from "../hooks/notification.js";
import { useEffect, useState } from "preact/hooks";
interface Props { interface Props {
account: string; account: string;
@ -35,60 +34,6 @@ interface Props {
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>, error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
) => VNode; ) => VNode;
} }
export const CopyIcon = (): VNode => (
<svg height="16" viewBox="0 0 16 16" width="16">
<path
fill-rule="evenodd"
d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"
/>
<path
fill-rule="evenodd"
d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z"
/>
</svg>
);
export const CopiedIcon = (): VNode => (
<svg height="16" viewBox="0 0 16 16" width="16">
<path
fill-rule="evenodd"
d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"
/>
</svg>
);
function CopyButton({ getContent }: { getContent: () => string }): VNode {
const [copied, setCopied] = useState(false);
function copyText(): void {
navigator.clipboard.writeText(getContent() || "");
setCopied(true);
}
useEffect(() => {
if (copied) {
setTimeout(() => {
setCopied(false);
}, 1000);
}
}, [copied]);
if (!copied) {
return (
<button onClick={copyText} style={{width:32, height:32, fontSize: "initial"}}>
<CopyIcon />
</button>
);
}
return (
<div content="Copied" style={{display:"inline-block"}}>
<button disabled style={{width:32, height:32 , fontSize: "initial"}}>
<CopiedIcon />
</button>
</div>
);
}
/** /**
* Query account information and show QR code if there is pending withdrawal * Query account information and show QR code if there is pending withdrawal
*/ */
@ -121,6 +66,7 @@ export function AccountPage({ account, onLoadNotOk }: Props): VNode {
<div>Payto from server is not valid &quot;{data.paytoUri}&quot;</div> <div>Payto from server is not valid &quot;{data.paytoUri}&quot;</div>
); );
} }
const accountNumber = payto.iban;
const balanceIsDebit = data.balance.credit_debit_indicator == "debit"; const balanceIsDebit = data.balance.credit_debit_indicator == "debit";
const limit = balanceIsDebit const limit = balanceIsDebit
? Amounts.sub(debitThreshold, balance).amount ? Amounts.sub(debitThreshold, balance).amount
@ -130,7 +76,8 @@ export function AccountPage({ account, onLoadNotOk }: Props): VNode {
<div> <div>
<h1 class="nav welcome-text"> <h1 class="nav welcome-text">
<i18n.Translate> <i18n.Translate>
Welcome, {account} (<a href={stringifyPaytoUri(payto)}>{payto.iban}</a>)! <CopyButton getContent={() => stringifyPaytoUri(payto)} /> Welcome, {accountNumber ? `${account} (${accountNumber})` : account}
!
</i18n.Translate> </i18n.Translate>
</h1> </h1>
</div> </div>

View File

@ -84,11 +84,11 @@ export function HomePage({
export function WithdrawalOperationPage({ export function WithdrawalOperationPage({
operationId, operationId,
onLoadNotOk, onLoadNotOk,
onContinue, onAbort,
}: { }: {
operationId: string; operationId: string;
onLoadNotOk: () => void; onLoadNotOk: () => void;
onContinue: () => void; onAbort: () => void;
}): VNode { }): VNode {
//FIXME: libeufin sandbox should return show to create the integration api endpoint //FIXME: libeufin sandbox should return show to create the integration api endpoint
//or return withdrawal uri from response //or return withdrawal uri from response
@ -99,6 +99,12 @@ export function WithdrawalOperationPage({
const parsedUri = parseWithdrawUri(uri); const parsedUri = parseWithdrawUri(uri);
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const [settings, updateSettings] = useSettings();
function clearCurrentWithdrawal(): void {
updateSettings("currentWithdrawalOperationId", undefined);
onAbort();
}
if (!parsedUri) { if (!parsedUri) {
notifyError({ notifyError({
title: i18n.str`The Withdrawal URI is not valid: "${uri}"`, title: i18n.str`The Withdrawal URI is not valid: "${uri}"`,
@ -109,7 +115,10 @@ export function WithdrawalOperationPage({
return ( return (
<WithdrawalQRCode <WithdrawalQRCode
withdrawUri={parsedUri} withdrawUri={parsedUri}
onContinue={onContinue} onConfirmed={() => {
notifyInfo(i18n.str`Withdrawal confirmed!`);
}}
onAborted={clearCurrentWithdrawal}
onLoadNotOk={onLoadNotOk} onLoadNotOk={onLoadNotOk}
/> />
); );

View File

@ -40,7 +40,7 @@ export function Routing(): VNode {
component={({ wopid }: { wopid: string }) => ( component={({ wopid }: { wopid: string }) => (
<WithdrawalOperationPage <WithdrawalOperationPage
operationId={wopid} operationId={wopid}
onContinue={() => { onAbort={() => {
route("/account"); route("/account");
}} }}
onLoadNotOk={() => { onLoadNotOk={() => {

View File

@ -33,6 +33,7 @@ import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
const logger = new Logger("WithdrawalConfirmationQuestion"); const logger = new Logger("WithdrawalConfirmationQuestion");
interface Props { interface Props {
onConfirmed: () => void;
onAborted: () => void; onAborted: () => void;
withdrawUri: WithdrawUriResult; withdrawUri: WithdrawUriResult;
} }
@ -41,6 +42,7 @@ interface Props {
* Not providing a back button, only abort. * Not providing a back button, only abort.
*/ */
export function WithdrawalConfirmationQuestion({ export function WithdrawalConfirmationQuestion({
onConfirmed,
onAborted, onAborted,
withdrawUri, withdrawUri,
}: Props): VNode { }: Props): VNode {
@ -117,6 +119,7 @@ export function WithdrawalConfirmationQuestion({
await confirmWithdrawal( await confirmWithdrawal(
withdrawUri.withdrawalOperationId, withdrawUri.withdrawalOperationId,
); );
onConfirmed();
} catch (error) { } catch (error) {
if (error instanceof RequestError) { if (error instanceof RequestError) {
notifyError( notifyError(

View File

@ -24,7 +24,6 @@ import { Fragment, VNode, h } from "preact";
import { Loading } from "../components/Loading.js"; import { Loading } from "../components/Loading.js";
import { useWithdrawalDetails } from "../hooks/access.js"; import { useWithdrawalDetails } from "../hooks/access.js";
import { notifyInfo } from "../hooks/notification.js"; import { notifyInfo } from "../hooks/notification.js";
import { useSettings } from "../hooks/settings.js";
import { handleNotOkResult } from "./HomePage.js"; import { handleNotOkResult } from "./HomePage.js";
import { QrCodeSection } from "./QrCodeSection.js"; import { QrCodeSection } from "./QrCodeSection.js";
import { WithdrawalConfirmationQuestion } from "./WithdrawalConfirmationQuestion.js"; import { WithdrawalConfirmationQuestion } from "./WithdrawalConfirmationQuestion.js";
@ -33,7 +32,8 @@ const logger = new Logger("WithdrawalQRCode");
interface Props { interface Props {
withdrawUri: WithdrawUriResult; withdrawUri: WithdrawUriResult;
onContinue: () => void; onAborted: () => void;
onConfirmed: () => void;
onLoadNotOk: () => void; onLoadNotOk: () => void;
} }
/** /**
@ -43,14 +43,10 @@ interface Props {
*/ */
export function WithdrawalQRCode({ export function WithdrawalQRCode({
withdrawUri, withdrawUri,
onContinue, onConfirmed,
onAborted,
onLoadNotOk, onLoadNotOk,
}: Props): VNode { }: Props): VNode {
const [settings, updateSettings] = useSettings();
function clearCurrentWithdrawal(): void {
updateSettings("currentWithdrawalOperationId", undefined);
onContinue();
}
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const result = useWithdrawalDetails(withdrawUri.withdrawalOperationId); const result = useWithdrawalDetails(withdrawUri.withdrawalOperationId);
if (!result.ok) { if (!result.ok) {
@ -68,64 +64,13 @@ export function WithdrawalQRCode({
} }
const { data } = result; const { data } = result;
if (data.aborted) { logger.trace("withdrawal status", data);
return <section id="main" class="content"> if (data.aborted || data.confirmation_done) {
<h1 class="nav">{i18n.str`Operation aborted`}</h1> // signal that this withdrawal is aborted
<section> // will redirect to account info
<p> notifyInfo(i18n.str`Operation completed`);
<i18n.Translate> onAborted();
The wire transfer to the GNU Taler Exchange bank's account was aborted, your balance return <Loading />;
was not affected.
</i18n.Translate>
</p>
<p>
<i18n.Translate>
You can close this page now or continue to the account page.
</i18n.Translate>
</p>
<a class="pure-button pure-button-primary"
style={{float:"right"}}
onClick={async (e) => {
e.preventDefault();
clearCurrentWithdrawal()
onContinue()
}}>
{i18n.str`Continue`}
</a>
</section>
</section>
}
if (data.confirmation_done) {
return <section id="main" class="content">
<h1 class="nav">{i18n.str`Operation completed`}</h1>
<section id="assets" style={{maxWidth: 400, marginLeft: "auto", marginRight:"auto"}}>
<p>
<i18n.Translate>
The wire transfer to the GNU Taler Exchange bank's account is completed, now the
exchange will send the requested amount into your GNU Taler wallet.
</i18n.Translate>
</p>
<p>
<i18n.Translate>
You can close this page now or continue to the account page.
</i18n.Translate>
</p>
<div style={{textAlign:"center"}}>
<a class="pure-button pure-button-primary"
onClick={async (e) => {
e.preventDefault();
clearCurrentWithdrawal()
onContinue()
}}>
{i18n.str`Continue`}
</a>
</div>
</section>
</section>
} }
if (!data.selection_done) { if (!data.selection_done) {
@ -134,21 +79,25 @@ export function WithdrawalQRCode({
withdrawUri={withdrawUri} withdrawUri={withdrawUri}
onAborted={() => { onAborted={() => {
notifyInfo(i18n.str`Operation canceled`); notifyInfo(i18n.str`Operation canceled`);
clearCurrentWithdrawal() onAborted();
onContinue() }}
}}
/> />
); );
} }
// Wallet POSTed the withdrawal details! Ask the
// user to authorize the operation (here CAPTCHA).
return ( return (
<WithdrawalConfirmationQuestion <WithdrawalConfirmationQuestion
withdrawUri={withdrawUri} withdrawUri={withdrawUri}
onConfirmed={() => {
notifyInfo(i18n.str`Operation confirmed`);
onConfirmed();
}}
onAborted={() => { onAborted={() => {
notifyInfo(i18n.str`Operation canceled`); notifyInfo(i18n.str`Operation canceled`);
clearCurrentWithdrawal() onAborted();
onContinue() }}
}}
/> />
); );
} }

View File

@ -314,40 +314,4 @@ h1.nav {
[name=wire-transfer-form] > input { [name=wire-transfer-form] > input {
margin-bottom: 1em; margin-bottom: 1em;
} }
.lds-ring {
display: inline-block;
position: relative;
width: 80px;
height: 80px;
}
.lds-ring div {
box-sizing: border-box;
display: block;
position: absolute;
width: 64px;
height: 64px;
margin: 8px;
border: 8px solid black;
border-radius: 50%;
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: black transparent transparent transparent;
}
.lds-ring div:nth-child(1) {
animation-delay: -0.45s;
}
.lds-ring div:nth-child(2) {
animation-delay: -0.3s;
}
.lds-ring div:nth-child(3) {
animation-delay: -0.15s;
}
@keyframes lds-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@ -134,7 +134,7 @@ export function buildRequestErrorMessage(
specialCases.onClientError && specialCases.onClientError(cause.status); specialCases.onClientError && specialCases.onClientError(cause.status);
result = { result = {
title: title ? title : i18n.str`The server didn't accept the request`, title: title ? title : i18n.str`The server didn't accept the request`,
description: cause?.payload?.error?.description, description: cause.payload.error.description,
debug: JSON.stringify(cause), debug: JSON.stringify(cause),
}; };
break; break;
@ -146,7 +146,7 @@ export function buildRequestErrorMessage(
title: title title: title
? title ? title
: i18n.str`The server had problems processing the request`, : i18n.str`The server had problems processing the request`,
description: cause?.payload?.error?.description, description: cause.payload.error.description,
debug: JSON.stringify(cause), debug: JSON.stringify(cause),
}; };
break; break;
@ -154,7 +154,7 @@ export function buildRequestErrorMessage(
case ErrorType.UNREADABLE: { case ErrorType.UNREADABLE: {
result = { result = {
title: i18n.str`Unexpected error`, title: i18n.str`Unexpected error`,
description: `Response from ${cause?.info?.url} is unreadable, status: ${cause?.status}`, description: `Response from ${cause.info?.url} is unreadable, status: ${cause.status}`,
debug: JSON.stringify(cause), debug: JSON.stringify(cause),
}; };
break; break;

View File

@ -26,7 +26,7 @@ import {
} from "@gnu-taler/web-util/browser"; } from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact"; import { Fragment, h, VNode } from "preact";
import { route } from "preact-router"; import { route } from "preact-router";
import { useMemo, useState } from "preact/hooks"; import { useMemo } from "preact/hooks";
import { ApplicationReadyRoutes } from "./ApplicationReadyRoutes.js"; import { ApplicationReadyRoutes } from "./ApplicationReadyRoutes.js";
import { Loading } from "./components/exception/loading.js"; import { Loading } from "./components/exception/loading.js";
import { import {
@ -42,7 +42,6 @@ import { useBackendConfig } from "./hooks/backend.js";
import { strings } from "./i18n/strings.js"; import { strings } from "./i18n/strings.js";
import LoginPage from "./paths/login/index.js"; import LoginPage from "./paths/login/index.js";
import { HttpStatusCode } from "@gnu-taler/taler-util"; import { HttpStatusCode } from "@gnu-taler/taler-util";
import { Settings } from "./paths/settings/index.js";
export function Application(): VNode { export function Application(): VNode {
return ( return (
@ -71,19 +70,10 @@ function ApplicationStatusRoutes(): VNode {
: { currency: "unknown", version: "unknown" }; : { currency: "unknown", version: "unknown" };
const ctx = useMemo(() => ({ currency, version }), [currency, version]); 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) { if (!triedToLog) {
return ( return (
<Fragment> <Fragment>
<NotYetReadyAppMenu title="Welcome!" onShowSettings={() => setShowSettings(true)} /> <NotYetReadyAppMenu title="Welcome!" />
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} /> <LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
</Fragment> </Fragment>
); );
@ -97,7 +87,7 @@ function ApplicationStatusRoutes(): VNode {
) { ) {
return ( return (
<Fragment> <Fragment>
<NotYetReadyAppMenu title="Login" onShowSettings={() => setShowSettings(true)} /> <NotYetReadyAppMenu title="Login" />
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} /> <LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
</Fragment> </Fragment>
); );
@ -108,7 +98,7 @@ function ApplicationStatusRoutes(): VNode {
) { ) {
return ( return (
<Fragment> <Fragment>
<NotYetReadyAppMenu title="Error" onShowSettings={() => setShowSettings(true)} /> <NotYetReadyAppMenu title="Error" />
<NotificationCard <NotificationCard
notification={{ notification={{
message: i18n.str`Server not found`, message: i18n.str`Server not found`,
@ -122,7 +112,7 @@ function ApplicationStatusRoutes(): VNode {
} }
if (result.type === ErrorType.SERVER) { if (result.type === ErrorType.SERVER) {
<Fragment> <Fragment>
<NotYetReadyAppMenu title="Error" onShowSettings={() => setShowSettings(true)} /> <NotYetReadyAppMenu title="Error" />
<NotificationCard <NotificationCard
notification={{ notification={{
message: i18n.str`Server response with an error code`, message: i18n.str`Server response with an error code`,
@ -135,7 +125,7 @@ function ApplicationStatusRoutes(): VNode {
} }
if (result.type === ErrorType.UNREADABLE) { if (result.type === ErrorType.UNREADABLE) {
<Fragment> <Fragment>
<NotYetReadyAppMenu title="Error" onShowSettings={() => setShowSettings(true)} /> <NotYetReadyAppMenu title="Error" />
<NotificationCard <NotificationCard
notification={{ notification={{
message: i18n.str`Response from server is unreadable, http status: ${result.status}`, message: i18n.str`Response from server is unreadable, http status: ${result.status}`,
@ -148,7 +138,7 @@ function ApplicationStatusRoutes(): VNode {
} }
return ( return (
<Fragment> <Fragment>
<NotYetReadyAppMenu title="Error" onShowSettings={() => setShowSettings(true)} /> <NotYetReadyAppMenu title="Error" />
<NotificationCard <NotificationCard
notification={{ notification={{
message: i18n.str`Unexpected Error`, message: i18n.str`Unexpected Error`,

View File

@ -33,7 +33,6 @@ import { InstanceRoutes } from "./InstanceRoutes.js";
import LoginPage from "./paths/login/index.js"; import LoginPage from "./paths/login/index.js";
import { INSTANCE_ID_LOOKUP } from "./utils/constants.js"; import { INSTANCE_ID_LOOKUP } from "./utils/constants.js";
import { HttpStatusCode } from "@gnu-taler/taler-util"; import { HttpStatusCode } from "@gnu-taler/taler-util";
import { Settings } from "./paths/settings/index.js";
export function ApplicationReadyRoutes(): VNode { export function ApplicationReadyRoutes(): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
@ -49,15 +48,8 @@ export function ApplicationReadyRoutes(): VNode {
clearAllTokens(); clearAllTokens();
route("/"); route("/");
}; };
const [showSettings, setShowSettings] = useState(false)
if (showSettings) { if (result.loading) return <NotYetReadyAppMenu title="Loading..." />;
return <Fragment>
<NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="UI Settings" onLogout={clearTokenAndGoToRoot} />
<Settings/>
</Fragment>
}
if (result.loading) return <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Loading..." />;
let admin = true; let admin = true;
let instanceNameByBackendURL; let instanceNameByBackendURL;
@ -69,7 +61,7 @@ export function ApplicationReadyRoutes(): VNode {
) { ) {
return ( return (
<Fragment> <Fragment>
<NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Login" onLogout={clearTokenAndGoToRoot} /> <NotYetReadyAppMenu title="Login" onLogout={clearTokenAndGoToRoot} />
<NotificationCard <NotificationCard
notification={{ notification={{
message: i18n.str`Access denied`, message: i18n.str`Access denied`,
@ -89,7 +81,7 @@ export function ApplicationReadyRoutes(): VNode {
// does not match our pattern // does not match our pattern
return ( return (
<Fragment> <Fragment>
<NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Error" onLogout={clearTokenAndGoToRoot} /> <NotYetReadyAppMenu title="Error" onLogout={clearTokenAndGoToRoot} />
<NotificationCard <NotificationCard
notification={{ notification={{
message: i18n.str`Couldn't access the server.`, message: i18n.str`Couldn't access the server.`,

View File

@ -68,7 +68,6 @@ import LoginPage from "./paths/login/index.js";
import NotFoundPage from "./paths/notfound/index.js"; import NotFoundPage from "./paths/notfound/index.js";
import { Notification } from "./utils/types.js"; import { Notification } from "./utils/types.js";
import { MerchantBackend } from "./declaration.js"; import { MerchantBackend } from "./declaration.js";
import { Settings } from "./paths/settings/index.js";
export enum InstancePaths { export enum InstancePaths {
// details = '/', // details = '/',
@ -101,8 +100,6 @@ export enum InstancePaths {
webhooks_list = "/webhooks", webhooks_list = "/webhooks",
webhooks_update = "/webhooks/:tid/update", webhooks_update = "/webhooks/:tid/update",
webhooks_new = "/webhooks/new", webhooks_new = "/webhooks/new",
settings = "/settings",
} }
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
@ -243,9 +240,6 @@ export function InstanceRoutes({
<Menu <Menu
instance={id} instance={id}
admin={admin} admin={admin}
onShowSettings={() => {
route("/settings")
}}
path={path} path={path}
onLogout={clearTokenAndGoToRoot} onLogout={clearTokenAndGoToRoot}
setInstanceName={setInstanceName} setInstanceName={setInstanceName}
@ -564,7 +558,6 @@ export function InstanceRoutes({
}} }}
/> />
<Route path={InstancePaths.kyc} component={ListKYCPage} /> <Route path={InstancePaths.kyc} component={ListKYCPage} />
<Route path={InstancePaths.settings} component={Settings} />
{/** {/**
* Example pages * Example pages
*/} */}

View File

@ -20,7 +20,7 @@
*/ */
import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { ComponentChildren, h, VNode } from "preact"; import { h, VNode } from "preact";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useBackendContext } from "../../context/backend.js"; import { useBackendContext } from "../../context/backend.js";
import { useInstanceContext } from "../../context/instance.js"; import { useInstanceContext } from "../../context/instance.js";
@ -40,7 +40,7 @@ function getTokenValuePart(t: string): string {
} }
function normalizeToken(r: string): string { function normalizeToken(r: string): string {
return `secret-token:${r}`; return `secret-token:${encodeURIComponent(r)}`;
} }
function cleanUp(s: string): string { function cleanUp(s: string): string {
@ -53,7 +53,7 @@ function cleanUp(s: string): string {
export function LoginModal({ onConfirm, withMessage }: Props): VNode { export function LoginModal({ onConfirm, withMessage }: Props): VNode {
const { url: backendUrl, token: baseToken } = useBackendContext(); const { url: backendUrl, token: baseToken } = useBackendContext();
const { admin, token: instanceToken, id } = useInstanceContext(); const { admin, token: instanceToken } = useInstanceContext();
const testLogin = useCredentialsChecker(); const testLogin = useCredentialsChecker();
const currentToken = getTokenValuePart( const currentToken = getTokenValuePart(
(!admin ? baseToken : instanceToken) ?? "", (!admin ? baseToken : instanceToken) ?? "",
@ -63,78 +63,6 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode {
const [url, setURL] = useState(cleanUp(backendUrl)); const [url, setURL] = useState(cleanUp(backendUrl));
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
if (admin && id !== "default") {
//admin trying to access another instance
return (<div class="columns is-centered" style={{ margin: "auto" }}>
<div class="column is-two-thirds ">
<div class="modal-card" style={{ width: "100%", margin: 0 }}>
<header
class="modal-card-head"
style={{ border: "1px solid", borderBottom: 0 }}
>
<p class="modal-card-title">{i18n.str`Login required`}</p>
</header>
<section
class="modal-card-body"
style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }}
>
<p>
<i18n.Translate>Need the access token for the instance.</i18n.Translate>
</p>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">
<i18n.Translate>Access Token</i18n.Translate>
</label>
</div>
<div class="field-body">
<div class="field">
<p class="control is-expanded">
<input
class="input"
type="password"
placeholder={"set new access token"}
name="token"
onKeyPress={(e) =>
e.keyCode === 13
? onConfirm(url, normalizeToken(token))
: null
}
value={token}
onInput={(e): void => setToken(e?.currentTarget.value)}
/>
</p>
</div>
</div>
</div>
</section>
<footer
class="modal-card-foot "
style={{
justifyContent: "flex-end",
border: "1px solid",
borderTop: 0,
}}
>
<AsyncButton
onClick={async () => {
const secretToken = normalizeToken(token);
const { valid, cause } = await testLogin(`${url}/instances/${id}`, secretToken);
if (valid) {
onConfirm(url, secretToken);
} else {
onConfirm(url);
}
}}
>
<i18n.Translate>Confirm</i18n.Translate>
</AsyncButton>
</footer>
</div>
</div>
</div>)
}
return ( return (
<div class="columns is-centered" style={{ margin: "auto" }}> <div class="columns is-centered" style={{ margin: "auto" }}>
<div class="column is-two-thirds "> <div class="column is-two-thirds ">
@ -209,7 +137,8 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode {
borderTop: 0, borderTop: 0,
}} }}
> >
<AsyncButton <button
class="button is-info"
onClick={async () => { onClick={async () => {
const secretToken = normalizeToken(token); const secretToken = normalizeToken(token);
const { valid, cause } = await testLogin(url, secretToken); const { valid, cause } = await testLogin(url, secretToken);
@ -221,24 +150,10 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode {
}} }}
> >
<i18n.Translate>Confirm</i18n.Translate> <i18n.Translate>Confirm</i18n.Translate>
</AsyncButton> </button>
</footer> </footer>
</div> </div>
</div> </div>
</div> </div>
); );
} }
function AsyncButton({ onClick, children }: { onClick: () => Promise<void>, children: ComponentChildren }): VNode {
const [running, setRunning] = useState(false)
return <button class="button is-info" disabled={running} onClick={() => {
setRunning(true)
onClick().then(() => {
setRunning(false)
}).catch(() => {
setRunning(false)
})
}}>
{children}
</button>
}

View File

@ -44,7 +44,7 @@ export function InputSelector<T>({
fromStr = defaultFromString, fromStr = defaultFromString,
toStr = defaultToString, toStr = defaultToString,
}: Props<keyof T>): VNode { }: Props<keyof T>): VNode {
const { error, value, onChange, required } = useField<T>(name); const { error, value, onChange } = useField<T>(name);
return ( return (
<div class="field is-horizontal"> <div class="field is-horizontal">
<div class="field-label is-normal"> <div class="field-label is-normal">
@ -58,8 +58,8 @@ export function InputSelector<T>({
</label> </label>
</div> </div>
<div class="field-body is-flex-grow-3"> <div class="field-body is-flex-grow-3">
<div class="field has-icons-right"> <div class="field">
<p class={expand ? "control is-expanded select" : "control select "}> <p class={expand ? "control is-expanded select" : "control select"}>
<select <select
class={error ? "select is-danger" : "select"} class={error ? "select is-danger" : "select"}
name={String(name)} name={String(name)}
@ -78,14 +78,8 @@ export function InputSelector<T>({
); );
})} })}
</select> </select>
{help} {help}
</p> </p>
{required && (
<span class="icon has-text-danger is-right" style={{height: "2.5em"}}>
<i class="mdi mdi-alert" />
</span>
)}
{error && <p class="help is-danger">{error}</p>} {error && <p class="help is-danger">{error}</p>}
</div> </div>
</div> </div>

View File

@ -1,91 +0,0 @@
/*
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 { InputProps, useField } from "./useField.js";
interface Props<T> extends InputProps<T> {
name: T;
readonly?: boolean;
expand?: boolean;
threeState?: boolean;
toBoolean?: (v?: any) => boolean | undefined;
fromBoolean?: (s: boolean | undefined) => any;
}
const defaultToBoolean = (f?: any): boolean | undefined => f || "";
const defaultFromBoolean = (v: boolean | undefined): any => v as any;
export function InputToggle<T>({
name,
readonly,
placeholder,
tooltip,
label,
help,
threeState,
expand,
fromBoolean = defaultFromBoolean,
toBoolean = defaultToBoolean,
}: Props<keyof T>): VNode {
const { error, value, onChange } = useField<T>(name);
const onCheckboxClick = (): void => {
const c = toBoolean(value);
if (c === false && threeState) return onChange(undefined as any);
return onChange(fromBoolean(!c));
};
return (
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label" style={{ width: 200 }}>
{label}
{tooltip && (
<span class="icon has-tooltip-right" data-tooltip={tooltip}>
<i class="mdi mdi-information" />
</span>
)}
</label>
</div>
<div class="field-body is-flex-grow-1">
<div class="field">
<p class={expand ? "control is-expanded" : "control"}>
<label class="toggle" style={{ marginLeft: 4, marginTop: 0 }}>
<input
type="checkbox"
class={toBoolean(value) === undefined ? "is-indeterminate" : "toggle-checkbox"}
checked={toBoolean(value)}
placeholder={placeholder}
readonly={readonly}
name={String(name)}
disabled={readonly}
onChange={onCheckboxClick}
/>
<div class="toggle-switch"></div>
</label>
{help}
</p>
{error && <p class="help is-danger">{error}</p>}
</div>
</div>
</div>
);
}

View File

@ -20,6 +20,7 @@
*/ */
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { LangSelector } from "./LangSelector.js";
import logo from "../../assets/logo-2021.svg"; import logo from "../../assets/logo-2021.svg";
interface Props { interface Props {
@ -64,6 +65,7 @@ export function NavigationBar({ onMobileMenu, title }: Props): VNode {
</a> </a>
<div class="navbar-end"> <div class="navbar-end">
<div class="navbar-item" style={{ paddingTop: 4, paddingBottom: 4 }}> <div class="navbar-item" style={{ paddingTop: 4, paddingBottom: 4 }}>
<LangSelector />
</div> </div>
</div> </div>
</div> </div>

View File

@ -31,7 +31,6 @@ const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
interface Props { interface Props {
onLogout: () => void; onLogout: () => void;
onShowSettings: () => void;
mobile?: boolean; mobile?: boolean;
instance: string; instance: string;
admin?: boolean; admin?: boolean;
@ -41,7 +40,6 @@ interface Props {
export function Sidebar({ export function Sidebar({
mobile, mobile,
instance, instance,
onShowSettings,
onLogout, onLogout,
admin, admin,
mimic, mimic,
@ -80,8 +78,21 @@ export function Sidebar({
<div class="menu is-menu-main"> <div class="menu is-menu-main">
{instance ? ( {instance ? (
<Fragment> <Fragment>
<p class="menu-label">
<i18n.Translate>Instance</i18n.Translate>
</p>
<ul class="menu-list"> <ul class="menu-list">
<li> <li>
<a href={"/update"} class="has-icon">
<span class="icon">
<i class="mdi mdi-square-edit-outline" />
</span>
<span class="menu-item-label">
<i18n.Translate>Settings</i18n.Translate>
</span>
</a>
</li>
<li>
<a href={"/orders"} class="has-icon"> <a href={"/orders"} class="has-icon">
<span class="icon"> <span class="icon">
<i class="mdi mdi-cash-register" /> <i class="mdi mdi-cash-register" />
@ -121,31 +132,6 @@ export function Sidebar({
</span> </span>
</a> </a>
</li> </li>
{needKYC && (
<li>
<a href={"/kyc"} class="has-icon">
<span class="icon">
<i class="mdi mdi-account-check" />
</span>
<span class="menu-item-label">KYC Status</span>
</a>
</li>
)}
</ul>
<p class="menu-label">
<i18n.Translate>Configuration</i18n.Translate>
</p>
<ul class="menu-list">
<li>
<a href={"/update"} class="has-icon">
<span class="icon">
<i class="mdi mdi-square-edit-outline" />
</span>
<span class="menu-item-label">
<i18n.Translate>Account</i18n.Translate>
</span>
</a>
</li>
<li> <li>
<a href={"/reserves"} class="has-icon"> <a href={"/reserves"} class="has-icon">
<span class="icon"> <span class="icon">
@ -164,6 +150,16 @@ export function Sidebar({
</span> </span>
</a> </a>
</li> </li>
{needKYC && (
<li>
<a href={"/kyc"} class="has-icon">
<span class="icon">
<i class="mdi mdi-account-check" />
</span>
<span class="menu-item-label">KYC Status</span>
</a>
</li>
)}
</ul> </ul>
</Fragment> </Fragment>
) : undefined} ) : undefined}
@ -171,18 +167,6 @@ export function Sidebar({
<i18n.Translate>Connection</i18n.Translate> <i18n.Translate>Connection</i18n.Translate>
</p> </p>
<ul class="menu-list"> <ul class="menu-list">
<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>
</span>
</a>
</li>
<li> <li>
<div> <div>
<span style={{ width: "3rem" }} class="icon"> <span style={{ width: "3rem" }} class="icon">

View File

@ -75,7 +75,6 @@ interface MenuProps {
instance: string; instance: string;
admin?: boolean; admin?: boolean;
onLogout?: () => void; onLogout?: () => void;
onShowSettings: () => void;
setInstanceName: (s: string) => void; setInstanceName: (s: string) => void;
} }
@ -94,7 +93,6 @@ function WithTitle({
export function Menu({ export function Menu({
onLogout, onLogout,
onShowSettings,
title, title,
instance, instance,
path, path,
@ -123,7 +121,6 @@ export function Menu({
{onLogout && ( {onLogout && (
<Sidebar <Sidebar
onShowSettings={onShowSettings}
onLogout={onLogout} onLogout={onLogout}
admin={admin} admin={admin}
mimic={mimic} mimic={mimic}
@ -133,12 +130,7 @@ export function Menu({
)} )}
{mimic && ( {mimic && (
<nav class="level" style={{ <nav class="level">
zIndex: 100,
position:"fixed",
width:"50%",
marginLeft: "20%"
}}>
<div class="level-item has-text-centered has-background-warning"> <div class="level-item has-text-centered has-background-warning">
<p class="is-size-5"> <p class="is-size-5">
You are viewing the instance <b>&quot;{instance}&quot;</b>.{" "} You are viewing the instance <b>&quot;{instance}&quot;</b>.{" "}
@ -162,7 +154,6 @@ export function Menu({
interface NotYetReadyAppMenuProps { interface NotYetReadyAppMenuProps {
title: string; title: string;
onLogout?: () => void; onLogout?: () => void;
onShowSettings: () => void;
} }
interface NotifProps { interface NotifProps {
@ -203,7 +194,6 @@ export function NotificationCard({
export function NotYetReadyAppMenu({ export function NotYetReadyAppMenu({
onLogout, onLogout,
onShowSettings,
title, title,
}: NotYetReadyAppMenuProps): VNode { }: NotYetReadyAppMenuProps): VNode {
const [mobileOpen, setMobileOpen] = useState(false); const [mobileOpen, setMobileOpen] = useState(false);
@ -222,7 +212,7 @@ export function NotYetReadyAppMenu({
title={title} title={title}
/> />
{onLogout && ( {onLogout && (
<Sidebar onShowSettings={onShowSettings} onLogout={onLogout} instance="" mobile={mobileOpen} /> <Sidebar onLogout={onLogout} instance="" mobile={mobileOpen} />
)} )}
</div> </div>
); );

View File

@ -1331,13 +1331,12 @@ export namespace MerchantBackend {
} }
namespace Webhooks { namespace Webhooks {
type MerchantWebhookType = "pay" | "refund";
interface WebhookAddDetails { interface WebhookAddDetails {
// Webhook ID to use. // Webhook ID to use.
webhook_id: string; webhook_id: string;
// The event of the webhook: why the webhook is used. // The event of the webhook: why the webhook is used.
event_type: MerchantWebhookType; event_type: string;
// URL of the webhook where the customer will be redirected. // URL of the webhook where the customer will be redirected.
url: string; url: string;

View File

@ -239,16 +239,16 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
searchDate?: Date, searchDate?: Date,
delta?: number, delta?: number,
): Promise<HttpResponseOk<T>> { ): Promise<HttpResponseOk<T>> {
const date_s = const date_ms =
delta && delta < 0 && searchDate delta && delta < 0 && searchDate
? (searchDate.getTime() / 1000) + 1 ? searchDate.getTime() + 1
: searchDate !== undefined ? (searchDate.getTime() / 1000) : undefined; : searchDate?.getTime();
const params: any = {}; const params: any = {};
if (paid !== undefined) params.paid = paid; if (paid !== undefined) params.paid = paid;
if (delta !== undefined) params.delta = delta; if (delta !== undefined) params.delta = delta;
if (refunded !== undefined) params.refunded = refunded; if (refunded !== undefined) params.refunded = refunded;
if (wired !== undefined) params.wired = wired; if (wired !== undefined) params.wired = wired;
if (date_s !== undefined) params.date_s = date_s; if (date_ms !== undefined) params.date_ms = date_ms;
return requestHandler<T>(baseUrl, endpoint, { params, token }); return requestHandler<T>(baseUrl, endpoint, { params, token });
}, },
[baseUrl, token], [baseUrl, token],

View File

@ -21,7 +21,6 @@
import { StateUpdater, useCallback, useState } from "preact/hooks"; import { StateUpdater, useCallback, useState } from "preact/hooks";
import { ValueOrFunction } from "../utils/types.js"; import { ValueOrFunction } from "../utils/types.js";
import { useMemoryStorage } from "@gnu-taler/web-util/browser";
const calculateRootPath = () => { const calculateRootPath = () => {
const rootPath = const rootPath =
@ -53,17 +52,14 @@ export function useBackendURL(
export function useBackendDefaultToken( export function useBackendDefaultToken(
initialValue?: string, initialValue?: string,
): [string | undefined, ((d: string | undefined) => void)] { ): [string | undefined, StateUpdater<string | undefined>] {
// uncomment for testing return useLocalStorage("backend-token", initialValue);
initialValue = "secret-token:secret" as string | undefined
const { update, value } = useMemoryStorage(`backend-token`, initialValue)
return [value, update];
} }
export function useBackendInstanceToken( export function useBackendInstanceToken(
id: string, id: string,
): [string | undefined, ((d: string | undefined) => void)] { ): [string | undefined, StateUpdater<string | undefined>] {
const { update: setToken, value: token, reset } = useMemoryStorage(`backend-token-${id}`) const [token, setToken] = useLocalStorage(`backend-token-${id}`);
const [defaultToken, defaultSetToken] = useBackendDefaultToken(); const [defaultToken, defaultSetToken] = useBackendDefaultToken();
// instance named 'default' use the default token // instance named 'default' use the default token
@ -71,16 +67,15 @@ export function useBackendInstanceToken(
return [defaultToken, defaultSetToken]; return [defaultToken, defaultSetToken];
} }
function updateToken( function updateToken(
value: (string | undefined) value:
| (string | undefined)
| ((s: string | undefined) => string | undefined),
): void { ): void {
console.log("seeting token", value) setToken((p) => {
if (value === undefined) { const toStore = value instanceof Function ? value(p) : value;
reset() return toStore;
} else { });
setToken(value)
}
} }
console.log("token", token)
return [token, updateToken]; return [token, updateToken];
} }

View File

@ -1,59 +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 { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
import {
Codec,
buildCodecForObject,
codecForBoolean,
} from "@gnu-taler/taler-util";
function parse_json_or_undefined<T>(str: string | undefined): T | undefined {
if (str === undefined) return undefined;
try {
return JSON.parse(str);
} catch {
return undefined;
}
}
export interface Settings {
advanceOrderMode: boolean
}
const defaultSettings: Settings = {
advanceOrderMode: false,
}
export const codecForSettings = (): Codec<Settings> =>
buildCodecForObject<Settings>()
.property("advanceOrderMode", codecForBoolean())
.build("Settings");
const SETTINGS_KEY = buildStorageKey("merchant-settings", codecForSettings());
export function useSettings(): [
Readonly<Settings>,
<T extends keyof Settings>(key: T, value: Settings[T]) => void,
] {
const { value, update } = useLocalStorage(SETTINGS_KEY);
const parsed: Settings = value ?? defaultSettings;
function updateField<T extends keyof Settings>(k: T, v: Settings[T]) {
update({ ...parsed, [k]: v });
}
return [parsed, updateField];
}

View File

@ -12,21 +12,20 @@
# You should have received a copy of the GNU General Public License along with # You should have received a copy of the GNU General Public License along with
# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> # TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
# #
#, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Taler Wallet\n" "Project-Id-Version: Taler Wallet\n"
"Report-Msgid-Bugs-To: taler@gnu.org\n" "Report-Msgid-Bugs-To: taler@gnu.org\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n" "POT-Creation-Date: 2016-11-23 00:00+0100\n"
"PO-Revision-Date: 2023-08-15 07:28+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Stefan Kügel <skuegel@web.de>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: German <https://weblate.taler.net/projects/gnu-taler/" "Language-Team: LANGUAGE <LL@li.org>\n"
"merchant-backoffice/de/>\n" "Language: \n"
"Language: de\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Weblate 4.13.1\n"
#: src/components/modal/index.tsx:71 #: src/components/modal/index.tsx:71
#, c-format #, c-format
@ -1253,7 +1252,7 @@ msgstr ""
#: src/paths/instance/orders/list/ListPage.tsx:145 #: src/paths/instance/orders/list/ListPage.tsx:145
#, c-format #, c-format
msgid "Refunded" msgid "Refunded"
msgstr "Rückerstattet" msgstr ""
#: src/paths/instance/orders/list/ListPage.tsx:152 #: src/paths/instance/orders/list/ListPage.tsx:152
#, c-format #, c-format

View File

@ -17,8 +17,8 @@ msgstr ""
"Project-Id-Version: Taler Wallet\n" "Project-Id-Version: Taler Wallet\n"
"Report-Msgid-Bugs-To: taler@gnu.org\n" "Report-Msgid-Bugs-To: taler@gnu.org\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n" "POT-Creation-Date: 2016-11-23 00:00+0100\n"
"PO-Revision-Date: 2023-08-13 10:14+0000\n" "PO-Revision-Date: 2023-04-24 06:43+0000\n"
"Last-Translator: Javier Sepulveda <javier.sepulveda@uv.es>\n" "Last-Translator: Stefan Kügel <skuegel@web.de>\n"
"Language-Team: Spanish <https://weblate.taler.net/projects/gnu-taler/" "Language-Team: Spanish <https://weblate.taler.net/projects/gnu-taler/"
"merchant-backoffice/es/>\n" "merchant-backoffice/es/>\n"
"Language: es\n" "Language: es\n"
@ -1273,7 +1273,7 @@ msgstr "No se pudo create el reembolso"
#: src/paths/instance/orders/list/ListPage.tsx:145 #: src/paths/instance/orders/list/ListPage.tsx:145
#, c-format #, c-format
msgid "Refunded" msgid "Refunded"
msgstr "Reembolsado" msgstr "Reembolzado"
#: src/paths/instance/orders/list/ListPage.tsx:152 #: src/paths/instance/orders/list/ListPage.tsx:152
#, c-format #, c-format

View File

@ -12,21 +12,20 @@
# You should have received a copy of the GNU General Public License along with # You should have received a copy of the GNU General Public License along with
# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> # TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
# #
#, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Taler Wallet\n" "Project-Id-Version: Taler Wallet\n"
"Report-Msgid-Bugs-To: taler@gnu.org\n" "Report-Msgid-Bugs-To: taler@gnu.org\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n" "POT-Creation-Date: 2016-11-23 00:00+0100\n"
"PO-Revision-Date: 2023-08-15 07:28+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Krystian Baran <kiszkot@murena.io>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Italian <https://weblate.taler.net/projects/gnu-taler/" "Language-Team: LANGUAGE <LL@li.org>\n"
"merchant-backoffice/it/>\n" "Language: \n"
"Language: it\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Weblate 4.13.1\n"
#: src/components/modal/index.tsx:71 #: src/components/modal/index.tsx:71
#, c-format #, c-format
@ -440,7 +439,7 @@ msgstr ""
#: src/components/form/InputTaxes.tsx:119 #: src/components/form/InputTaxes.tsx:119
#, c-format #, c-format
msgid "Amount" msgid "Amount"
msgstr "Importo" msgstr ""
#: src/components/form/InputTaxes.tsx:120 #: src/components/form/InputTaxes.tsx:120
#, c-format #, c-format
@ -888,7 +887,7 @@ msgstr ""
#: src/paths/instance/orders/list/Table.tsx:154 #: src/paths/instance/orders/list/Table.tsx:154
#, c-format #, c-format
msgid "Date" msgid "Date"
msgstr "Data" msgstr ""
#: src/paths/instance/orders/list/Table.tsx:200 #: src/paths/instance/orders/list/Table.tsx:200
#, c-format #, c-format
@ -1253,7 +1252,7 @@ msgstr ""
#: src/paths/instance/orders/list/ListPage.tsx:145 #: src/paths/instance/orders/list/ListPage.tsx:145
#, c-format #, c-format
msgid "Refunded" msgid "Refunded"
msgstr "Rimborsato" msgstr ""
#: src/paths/instance/orders/list/ListPage.tsx:152 #: src/paths/instance/orders/list/ListPage.tsx:152
#, c-format #, c-format
@ -1634,7 +1633,7 @@ msgstr ""
#: src/paths/instance/reserves/details/DetailPage.tsx:119 #: src/paths/instance/reserves/details/DetailPage.tsx:119
#, c-format #, c-format
msgid "Subject" msgid "Subject"
msgstr "Soggetto" msgstr ""
#: src/paths/instance/reserves/details/DetailPage.tsx:130 #: src/paths/instance/reserves/details/DetailPage.tsx:130
#, c-format #, c-format

View File

@ -43,7 +43,6 @@ import { Duration, MerchantBackend, WithId } from "../../../../declaration.js";
import { OrderCreateSchema as schema } from "../../../../schemas/index.js"; import { OrderCreateSchema as schema } from "../../../../schemas/index.js";
import { rate } from "../../../../utils/amount.js"; import { rate } from "../../../../utils/amount.js";
import { undefinedIfEmpty } from "../../../../utils/table.js"; import { undefinedIfEmpty } from "../../../../utils/table.js";
import { useSettings } from "../../../../hooks/useSettings.js";
interface Props { interface Props {
onCreate: (d: MerchantBackend.Orders.PostOrderRequest) => void; onCreate: (d: MerchantBackend.Orders.PostOrderRequest) => void;
@ -63,8 +62,8 @@ function with_defaults(config: InstanceConfig): Partial<Entity> {
!config.default_pay_delay || config.default_pay_delay.d_us === "forever" !config.default_pay_delay || config.default_pay_delay.d_us === "forever"
? undefined ? undefined
: add(new Date(), { : add(new Date(), {
seconds: config.default_pay_delay.d_us / (1000 * 1000), seconds: config.default_pay_delay.d_us / (1000 * 1000),
}); });
return { return {
inventoryProducts: {}, inventoryProducts: {},
@ -139,7 +138,7 @@ export function CreatePage({
const [value, valueHandler] = useState(with_defaults(instanceConfig)); const [value, valueHandler] = useState(with_defaults(instanceConfig));
const config = useConfigContext(); const config = useConfigContext();
const zero = Amounts.zeroOfCurrency(config.currency); const zero = Amounts.zeroOfCurrency(config.currency);
const [settings] = useSettings()
const inventoryList = Object.values(value.inventoryProducts || {}); const inventoryList = Object.values(value.inventoryProducts || {});
const productList = Object.values(value.products || {}); const productList = Object.values(value.products || {});
@ -155,10 +154,10 @@ export function CreatePage({
order_price: !value.pricing?.order_price order_price: !value.pricing?.order_price
? i18n.str`required` ? i18n.str`required`
: !parsedPrice : !parsedPrice
? i18n.str`not valid` ? i18n.str`not valid`
: Amounts.isZero(parsedPrice) : Amounts.isZero(parsedPrice)
? i18n.str`must be greater than 0` ? i18n.str`must be greater than 0`
: undefined, : undefined,
}), }),
extra: extra:
value.extra && !stringIsValidJSON(value.extra) value.extra && !stringIsValidJSON(value.extra)
@ -168,47 +167,47 @@ export function CreatePage({
refund_deadline: !value.payments?.refund_deadline refund_deadline: !value.payments?.refund_deadline
? undefined ? undefined
: !isFuture(value.payments.refund_deadline) : !isFuture(value.payments.refund_deadline)
? i18n.str`should be in the future` ? i18n.str`should be in the future`
: value.payments.pay_deadline && : value.payments.pay_deadline &&
isBefore(value.payments.refund_deadline, value.payments.pay_deadline) isBefore(value.payments.refund_deadline, value.payments.pay_deadline)
? i18n.str`refund deadline cannot be before pay deadline` ? i18n.str`refund deadline cannot be before pay deadline`
: value.payments.wire_transfer_deadline && : value.payments.wire_transfer_deadline &&
isBefore( isBefore(
value.payments.wire_transfer_deadline, value.payments.wire_transfer_deadline,
value.payments.refund_deadline, value.payments.refund_deadline,
) )
? i18n.str`wire transfer deadline cannot be before refund deadline` ? i18n.str`wire transfer deadline cannot be before refund deadline`
: undefined, : undefined,
pay_deadline: !value.payments?.pay_deadline pay_deadline: !value.payments?.pay_deadline
? undefined ? undefined
: !isFuture(value.payments.pay_deadline) : !isFuture(value.payments.pay_deadline)
? i18n.str`should be in the future` ? i18n.str`should be in the future`
: value.payments.wire_transfer_deadline && : value.payments.wire_transfer_deadline &&
isBefore( isBefore(
value.payments.wire_transfer_deadline, value.payments.wire_transfer_deadline,
value.payments.pay_deadline, value.payments.pay_deadline,
) )
? i18n.str`wire transfer deadline cannot be before pay deadline` ? i18n.str`wire transfer deadline cannot be before pay deadline`
: undefined, : undefined,
auto_refund_deadline: !value.payments?.auto_refund_deadline auto_refund_deadline: !value.payments?.auto_refund_deadline
? undefined ? undefined
: !isFuture(value.payments.auto_refund_deadline) : !isFuture(value.payments.auto_refund_deadline)
? i18n.str`should be in the future` ? i18n.str`should be in the future`
: !value.payments?.refund_deadline : !value.payments?.refund_deadline
? i18n.str`should have a refund deadline` ? i18n.str`should have a refund deadline`
: !isAfter( : !isAfter(
value.payments.refund_deadline, value.payments.refund_deadline,
value.payments.auto_refund_deadline, value.payments.auto_refund_deadline,
) )
? i18n.str`auto refund cannot be after refund deadline` ? i18n.str`auto refund cannot be after refund deadline`
: undefined, : undefined,
}), }),
shipping: undefinedIfEmpty({ shipping: undefinedIfEmpty({
delivery_date: !value.shipping?.delivery_date delivery_date: !value.shipping?.delivery_date
? undefined ? undefined
: !isFuture(value.shipping.delivery_date) : !isFuture(value.shipping.delivery_date)
? i18n.str`should be in the future` ? i18n.str`should be in the future`
: undefined, : undefined,
}), }),
}; };
const hasErrors = Object.keys(errors).some( const hasErrors = Object.keys(errors).some(
@ -228,27 +227,27 @@ export function CreatePage({
extra: value.extra, extra: value.extra,
pay_deadline: value.payments.pay_deadline pay_deadline: value.payments.pay_deadline
? { ? {
t_s: Math.floor(value.payments.pay_deadline.getTime() / 1000), t_s: Math.floor(value.payments.pay_deadline.getTime() / 1000),
} }
: undefined, : undefined,
wire_transfer_deadline: value.payments.wire_transfer_deadline wire_transfer_deadline: value.payments.wire_transfer_deadline
? { ? {
t_s: Math.floor( t_s: Math.floor(
value.payments.wire_transfer_deadline.getTime() / 1000, value.payments.wire_transfer_deadline.getTime() / 1000,
), ),
} }
: undefined, : undefined,
refund_deadline: value.payments.refund_deadline refund_deadline: value.payments.refund_deadline
? { ? {
t_s: Math.floor(value.payments.refund_deadline.getTime() / 1000), t_s: Math.floor(value.payments.refund_deadline.getTime() / 1000),
} }
: undefined, : undefined,
auto_refund: value.payments.auto_refund_deadline auto_refund: value.payments.auto_refund_deadline
? { ? {
d_us: Math.floor( d_us: Math.floor(
value.payments.auto_refund_deadline.getTime() * 1000, value.payments.auto_refund_deadline.getTime() * 1000,
), ),
} }
: undefined, : undefined,
wire_fee_amortization: value.payments.wire_fee_amortization as number, wire_fee_amortization: value.payments.wire_fee_amortization as number,
max_fee: value.payments.max_fee as string, max_fee: value.payments.max_fee as string,
@ -375,15 +374,13 @@ export function CreatePage({
inventory={instanceInventory} inventory={instanceInventory}
/> />
{settings.advanceOrderMode && <NonInventoryProductFrom
<NonInventoryProductFrom productToEdit={editingProduct}
productToEdit={editingProduct} onAddProduct={(p) => {
onAddProduct={(p) => { setEditingProduct(undefined);
setEditingProduct(undefined); return addNewProduct(p);
return addNewProduct(p); }}
}} />
/>
}
{allProducts.length > 0 && ( {allProducts.length > 0 && (
<ProductList <ProductList
@ -426,8 +423,8 @@ export function CreatePage({
discountOrRise > 0 && discountOrRise > 0 &&
(discountOrRise < 1 (discountOrRise < 1
? `discount of %${Math.round( ? `discount of %${Math.round(
(1 - discountOrRise) * 100, (1 - discountOrRise) * 100,
)}` )}`
: `rise of %${Math.round((discountOrRise - 1) * 100)}`) : `rise of %${Math.round((discountOrRise - 1) * 100)}`)
} }
tooltip={i18n.str`Amount to be paid by the customer`} tooltip={i18n.str`Amount to be paid by the customer`}
@ -448,108 +445,102 @@ export function CreatePage({
tooltip={i18n.str`Title of the order to be shown to the customer`} tooltip={i18n.str`Title of the order to be shown to the customer`}
/> />
{settings.advanceOrderMode && <InputGroup
<InputGroup name="shipping"
name="shipping" label={i18n.str`Shipping and Fulfillment`}
label={i18n.str`Shipping and Fulfillment`} initialActive
initialActive >
> <InputDate
<InputDate name="shipping.delivery_date"
name="shipping.delivery_date" label={i18n.str`Delivery date`}
label={i18n.str`Delivery date`} tooltip={i18n.str`Deadline for physical delivery assured by the merchant.`}
tooltip={i18n.str`Deadline for physical delivery assured by the merchant.`} />
/> {value.shipping?.delivery_date && (
{value.shipping?.delivery_date && ( <InputGroup
<InputGroup name="shipping.delivery_location"
name="shipping.delivery_location" label={i18n.str`Location`}
label={i18n.str`Location`} tooltip={i18n.str`address where the products will be delivered`}
tooltip={i18n.str`address where the products will be delivered`} >
> <InputLocation name="shipping.delivery_location" />
<InputLocation name="shipping.delivery_location" /> </InputGroup>
</InputGroup> )}
)} <Input
<Input name="shipping.fullfilment_url"
name="shipping.fullfilment_url" label={i18n.str`Fulfillment URL`}
label={i18n.str`Fulfillment URL`} tooltip={i18n.str`URL to which the user will be redirected after successful payment.`}
tooltip={i18n.str`URL to which the user will be redirected after successful payment.`} />
/> </InputGroup>
</InputGroup>
}
{settings.advanceOrderMode && <InputGroup
<InputGroup name="payments"
name="payments" label={i18n.str`Taler payment options`}
label={i18n.str`Taler payment options`} tooltip={i18n.str`Override default Taler payment settings for this order`}
tooltip={i18n.str`Override default Taler payment settings for this order`} >
> <InputDate
<InputDate name="payments.pay_deadline"
name="payments.pay_deadline" label={i18n.str`Payment 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.`}
tooltip={i18n.str`Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.`} />
/> <InputDate
<InputDate name="payments.refund_deadline"
name="payments.refund_deadline" label={i18n.str`Refund deadline`}
label={i18n.str`Refund deadline`} tooltip={i18n.str`Time until which the order can be refunded by the merchant.`}
tooltip={i18n.str`Time until which the order can be refunded by the merchant.`} />
/> <InputDate
<InputDate name="payments.wire_transfer_deadline"
name="payments.wire_transfer_deadline" label={i18n.str`Wire transfer deadline`}
label={i18n.str`Wire transfer deadline`} tooltip={i18n.str`Deadline for the exchange to make the wire transfer.`}
tooltip={i18n.str`Deadline for the exchange to make the wire transfer.`} />
/> <InputDate
<InputDate name="payments.auto_refund_deadline"
name="payments.auto_refund_deadline" label={i18n.str`Auto-refund deadline`}
label={i18n.str`Auto-refund deadline`} tooltip={i18n.str`Time until which the wallet will automatically check for refunds without user interaction.`}
tooltip={i18n.str`Time until which the wallet will automatically check for refunds without user interaction.`} />
/>
<InputCurrency <InputCurrency
name="payments.max_fee" name="payments.max_fee"
label={i18n.str`Maximum deposit 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.`} 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.`}
/> />
<InputCurrency <InputCurrency
name="payments.max_wire_fee" name="payments.max_wire_fee"
label={i18n.str`Maximum 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.`} 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 <InputNumber
name="payments.wire_fee_amortization" name="payments.wire_fee_amortization"
label={i18n.str`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.`} 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 <InputBoolean
name="payments.createToken" name="payments.createToken"
label={i18n.str`Create token`} 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`Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.`}
/> />
<InputNumber <InputNumber
name="payments.minimum_age" name="payments.minimum_age"
label={i18n.str`Minimum age required`} label={i18n.str`Minimum age required`}
tooltip={i18n.str`Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products`} tooltip={i18n.str`Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products`}
help={ help={
minAgeByProducts > 0 minAgeByProducts > 0
? i18n.str`Min age defined by the producs is ${minAgeByProducts}` ? i18n.str`Min age defined by the producs is ${minAgeByProducts}`
: undefined : undefined
} }
/> />
</InputGroup> </InputGroup>
}
{settings.advanceOrderMode && <InputGroup
<InputGroup name="extra"
label={i18n.str`Additional information`}
tooltip={i18n.str`Custom information to be included in the contract for this order.`}
>
<Input
name="extra" name="extra"
label={i18n.str`Additional information`} inputType="multiline"
tooltip={i18n.str`Custom information to be included in the contract for this order.`} label={`Value`}
> tooltip={i18n.str`You must enter a value in JavaScript Object Notation (JSON).`}
<Input />
name="extra" </InputGroup>
inputType="multiline"
label={`Value`}
tooltip={i18n.str`You must enter a value in JavaScript Object Notation (JSON).`}
/>
</InputGroup>
}
</FormProvider> </FormProvider>
<div class="buttons is-right mt-5"> <div class="buttons is-right mt-5">

View File

@ -21,7 +21,7 @@
import { AmountJson, Amounts } from "@gnu-taler/taler-util"; import { AmountJson, Amounts } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { format, formatDistance } from "date-fns"; import { format } from "date-fns";
import { Fragment, h, VNode } from "preact"; import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { FormProvider } from "../../../../components/form/FormProvider.js"; import { FormProvider } from "../../../../components/form/FormProvider.js";
@ -223,7 +223,6 @@ function ClaimedPage({
</div> </div>
</div> </div>
</div> </div>
<div class="level"> <div class="level">
<div class="level-left"> <div class="level-left">
<div class="level-item"> <div class="level-item">
@ -420,11 +419,6 @@ function PaidPage({
} }
} }
const now = new Date()
const nextEvent = events.find((e) => {
return e.when.getTime() > now.getTime()
})
const [value, valueHandler] = useState<Partial<Paid>>(order); const [value, valueHandler] = useState<Partial<Paid>>(order);
const { url } = useBackendContext(); const { url } = useBackendContext();
const refundHost = url.replace(/.*:\/\//, ""); // remove protocol part const refundHost = url.replace(/.*:\/\//, ""); // remove protocol part
@ -510,13 +504,22 @@ function PaidPage({
whiteSpace: "nowrap", whiteSpace: "nowrap",
overflow: "hidden", overflow: "hidden",
textOverflow: "ellipsis", textOverflow: "ellipsis",
// maxWidth: '100%',
}} }}
> >
<p> <p>
<i18n.Translate>Next event in </i18n.Translate> {formatDistance( <a
nextEvent!.when, href={order.contract_terms.fulfillment_url}
new Date(), rel="nofollow"
// "yyyy/MM/dd HH:mm:ss", target="new"
>
{order.contract_terms.fulfillment_url}
</a>
</p>
<p>
{format(
new Date(order.contract_terms.timestamp.t_s * 1000),
"yyyy/MM/dd HH:mm:ss",
)} )}
</p> </p>
</div> </div>
@ -665,9 +668,9 @@ function UnpaidPage({
{order.creation_time.t_s === "never" {order.creation_time.t_s === "never"
? "never" ? "never"
: format( : format(
new Date(order.creation_time.t_s * 1000), new Date(order.creation_time.t_s * 1000),
"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm:ss",
)} )}
</p> </p>
</div> </div>
</div> </div>

View File

@ -67,7 +67,7 @@ export function Timeline({ events: e }: Props) {
); );
case "start": case "start":
return ( return (
<div class="timeline-marker is-icon"> <div class="timeline-marker is-icon is-success">
<i class="mdi mdi-flag " /> <i class="mdi mdi-flag " />
</div> </div>
); );
@ -104,7 +104,7 @@ export function Timeline({ events: e }: Props) {
} }
})()} })()}
<div class="timeline-content"> <div class="timeline-content">
{e.description !== "now" && <p class="heading">{format(e.when, "yyyy/MM/dd HH:mm:ss")}</p>} <p class="heading">{format(e.when, "yyyy/MM/dd HH:mm:ss")}</p>
<p>{e.description}</p> <p>{e.description}</p>
</div> </div>
</div> </div>
@ -117,12 +117,12 @@ export interface Event {
when: Date; when: Date;
description: string; description: string;
type: type:
| "start" | "start"
| "refund" | "refund"
| "refund-taken" | "refund-taken"
| "wired" | "wired"
| "wired-range" | "wired-range"
| "deadline" | "deadline"
| "delivery" | "delivery"
| "now"; | "now";
} }

View File

@ -164,7 +164,7 @@ export function ListPage({
<div class="field has-addons"> <div class="field has-addons">
{jumpToDate && ( {jumpToDate && (
<div class="control"> <div class="control">
<a class="button is-fullwidth" onClick={() => onSelectDate(undefined)}> <a class="button" onClick={() => onSelectDate(undefined)}>
<span <span
class="icon" class="icon"
data-tooltip={i18n.str`clear date filter`} data-tooltip={i18n.str`clear date filter`}
@ -191,7 +191,7 @@ export function ListPage({
<div class="control"> <div class="control">
<span class="has-tooltip-left" data-tooltip={dateTooltip}> <span class="has-tooltip-left" data-tooltip={dateTooltip}>
<a <a
class="button is-fullwidth" class="button"
onClick={() => { onClick={() => {
setPickDate(true); setPickDate(true);
}} }}

View File

@ -85,34 +85,34 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
template_contract: !state.template_contract template_contract: !state.template_contract
? undefined ? undefined
: undefinedIfEmpty({ : undefinedIfEmpty({
amount: !state.template_contract?.amount amount: !state.template_contract?.amount
? undefined ? undefined
: !parsedPrice : !parsedPrice
? i18n.str`not valid` ? i18n.str`not valid`
: Amounts.isZero(parsedPrice) : Amounts.isZero(parsedPrice)
? i18n.str`must be greater than 0` ? i18n.str`must be greater than 0`
: undefined,
minimum_age:
state.template_contract.minimum_age < 0
? i18n.str`should be greater that 0`
: undefined, : undefined,
pay_duration: !state.template_contract.pay_duration minimum_age:
? i18n.str`can't be empty` state.template_contract.minimum_age < 0
: state.template_contract.pay_duration.d_us === "forever" ? 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 ? undefined
: state.template_contract.pay_duration.d_us < 1000 * 1000 //less than one second : state.template_contract.pay_duration.d_us < 1000 * 1000 //less than one second
? i18n.str`to short` ? i18n.str`to short`
: undefined, : undefined,
} as Partial<MerchantTemplateContractDetails>), } as Partial<MerchantTemplateContractDetails>),
pos_key: !state.pos_key pos_key: !state.pos_key
? !state.pos_algorithm ? !state.pos_algorithm
? undefined ? undefined
: i18n.str`required` : i18n.str`required`
: !isBase32RFC3548Charset(state.pos_key) : !isBase32RFC3548Charset(state.pos_key)
? i18n.str`just letters and numbers from 2 to 7` ? i18n.str`just letters and numbers from 2 to 7`
: state.pos_key.length !== 32 : state.pos_key.length !== 32
? i18n.str`size of the key should be 32` ? i18n.str`size of the key should be 32`
: undefined, : undefined,
}; };
const hasErrors = Object.keys(errors).some( const hasErrors = Object.keys(errors).some(
@ -139,7 +139,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
> >
<InputWithAddon<Entity> <InputWithAddon<Entity>
name="template_id" name="template_id"
help={`${backend.url}/instances/templates/${state.template_id ?? ""}`} addonBefore={`${backend.url}/instances/templates/`}
label={i18n.str`Identifier`} label={i18n.str`Identifier`}
tooltip={i18n.str`Name of the template in URLs.`} tooltip={i18n.str`Name of the template in URLs.`}
/> />

View File

@ -34,7 +34,6 @@ import { useBackendContext } from "../../../../context/backend.js";
import { useConfigContext } from "../../../../context/config.js"; import { useConfigContext } from "../../../../context/config.js";
import { useInstanceContext } from "../../../../context/instance.js"; import { useInstanceContext } from "../../../../context/instance.js";
import { MerchantBackend } from "../../../../declaration.js"; import { MerchantBackend } from "../../../../declaration.js";
import { stringifyPayTemplateUri } from "@gnu-taler/taler-util";
type Entity = MerchantBackend.Template.UsingTemplateDetails; type Entity = MerchantBackend.Template.UsingTemplateDetails;
@ -65,47 +64,46 @@ export function QrPage({ template, id: templateId, onBack }: Props): VNode {
const fixedAmount = !!template.template_contract.amount; const fixedAmount = !!template.template_contract.amount;
const fixedSummary = !!template.template_contract.summary; const fixedSummary = !!template.template_contract.summary;
const templateParams: Record<string, string> = {} const params = new URLSearchParams();
if (!fixedAmount) { if (!fixedAmount) {
if (state.amount) { if (state.amount) {
templateParams.amount = state.amount params.append("amount", state.amount);
} else { } else {
templateParams.amount = config.currency params.append("amount", config.currency);
} }
} }
if (!fixedSummary) { if (!fixedSummary) {
templateParams.summary = state.summary ?? "" params.append("summary", state.summary ?? "");
} }
const merchantBaseUrl = new URL(backendUrl).href; const paramsStr = fixedAmount && fixedSummary ? "" : "?" + params.toString();
const merchantURL = new URL(backendUrl);
const payTemplateUri = stringifyPayTemplateUri({ const talerProto =
merchantBaseUrl, merchantURL.protocol === "http:" ? "taler+http:" : "taler:";
templateId,
templateParams const payTemplateUri = `${talerProto}//pay-template/${merchantURL.hostname}/${templateId}${paramsStr}`;
})
const issuer = encodeURIComponent( const issuer = encodeURIComponent(
`${new URL(backendUrl).host}/${instanceId}`, `${new URL(backendUrl).hostname}/${instanceId}`,
); );
const oauthUri = !template.pos_algorithm const oauthUri = !template.pos_algorithm
? undefined ? undefined
: template.pos_algorithm === 1 : template.pos_algorithm === 1
? `otpauth://totp/${issuer}:${templateId}?secret=${template.pos_key}&issuer=${issuer}&algorithm=SHA1&digits=8&period=30` ? `otpauth://totp/${issuer}:${templateId}?secret=${template.pos_key}&issuer=${issuer}&algorithm=SHA1&digits=8&period=30`
: template.pos_algorithm === 2 : template.pos_algorithm === 2
? `otpauth://totp/${issuer}:${templateId}?secret=${template.pos_key}&issuer=${issuer}&algorithm=SHA1&digits=8&period=30` ? `otpauth://totp/${issuer}:${templateId}?secret=${template.pos_key}&issuer=${issuer}&algorithm=SHA1&digits=8&period=30`
: undefined; : undefined;
const keySlice = template.pos_key?.substring(0, 4); const keySlice = template.pos_key?.substring(0, 4);
const oauthUriWithoutSecret = !template.pos_algorithm const oauthUriWithoutSecret = !template.pos_algorithm
? undefined ? undefined
: template.pos_algorithm === 1 : template.pos_algorithm === 1
? `otpauth://totp/${issuer}:${templateId}?secret=${keySlice}...&issuer=${issuer}&algorithm=SHA1&digits=8&period=30` ? `otpauth://totp/${issuer}:${templateId}?secret=${keySlice}...&issuer=${issuer}&algorithm=SHA1&digits=8&period=30`
: template.pos_algorithm === 2 : template.pos_algorithm === 2
? `otpauth://totp/${issuer}:${templateId}?secret=${keySlice}...&issuer=${issuer}&algorithm=SHA1&digits=8&period=30` ? `otpauth://totp/${issuer}:${templateId}?secret=${keySlice}...&issuer=${issuer}&algorithm=SHA1&digits=8&period=30`
: undefined; : undefined;
return ( return (
<div> <div>
{oauthUri && ( {oauthUri && (

View File

@ -33,7 +33,6 @@ import { InputDuration } from "../../../../components/form/InputDuration.js";
import { InputNumber } from "../../../../components/form/InputNumber.js"; import { InputNumber } from "../../../../components/form/InputNumber.js";
import { useBackendContext } from "../../../../context/backend.js"; import { useBackendContext } from "../../../../context/backend.js";
import { MerchantBackend } from "../../../../declaration.js"; import { MerchantBackend } from "../../../../declaration.js";
import { InputSelector } from "../../../../components/form/InputSelector.js";
type Entity = MerchantBackend.Webhooks.WebhookAddDetails; type Entity = MerchantBackend.Webhooks.WebhookAddDetails;
@ -51,9 +50,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
const errors: FormErrors<Entity> = { const errors: FormErrors<Entity> = {
webhook_id: !state.webhook_id ? i18n.str`required` : undefined, webhook_id: !state.webhook_id ? i18n.str`required` : undefined,
event_type: !state.event_type ? i18n.str`required` event_type: !state.event_type ? i18n.str`required` : undefined,
: state.event_type !== "pay" && state.event_type !== "refund" ? i18n.str`it should be "pay" or "refund"`
: undefined,
http_method: !state.http_method http_method: !state.http_method
? i18n.str`required` ? i18n.str`required`
: !validMethod.includes(state.http_method) : !validMethod.includes(state.http_method)
@ -87,30 +84,16 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
label={i18n.str`ID`} label={i18n.str`ID`}
tooltip={i18n.str`Webhook ID to use`} tooltip={i18n.str`Webhook ID to use`}
/> />
<InputSelector <Input<Entity>
name="event_type" name="event_type"
label={i18n.str`Event`} label={i18n.str`Event`}
values={[
i18n.str`Choose one...`,
i18n.str`pay`,
i18n.str`refund`,
]}
tooltip={i18n.str`The event of the webhook: why the webhook is used`} tooltip={i18n.str`The event of the webhook: why the webhook is used`}
/> />
<InputSelector <Input<Entity>
name="http_method" name="http_method"
label={i18n.str`Method`} label={i18n.str`Method`}
values={[
i18n.str`Choose one...`,
i18n.str`GET`,
i18n.str`POST`,
i18n.str`PUT`,
i18n.str`PATCH`,
i18n.str`HEAD`,
]}
tooltip={i18n.str`Method used by the webhook`} tooltip={i18n.str`Method used by the webhook`}
/> />
<Input<Entity> <Input<Entity>
name="url" name="url"
label={i18n.str`URL`} label={i18n.str`URL`}

View File

@ -25,6 +25,7 @@ import { Link } from "preact-router";
export default function NotFoundPage(): VNode { export default function NotFoundPage(): VNode {
return ( return (
<div> <div>
<h1>Error 404</h1>
<p>That page doesn&apos;t exist.</p> <p>That page doesn&apos;t exist.</p>
<Link href="/"> <Link href="/">
<h4>Back to Home</h4> <h4>Back to Home</h4>

View File

@ -1,77 +0,0 @@
import { VNode, h } from "preact";
import { LangSelector } from "../../components/menu/LangSelector.js";
import { useLang, useTranslationContext } from "@gnu-taler/web-util/browser";
import { InputToggle } from "../../components/form/InputToggle.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;
if (window.navigator.languages) return window.navigator.languages[0];
if (window.navigator.language) return window.navigator.language;
return undefined;
}
export function Settings(): VNode {
const { i18n } = useTranslationContext()
const borwserLang = getBrowserLang()
const { update } = useLang()
const [value, updateValue] = useSettings()
const errors: FormErrors<Settings> = {
}
function valueHandler(s: (d: Partial<Settings>) => Partial<Settings>): void {
const next = s(value)
updateValue("advanceOrderMode", next.advanceOrderMode ?? false)
}
return <div>
<section class="section is-main-section">
<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 />
&nbsp;
{borwserLang !== undefined && <button
data-tooltip={i18n.str`generate random secret key`}
class="button is-info mr-3"
onClick={(e) => {
update(borwserLang.substring(0, 2))
}}
>
<i18n.Translate>Set default</i18n.Translate>
</button>}
</div>
</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>
<div class="column" />
</div>
</section>
</div>
}

View File

@ -52,8 +52,6 @@ $tooltip-color: red;
@import "../../node_modules/@creativebulma/bulma-tooltip/dist/bulma-tooltip.min.css"; @import "../../node_modules/@creativebulma/bulma-tooltip/dist/bulma-tooltip.min.css";
@import "../../node_modules/bulma-timeline/dist/css/bulma-timeline.min.css"; @import "../../node_modules/bulma-timeline/dist/css/bulma-timeline.min.css";
@import "toggle";
.notification { .notification {
background-color: transparent; background-color: transparent;
} }
@ -84,7 +82,7 @@ $tooltip-color: red;
pointer-events: none; pointer-events: none;
} }
.toast>.message { .toast > .message {
white-space: pre-wrap; white-space: pre-wrap;
opacity: 80%; opacity: 80%;
} }
@ -94,7 +92,6 @@ div {
position: relative; position: relative;
pointer-events: none; pointer-events: none;
opacity: 0.5; opacity: 0.5;
&:after { &:after {
// @include loader; // @include loader;
position: absolute; position: absolute;
@ -107,7 +104,7 @@ div {
} }
} }
input[type="checkbox"]:indeterminate+.check { input[type="checkbox"]:indeterminate + .check {
background: red !important; background: red !important;
} }
@ -128,7 +125,6 @@ input[type="checkbox"]:indeterminate+.check {
tr:hover .right-sticky { tr:hover .right-sticky {
background-color: hsl(0, 0%, 80%); background-color: hsl(0, 0%, 80%);
} }
.table.is-striped tbody tr:nth-child(even):hover .right-sticky { .table.is-striped tbody tr:nth-child(even):hover .right-sticky {
background-color: hsl(0, 0%, 95%); background-color: hsl(0, 0%, 95%);
} }
@ -185,11 +181,11 @@ div[data-tooltip]::before {
position: absolute; position: absolute;
} }
.modal-card-body>p { .modal-card-body > p {
padding: 1em; padding: 1em;
} }
.modal-card-body>p.warning { .modal-card-body > p.warning {
background-color: #fffbdd; background-color: #fffbdd;
border: solid 1px #f2e9bf; border: solid 1px #f2e9bf;
} }

View File

@ -1,51 +0,0 @@
$green: #56c080;
.toggle {
cursor: pointer;
display: inline-block;
}
.toggle-switch {
display: inline-block;
background: #ccc;
border-radius: 16px;
width: 58px;
height: 32px;
position: relative;
vertical-align: middle;
transition: background 0.25s;
&:before,
&:after {
content: "";
}
&:before {
display: block;
background: linear-gradient(to bottom, #fff 0%, #eee 100%);
border-radius: 50%;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
width: 24px;
height: 24px;
position: absolute;
top: 4px;
left: 4px;
transition: left 0.25s;
}
.toggle:hover &:before {
background: linear-gradient(to bottom, #fff 0%, #fff 100%);
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5);
}
.toggle-checkbox:checked + & {
background: $green;
&:before {
left: 30px;
}
}
}
.toggle-checkbox {
position: absolute;
visibility: hidden;
}
.toggle-label {
margin-left: 5px;
position: relative;
top: 2px;
}

View File

@ -51,8 +51,8 @@ import {
stringToBytes, stringToBytes,
TalerError, TalerError,
TalerProtocolDuration, TalerProtocolDuration,
RewardCreateConfirmation, TipCreateConfirmation,
RewardCreateRequest, TipCreateRequest,
TippingReserveStatus, TippingReserveStatus,
WalletNotification, WalletNotification,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
@ -1751,8 +1751,8 @@ export namespace MerchantPrivateApi {
export async function giveTip( export async function giveTip(
merchantService: MerchantServiceInterface, merchantService: MerchantServiceInterface,
instance: string, instance: string,
req: RewardCreateRequest, req: TipCreateRequest,
): Promise<RewardCreateConfirmation> { ): Promise<TipCreateConfirmation> {
const reqUrl = new URL( const reqUrl = new URL(
`private/tips`, `private/tips`,
merchantService.makeInstanceBaseUrl(instance), merchantService.makeInstanceBaseUrl(instance),

View File

@ -191,12 +191,12 @@ export async function runAgeRestrictionsMerchantTest(t: GlobalTestState) {
const walletTipping = new WalletCli(t, "age-tipping"); const walletTipping = new WalletCli(t, "age-tipping");
const ptr = await walletTipping.client.call(WalletApiOperation.PrepareReward, { const ptr = await walletTipping.client.call(WalletApiOperation.PrepareTip, {
talerRewardUri: tip.taler_reward_uri, talerTipUri: tip.taler_tip_uri,
}); });
await walletTipping.client.call(WalletApiOperation.AcceptReward, { await walletTipping.client.call(WalletApiOperation.AcceptTip, {
walletRewardId: ptr.walletRewardId, walletTipId: ptr.walletTipId,
}); });
await walletTipping.runUntilDone(); await walletTipping.runUntilDone();

View File

@ -17,20 +17,13 @@
/** /**
* Imports. * Imports.
*/ */
import { import { AbsoluteTime, Duration } from "@gnu-taler/taler-util";
AbsoluteTime,
Duration,
NotificationType,
TransactionMajorState,
TransactionMinorState,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { defaultCoinConfig } from "../harness/denomStructures.js"; import { defaultCoinConfig } from "../harness/denomStructures.js";
import { GlobalTestState } from "../harness/harness.js"; import { GlobalTestState, WalletCli } from "../harness/harness.js";
import { import {
createSimpleTestkudosEnvironmentV2, createSimpleTestkudosEnvironment,
createWalletDaemonWithClient, withdrawViaBank,
withdrawViaBankV2,
} from "../harness/helpers.js"; } from "../harness/helpers.js";
/** /**
@ -39,7 +32,12 @@ import {
export async function runAgeRestrictionsPeerTest(t: GlobalTestState) { export async function runAgeRestrictionsPeerTest(t: GlobalTestState) {
// Set up test environment // Set up test environment
const { bank, exchange } = await createSimpleTestkudosEnvironmentV2( const {
wallet: walletOne,
bank,
exchange,
merchant,
} = await createSimpleTestkudosEnvironment(
t, t,
defaultCoinConfig.map((x) => x("TESTKUDOS")), defaultCoinConfig.map((x) => x("TESTKUDOS")),
{ {
@ -47,29 +45,20 @@ export async function runAgeRestrictionsPeerTest(t: GlobalTestState) {
}, },
); );
const w1 = await createWalletDaemonWithClient(t, { const walletTwo = new WalletCli(t, "walletTwo");
name: "w1", const walletThree = new WalletCli(t, "walletThree");
persistent: true,
});
const w2 = await createWalletDaemonWithClient(t, {
name: "w2",
persistent: true,
});
const wallet1 = w1.walletClient;
const wallet2 = w2.walletClient;
{ {
const withdrawalRes = await withdrawViaBankV2(t, { const wallet = walletOne;
walletClient: wallet1,
await withdrawViaBank(t, {
wallet,
bank, bank,
exchange, exchange,
amount: "TESTKUDOS:20", amount: "TESTKUDOS:20",
restrictAge: 13, restrictAge: 13,
}); });
await withdrawalRes.withdrawalFinishedCond;
const purse_expiration = AbsoluteTime.toProtocolTimestamp( const purse_expiration = AbsoluteTime.toProtocolTimestamp(
AbsoluteTime.addDuration( AbsoluteTime.addDuration(
AbsoluteTime.now(), AbsoluteTime.now(),
@ -77,7 +66,7 @@ export async function runAgeRestrictionsPeerTest(t: GlobalTestState) {
), ),
); );
const initResp = await wallet1.client.call( const initResp = await wallet.client.call(
WalletApiOperation.InitiatePeerPushDebit, WalletApiOperation.InitiatePeerPushDebit,
{ {
partialContractTerms: { partialContractTerms: {
@ -88,35 +77,20 @@ export async function runAgeRestrictionsPeerTest(t: GlobalTestState) {
}, },
); );
const peerPushReadyCond = wallet1.waitForNotificationCond( await wallet.runUntilDone();
(x) =>
x.type === NotificationType.TransactionStateTransition &&
x.newTxState.major === TransactionMajorState.Pending &&
x.newTxState.minor === TransactionMinorState.Ready &&
x.transactionId === initResp.transactionId,
);
await peerPushReadyCond; const checkResp = await walletTwo.client.call(
const checkResp = await wallet2.call(
WalletApiOperation.PreparePeerPushCredit, WalletApiOperation.PreparePeerPushCredit,
{ {
talerUri: initResp.talerUri, talerUri: initResp.talerUri,
}, },
); );
await wallet2.call(WalletApiOperation.ConfirmPeerPushCredit, { await walletTwo.client.call(WalletApiOperation.ConfirmPeerPushCredit, {
transactionId: checkResp.transactionId, peerPushPaymentIncomingId: checkResp.peerPushPaymentIncomingId,
}); });
const peerPullCreditDoneCond = wallet2.waitForNotificationCond( await walletTwo.runUntilDone();
(x) =>
x.type === NotificationType.TransactionStateTransition &&
x.newTxState.major === TransactionMajorState.Done &&
x.transactionId === checkResp.transactionId,
);
await peerPullCreditDoneCond;
} }
} }

View File

@ -23,7 +23,6 @@ import {
j2s, j2s,
NotificationType, NotificationType,
TransactionMajorState, TransactionMajorState,
TransactionMinorState,
WalletNotification, WalletNotification,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
@ -47,14 +46,12 @@ export async function runPeerToPeerPullTest(t: GlobalTestState) {
const w1 = await createWalletDaemonWithClient(t, { const w1 = await createWalletDaemonWithClient(t, {
name: "w1", name: "w1",
persistent: true,
handleNotification(wn) { handleNotification(wn) {
allW1Notifications.push(wn); allW1Notifications.push(wn);
}, },
}); });
const w2 = await createWalletDaemonWithClient(t, { const w2 = await createWalletDaemonWithClient(t, {
name: "w2", name: "w2",
persistent: true,
handleNotification(wn) { handleNotification(wn) {
allW2Notifications.push(wn); allW2Notifications.push(wn);
}, },
@ -92,15 +89,6 @@ export async function runPeerToPeerPullTest(t: GlobalTestState) {
}, },
); );
const peerPullCreditReadyCond = wallet1.waitForNotificationCond(
(x) => x.type === NotificationType.TransactionStateTransition &&
x.transactionId === resp.transactionId &&
x.newTxState.major === TransactionMajorState.Pending &&
x.newTxState.minor === TransactionMinorState.Ready,
);
await peerPullCreditReadyCond;
const checkResp = await wallet2.client.call( const checkResp = await wallet2.client.call(
WalletApiOperation.PreparePeerPullDebit, WalletApiOperation.PreparePeerPullDebit,
{ {
@ -110,6 +98,8 @@ export async function runPeerToPeerPullTest(t: GlobalTestState) {
console.log(`checkResp: ${j2s(checkResp)}`); console.log(`checkResp: ${j2s(checkResp)}`);
// FIXME: The wallet should emit a more appropriate notification here.
// Yes, it's technically a withdrawal.
const peerPullCreditDoneCond = wallet1.waitForNotificationCond( const peerPullCreditDoneCond = wallet1.waitForNotificationCond(
(x) => x.type === NotificationType.TransactionStateTransition && (x) => x.type === NotificationType.TransactionStateTransition &&
x.transactionId === resp.transactionId && x.transactionId === resp.transactionId &&

View File

@ -99,17 +99,17 @@ export async function runTippingTest(t: GlobalTestState) {
console.log("created tip", tip); console.log("created tip", tip);
const doTip = async (): Promise<void> => { const doTip = async (): Promise<void> => {
const ptr = await wallet.client.call(WalletApiOperation.PrepareReward, { const ptr = await wallet.client.call(WalletApiOperation.PrepareTip, {
talerRewardUri: tip.taler_reward_uri, talerTipUri: tip.taler_tip_uri,
}); });
console.log(ptr); console.log(ptr);
t.assertAmountEquals(ptr.rewardAmountRaw, "TESTKUDOS:5"); t.assertAmountEquals(ptr.tipAmountRaw, "TESTKUDOS:5");
t.assertAmountEquals(ptr.rewardAmountEffective, "TESTKUDOS:4.85"); t.assertAmountEquals(ptr.tipAmountEffective, "TESTKUDOS:4.85");
await wallet.client.call(WalletApiOperation.AcceptReward, { await wallet.client.call(WalletApiOperation.AcceptTip, {
walletRewardId: ptr.walletRewardId, walletTipId: ptr.walletTipId,
}); });
await wallet.runUntilDone(); await wallet.runUntilDone();
@ -127,7 +127,7 @@ export async function runTippingTest(t: GlobalTestState) {
console.log("Transactions:", JSON.stringify(txns, undefined, 2)); console.log("Transactions:", JSON.stringify(txns, undefined, 2));
t.assertDeepEqual(txns.transactions[0].type, "reward"); t.assertDeepEqual(txns.transactions[0].type, "tip");
t.assertDeepEqual(txns.transactions[0].txState.major, TransactionMajorState.Done); t.assertDeepEqual(txns.transactions[0].txState.major, TransactionMajorState.Done);
t.assertAmountEquals( t.assertAmountEquals(
txns.transactions[0].amountEffective, txns.transactions[0].amountEffective,

View File

@ -338,17 +338,8 @@ export async function runTests(spec: TestRunSpec) {
currentChild.stdout?.pipe(harnessLogStream); currentChild.stdout?.pipe(harnessLogStream);
currentChild.stderr?.pipe(harnessLogStream); currentChild.stderr?.pipe(harnessLogStream);
// Default timeout when the test doesn't override it. const defaultTimeout = 60000;
let defaultTimeout = 60000; const testTimeoutMs = testCase.timeoutMs ?? defaultTimeout;
const overrideDefaultTimeout = process.env.TALER_TEST_TIMEOUT;
if (overrideDefaultTimeout) {
defaultTimeout = Number.parseInt(overrideDefaultTimeout, 10) * 1000;
}
// Set the timeout to at least be the default timeout.
const testTimeoutMs = testCase.timeoutMs
? Math.max(testCase.timeoutMs, defaultTimeout)
: defaultTimeout;
if (spec.noTimeout) { if (spec.noTimeout) {
console.log(`running ${testName}, no timeout`); console.log(`running ${testName}, no timeout`);

View File

@ -49,10 +49,6 @@
"node": "./lib/http-impl.node.js", "node": "./lib/http-impl.node.js",
"qtart": "./lib/http-impl.qtart.js", "qtart": "./lib/http-impl.qtart.js",
"default": "./lib/http-impl.missing.js" "default": "./lib/http-impl.missing.js"
},
"#argon2-impl": {
"node": "./lib/argon2-impl.node.js",
"default": "./lib/argon2-impl.missing.js"
} }
}, },
"scripts": { "scripts": {
@ -73,12 +69,11 @@
"big-integer": "^1.6.51", "big-integer": "^1.6.51",
"fflate": "^0.7.4", "fflate": "^0.7.4",
"jed": "^1.1.1", "jed": "^1.1.1",
"tslib": "^2.5.3", "tslib": "^2.5.3"
"hash-wasm": "^4.9.0"
}, },
"ava": { "ava": {
"files": [ "files": [
"lib/**/*test.js" "lib/**/*test.js"
] ]
} }
} }

View File

@ -1,10 +0,0 @@
export async function HashArgon2idImpl(
password: Uint8Array,
salt: Uint8Array,
iterations: number,
memorySize: number,
hashLength: number,
): Promise<Uint8Array> {
throw new Error("Method not implemented.");
}

View File

@ -1,19 +0,0 @@
import { argon2id } from "hash-wasm";
export async function HashArgon2idImpl(
password: Uint8Array,
salt: Uint8Array,
iterations: number,
memorySize: number,
hashLength: number,
): Promise<Uint8Array> {
return await argon2id({
password: password,
salt: salt,
iterations: iterations,
memorySize: memorySize,
hashLength: hashLength,
parallelism: 1,
outputType: "binary",
});
}

View File

@ -1,18 +0,0 @@
import * as impl from "#argon2-impl";
export async function hashArgon2id(
password: Uint8Array,
salt: Uint8Array,
iterations: number,
memorySize: number,
hashLength: number,
): Promise<Uint8Array> {
return await impl.HashArgon2idImpl(
password,
salt,
iterations,
memorySize,
hashLength,
);
}

View File

@ -499,7 +499,7 @@ export interface BackupRecoupGroup {
export enum BackupCoinSourceType { export enum BackupCoinSourceType {
Withdraw = "withdraw", Withdraw = "withdraw",
Refresh = "refresh", Refresh = "refresh",
Reward = "reward", Tip = "tip",
} }
/** /**
@ -546,7 +546,7 @@ export interface BackupRefreshCoinSource {
* Metadata about a coin obtained from a tip. * Metadata about a coin obtained from a tip.
*/ */
export interface BackupTipCoinSource { export interface BackupTipCoinSource {
type: BackupCoinSourceType.Reward; type: BackupCoinSourceType.Tip;
/** /**
* Wallet's identifier for the tip that this coin * Wallet's identifier for the tip that this coin

View File

@ -183,16 +183,7 @@ export class HttpLibImpl implements HttpRequestLibrary {
resolve(resp); resolve(resp);
}); });
res.on("error", (e) => { res.on("error", (e) => {
const err = TalerError.fromDetail( reject(e);
TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
{
requestUrl: url,
requestMethod: method,
httpStatusCode: 0,
},
`Error in HTTP response handler: ${e.message}`,
);
reject(err);
}); });
}; };
@ -206,16 +197,7 @@ export class HttpLibImpl implements HttpRequestLibrary {
} }
req.on("error", (e: Error) => { req.on("error", (e: Error) => {
const err = TalerError.fromDetail( reject(e);
TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
{
requestUrl: url,
requestMethod: method,
httpStatusCode: 0,
},
`Error in HTTP request: ${e.message}`,
);
reject(err);
}); });
if (reqBody) { if (reqBody) {

View File

@ -290,22 +290,22 @@ export interface ReserveStatusEntry {
active: boolean; active: boolean;
} }
export interface RewardCreateConfirmation { export interface TipCreateConfirmation {
// Unique tip identifier for the tip that was created. // Unique tip identifier for the tip that was created.
reward_id: string; tip_id: string;
// taler://tip URI for the tip // taler://tip URI for the tip
taler_reward_uri: string; taler_tip_uri: string;
// URL that will directly trigger processing // URL that will directly trigger processing
// the tip when the browser is redirected to it // the tip when the browser is redirected to it
reward_status_url: string; tip_status_url: string;
// when does the reward expire // when does the tip expire
reward_expiration: AbsoluteTime; tip_expiration: AbsoluteTime;
} }
export interface RewardCreateRequest { export interface TipCreateRequest {
// Amount that the customer should be tipped // Amount that the customer should be tipped
amount: AmountString; amount: AmountString;

View File

@ -24,7 +24,6 @@
import * as nacl from "./nacl-fast.js"; import * as nacl from "./nacl-fast.js";
import { hmacSha256, hmacSha512 } from "./kdf.js"; import { hmacSha256, hmacSha512 } from "./kdf.js";
import bigint from "big-integer"; import bigint from "big-integer";
import * as argon2 from "./argon2.js";
import { import {
CoinEnvelope, CoinEnvelope,
CoinPublicKeyString, CoinPublicKeyString,
@ -70,13 +69,6 @@ interface NativeTartLib {
encodeCrock(buf: Uint8Array | ArrayBuffer): string; encodeCrock(buf: Uint8Array | ArrayBuffer): string;
decodeCrock(str: string): Uint8Array; decodeCrock(str: string): Uint8Array;
hash(buf: Uint8Array): Uint8Array; hash(buf: Uint8Array): Uint8Array;
hashArgon2id(
password: Uint8Array,
salt: Uint8Array,
iterations: number,
memorySize: number,
hashLength: number,
): Uint8Array;
eddsaGetPublic(buf: Uint8Array): Uint8Array; eddsaGetPublic(buf: Uint8Array): Uint8Array;
ecdheGetPublic(buf: Uint8Array): Uint8Array; ecdheGetPublic(buf: Uint8Array): Uint8Array;
eddsaSign(msg: Uint8Array, priv: Uint8Array): Uint8Array; eddsaSign(msg: Uint8Array, priv: Uint8Array): Uint8Array;
@ -261,31 +253,6 @@ export function decodeCrock(encoded: string): Uint8Array {
return out; return out;
} }
export async function hashArgon2id(
password: Uint8Array,
salt: Uint8Array,
iterations: number,
memorySize: number,
hashLength: number,
): Promise<Uint8Array> {
if (tart) {
return tart.hashArgon2id(
password,
salt,
iterations,
memorySize,
hashLength,
);
}
return await argon2.hashArgon2id(
password,
salt,
iterations,
memorySize,
hashLength,
);
}
export function eddsaGetPublic(eddsaPriv: Uint8Array): Uint8Array { export function eddsaGetPublic(eddsaPriv: Uint8Array): Uint8Array {
if (tart) { if (tart) {
return tart.eddsaGetPublic(eddsaPriv); return tart.eddsaGetPublic(eddsaPriv);
@ -1254,14 +1221,32 @@ export namespace AgeRestriction {
age: number, age: number,
): Promise<AgeCommitmentProof> { ): Promise<AgeCommitmentProof> {
invariant((ageMask & 1) === 1); invariant((ageMask & 1) === 1);
const seed = getRandomBytes(32); const numPubs = countAgeGroups(ageMask) - 1;
const numPrivs = getAgeGroupIndex(ageMask, age);
return restrictionCommitSeeded(ageMask, age, seed); const pubs: Edx25519PublicKey[] = [];
const privs: Edx25519PrivateKey[] = [];
for (let i = 0; i < numPubs; i++) {
const priv = await Edx25519.keyCreate();
const pub = await Edx25519.getPublic(priv);
pubs.push(pub);
if (i < numPrivs) {
privs.push(priv);
}
}
return {
commitment: {
mask: ageMask,
publicKeys: pubs.map((x) => encodeCrock(x)),
},
proof: {
privateKeys: privs.map((x) => encodeCrock(x)),
},
};
} }
const PublishedAgeRestrictionBaseKey: Edx25519PublicKey = decodeCrock(
"CH0VKFDZ2GWRWHQBBGEK9MWV5YDQVJ0RXEE0KYT3NMB69F0R96TG");
export async function restrictionCommitSeeded( export async function restrictionCommitSeeded(
ageMask: number, ageMask: number,
age: number, age: number,
@ -1274,32 +1259,19 @@ export namespace AgeRestriction {
const pubs: Edx25519PublicKey[] = []; const pubs: Edx25519PublicKey[] = [];
const privs: Edx25519PrivateKey[] = []; const privs: Edx25519PrivateKey[] = [];
for (let i = 0; i < numPrivs; i++) { for (let i = 0; i < numPubs; i++) {
const privSeed = await kdfKw({ const privSeed = await kdfKw({
outputLength: 32, outputLength: 32,
ikm: seed, ikm: seed,
info: stringToBytes("age-commitment"), info: stringToBytes("age-restriction-commit"),
salt: bufferForUint32(i), salt: bufferForUint32(i),
}); });
const priv = await Edx25519.keyCreateFromSeed(privSeed); const priv = await Edx25519.keyCreateFromSeed(privSeed);
const pub = await Edx25519.getPublic(priv); const pub = await Edx25519.getPublic(priv);
pubs.push(pub); pubs.push(pub);
privs.push(priv); if (i < numPrivs) {
} privs.push(priv);
}
for (let i = numPrivs; i < numPubs; i++) {
const deriveSeed = await kdfKw({
outputLength: 32,
ikm: seed,
info: stringToBytes("age-factor"),
salt: bufferForUint32(i),
});
const pub = await Edx25519.publicKeyDerive(
PublishedAgeRestrictionBaseKey,
deriveSeed,
);
pubs.push(pub);
} }
return { return {
@ -1599,9 +1571,7 @@ export function amountToBuffer(amount: AmountLike): Uint8Array {
return u8buf; return u8buf;
} }
export function timestampRoundedToBuffer( export function timestampRoundedToBuffer(ts: TalerProtocolTimestamp): Uint8Array {
ts: TalerProtocolTimestamp,
): Uint8Array {
const b = new ArrayBuffer(8); const b = new ArrayBuffer(8);
const v = new DataView(b); const v = new DataView(b);
// The buffer we sign over represents the timestamp in microseconds. // The buffer we sign over represents the timestamp in microseconds.

View File

@ -1788,89 +1788,6 @@ export interface ExchangeRefreshRevealRequest {
old_age_commitment?: Edx25519PublicKeyEnc[]; old_age_commitment?: Edx25519PublicKeyEnc[];
} }
export interface ExchangeAgeWithdrawRequest {
// Array of n hash codes of denomination public keys to order.
// These denominations MUST support age restriction as defined in the
// output to /keys.
// The sum of all denomination's values and fees MUST be at most the
// balance of the reserve. The balance of the reserve will be
// immediatley reduced by that amount.
denoms_h: HashCodeString[];
// n arrays of kappa entries with blinded coin envelopes. Each
// (toplevel) entry represents kappa canditates for a particular
// coin. The exchange will respond with an index gamma, which is
// the index that shall remain undisclosed during the reveal phase.
// The SHA512 hash $ACH over the blinded coin envelopes is the commitment
// that is later used as the key to the reveal-URL.
blinded_coins_evs: CoinEnvelope[][];
// The maximum age to commit to. MUST be the same as the maximum
// age value assigned to the reserve, based on its birthday date.
max_age: number;
// Signature of TALER_AgeWithdrawRequestPS created with
// the reserves's private key
// using purpose TALER_SIGNATURE_WALLET_RESERVE_AGE_WITHDRAW.
reserve_sig: EddsaSignatureString;
}
export interface ExchangeAgeWithdrawResponse {
// index of the commitments that the client doesn't
// have to disclose
noreveal_index: number;
// Signature of TALER_AgeWithdrawConfirmationPS whereby
// the exchange confirms the noreveal_index.
exchange_sig: EddsaSignatureString;
// Public EdDSA key of the exchange that was used to
// generate the signature. Should match one of the exchange's signing
// keys from /keys. Again given explicitly as the client might
// otherwise be confused by clock skew as to which signing key was used.
exchange_pub: EddsaPublicKeyString;
}
export interface ExchangeAgeWithdrawRevealRequest {
// Array of n of (kappa - 1) disclosed coin master secrets, from
// which the coins' private key, blinding, nonce (for Clause-Schnorr) and
// age-restriction is calculated.
//
// Given each coin's private key and age commitment, the exchange will
// calculate each coin's blinded hash value und use all those (disclosed)
// blinded hashes together with the non-disclosed envelopes coin_evs
// during the verification of the original age-withdraw-commitment.
disclosed_coin_secrets: AgeRestrictedCoinSecret[][];
}
// The Master key material from which the coins' private key coin_priv,
// blinding beta and nonce nonce (for Clause-Schnorr) itself are
// derived as usually in wallet-core. Given a coin's master key material,
// the age commitment for the coin MUST be derived from this private key as
// follows:
//
// Let m ∈ {1,...,M} be the maximum age group as defined in the reserve
// that the wallet can commit to.
//
// For age group $AG ∈ {1,...m}, set
// seed = HDKF(coin_secret, "age-commitment", $AG)
// p[$AG] = Edx25519_generate_private(seed)
// and calculate the corresponding Edx25519PublicKey as
// q[$AG] = Edx25519_public_from_private(p[$AG])
//
// For age groups $AG ∈ {m,...,M}, set
// f[$AG] = HDKF(coin_secret, "age-factor", $AG)
// and calculate the corresponding Edx25519PublicKey as
// q[$AG] = Edx25519_derive_public(PublishedAgeRestrictionBaseKey, f[$AG])
//
// FIXME: shall we add some flavor to this string?
export type AgeRestrictedCoinSecret = string;
export interface ExchangeAgeWithdrawRevealResponse {
// List of the exchange's blinded RSA or CS signatures on the new coins.
ev_sigs : BlindedDenominationSignature[];
}
export interface DepositSuccess { export interface DepositSuccess {
// Optional base URL of the exchange for looking up wire transfers // Optional base URL of the exchange for looking up wire transfers
// associated with this transaction. If not given, // associated with this transaction. If not given,

View File

@ -21,7 +21,7 @@ import {
parsePayUri, parsePayUri,
parseRefundUri, parseRefundUri,
parseRestoreUri, parseRestoreUri,
parseRewardUri, parseTipUri,
parseWithdrawExchangeUri, parseWithdrawExchangeUri,
parseWithdrawUri, parseWithdrawUri,
stringifyPayPushUri, stringifyPayPushUri,
@ -161,7 +161,7 @@ test("taler refund uri parsing with instance", (t) => {
test("taler tip pickup uri", (t) => { test("taler tip pickup uri", (t) => {
const url1 = "taler://tip/merchant.example.com/tipid"; const url1 = "taler://tip/merchant.example.com/tipid";
const r1 = parseRewardUri(url1); const r1 = parseTipUri(url1);
if (!r1) { if (!r1) {
t.fail(); t.fail();
return; return;
@ -171,7 +171,7 @@ test("taler tip pickup uri", (t) => {
test("taler tip pickup uri with instance", (t) => { test("taler tip pickup uri with instance", (t) => {
const url1 = "taler://tip/merchant.example.com/instances/tipm/tipid"; const url1 = "taler://tip/merchant.example.com/instances/tipm/tipid";
const r1 = parseRewardUri(url1); const r1 = parseTipUri(url1);
if (!r1) { if (!r1) {
t.fail(); t.fail();
return; return;
@ -182,7 +182,7 @@ test("taler tip pickup uri with instance", (t) => {
test("taler tip pickup uri with instance and prefix", (t) => { test("taler tip pickup uri with instance and prefix", (t) => {
const url1 = "taler://tip/merchant.example.com/my/pfx/tipm/tipid"; const url1 = "taler://tip/merchant.example.com/my/pfx/tipm/tipid";
const r1 = parseRewardUri(url1); const r1 = parseTipUri(url1);
if (!r1) { if (!r1) {
t.fail(); t.fail();
return; return;
@ -367,6 +367,6 @@ test("taler withdraw exchange URI with amount (stringify)", (t) => {
}); });
t.deepEqual( t.deepEqual(
url, url,
"taler://withdraw-exchange/exchange.demo.taler.net/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0?a=KUDOS%3A19", "taler://withdraw-exchange/exchange.demo.taler.net/JFX1NE38C65A5XT8VSNQXX7R7BBG4GNZ63F5T7Y6859V4J8KBKF0?a=KUDOS%3A19",
); );
}); });

View File

@ -26,7 +26,7 @@ export type TalerUri =
| PayPushUriResult | PayPushUriResult
| BackupRestoreUri | BackupRestoreUri
| RefundUriResult | RefundUriResult
| RewardUriResult | TipUriResult
| WithdrawUriResult | WithdrawUriResult
| ExchangeUri | ExchangeUri
| WithdrawExchangeUri | WithdrawExchangeUri
@ -60,8 +60,8 @@ export interface RefundUriResult {
orderId: string; orderId: string;
} }
export interface RewardUriResult { export interface TipUriResult {
type: TalerUriAction.Reward; type: TalerUriAction.Tip;
merchantBaseUrl: string; merchantBaseUrl: string;
merchantTipId: string; merchantTipId: string;
} }
@ -167,7 +167,7 @@ export enum TalerUriAction {
Pay = "pay", Pay = "pay",
Withdraw = "withdraw", Withdraw = "withdraw",
Refund = "refund", Refund = "refund",
Reward = "reward", Tip = "tip",
PayPull = "pay-pull", PayPull = "pay-pull",
PayPush = "pay-push", PayPush = "pay-push",
PayTemplate = "pay-template", PayTemplate = "pay-template",
@ -212,7 +212,7 @@ const parsers: { [A in TalerUriAction]: Parser } = {
[TalerUriAction.PayTemplate]: parsePayTemplateUri, [TalerUriAction.PayTemplate]: parsePayTemplateUri,
[TalerUriAction.Restore]: parseRestoreUri, [TalerUriAction.Restore]: parseRestoreUri,
[TalerUriAction.Refund]: parseRefundUri, [TalerUriAction.Refund]: parseRefundUri,
[TalerUriAction.Reward]: parseRewardUri, [TalerUriAction.Tip]: parseTipUri,
[TalerUriAction.Withdraw]: parseWithdrawUri, [TalerUriAction.Withdraw]: parseWithdrawUri,
[TalerUriAction.DevExperiment]: parseDevExperimentUri, [TalerUriAction.DevExperiment]: parseDevExperimentUri,
[TalerUriAction.Exchange]: parseExchangeUri, [TalerUriAction.Exchange]: parseExchangeUri,
@ -255,8 +255,8 @@ export function stringifyTalerUri(uri: TalerUri): string {
case TalerUriAction.Refund: { case TalerUriAction.Refund: {
return stringifyRefundUri(uri); return stringifyRefundUri(uri);
} }
case TalerUriAction.Reward: { case TalerUriAction.Tip: {
return stringifyRewardUri(uri); return stringifyTipUri(uri);
} }
case TalerUriAction.Withdraw: { case TalerUriAction.Withdraw: {
return stringifyWithdrawUri(uri); return stringifyWithdrawUri(uri);
@ -394,11 +394,11 @@ export function parsePayPullUri(s: string): PayPullUriResult | undefined {
} }
/** /**
* Parse a taler[+http]://reward URI. * Parse a taler[+http]://tip URI.
* Return undefined if not passed a valid URI. * Return undefined if not passed a valid URI.
*/ */
export function parseRewardUri(s: string): RewardUriResult | undefined { export function parseTipUri(s: string): TipUriResult | undefined {
const pi = parseProtoInfo(s, "reward"); const pi = parseProtoInfo(s, "tip");
if (!pi) { if (!pi) {
return undefined; return undefined;
} }
@ -416,7 +416,7 @@ export function parseRewardUri(s: string): RewardUriResult | undefined {
); );
return { return {
type: TalerUriAction.Reward, type: TalerUriAction.Tip,
merchantBaseUrl, merchantBaseUrl,
merchantTipId: tipId, merchantTipId: tipId,
}; };
@ -699,12 +699,12 @@ export function stringifyRefundUri({
const { proto, path } = getUrlInfo(merchantBaseUrl); const { proto, path } = getUrlInfo(merchantBaseUrl);
return `${proto}://refund/${path}${orderId}`; return `${proto}://refund/${path}${orderId}`;
} }
export function stringifyRewardUri({ export function stringifyTipUri({
merchantBaseUrl, merchantBaseUrl,
merchantTipId, merchantTipId,
}: Omit<RewardUriResult, "type">): string { }: Omit<TipUriResult, "type">): string {
const { proto, path } = getUrlInfo(merchantBaseUrl); const { proto, path } = getUrlInfo(merchantBaseUrl);
return `${proto}://reward/${path}${merchantTipId}`; return `${proto}://tip/${path}${merchantTipId}`;
} }
export function stringifyExchangeUri({ export function stringifyExchangeUri({
@ -767,7 +767,7 @@ function getUrlInfo(
const qp = new URLSearchParams(); const qp = new URLSearchParams();
let withParams = false; let withParams = false;
Object.entries(params).forEach(([name, value]) => { Object.entries(params).forEach(([name, value]) => {
if (value !== undefined) { if (value) {
withParams = true; withParams = true;
qp.append(name, value); qp.append(name, value);
} }

View File

@ -186,7 +186,7 @@ export type Transaction =
| TransactionWithdrawal | TransactionWithdrawal
| TransactionPayment | TransactionPayment
| TransactionRefund | TransactionRefund
| TransactionReward | TransactionTip
| TransactionRefresh | TransactionRefresh
| TransactionDeposit | TransactionDeposit
| TransactionPeerPullCredit | TransactionPeerPullCredit
@ -201,7 +201,7 @@ export enum TransactionType {
Payment = "payment", Payment = "payment",
Refund = "refund", Refund = "refund",
Refresh = "refresh", Refresh = "refresh",
Reward = "reward", Tip = "tip",
Deposit = "deposit", Deposit = "deposit",
PeerPushDebit = "peer-push-debit", PeerPushDebit = "peer-push-debit",
PeerPushCredit = "peer-push-credit", PeerPushCredit = "peer-push-credit",
@ -591,8 +591,8 @@ export interface TransactionRefund extends TransactionCommon {
paymentInfo: RefundPaymentInfo | undefined; paymentInfo: RefundPaymentInfo | undefined;
} }
export interface TransactionReward extends TransactionCommon { export interface TransactionTip extends TransactionCommon {
type: TransactionType.Reward; type: TransactionType.Tip;
// Raw amount of the tip, without extra fees that apply // Raw amount of the tip, without extra fees that apply
amountRaw: AmountString; amountRaw: AmountString;

View File

@ -379,54 +379,6 @@ export interface Balance {
requiresUserInput: boolean; requiresUserInput: boolean;
} }
export const codecForScopeInfoGlobal = (): Codec<ScopeInfoGlobal> =>
buildCodecForObject<ScopeInfoGlobal>()
.property("currency", codecForString())
.property("type", codecForConstString(ScopeType.Global))
.build("ScopeInfoGlobal");
export const codecForScopeInfoExchange = (): Codec<ScopeInfoExchange> =>
buildCodecForObject<ScopeInfoExchange>()
.property("currency", codecForString())
.property("type", codecForConstString(ScopeType.Exchange))
.property("url", codecForString())
.build("ScopeInfoExchange");
export const codecForScopeInfoAuditor = (): Codec<ScopeInfoAuditor> =>
buildCodecForObject<ScopeInfoAuditor>()
.property("currency", codecForString())
.property("type", codecForConstString(ScopeType.Auditor))
.property("url", codecForString())
.build("ScopeInfoAuditor");
export const codecForScopeInfo = (): Codec<ScopeInfo> =>
buildCodecForUnion<ScopeInfo>()
.discriminateOn("type")
.alternative(ScopeType.Global, codecForScopeInfoGlobal())
.alternative(ScopeType.Exchange, codecForScopeInfoExchange())
.alternative(ScopeType.Auditor, codecForScopeInfoAuditor())
.build("ScopeInfo");
export interface GetCurrencyInfoRequest {
scope: ScopeInfo;
}
export const codecForGetCurrencyInfoRequest =
(): Codec<GetCurrencyInfoRequest> =>
buildCodecForObject<GetCurrencyInfoRequest>()
.property("scope", codecForScopeInfo())
.build("GetCurrencyInfoRequest");
export interface GetCurrencyInfoResponse {
decimalSeparator: string;
numFractionalDigits: number;
numTinyDigits: number;
/**
* Is the currency name leading or trailing?
*/
isCurrencyNameLeading: boolean;
}
export interface InitRequest { export interface InitRequest {
skipDefaults?: boolean; skipDefaults?: boolean;
} }
@ -441,19 +393,10 @@ export enum ScopeType {
Auditor = "auditor", Auditor = "auditor",
} }
export type ScopeInfoGlobal = { type: ScopeType.Global; currency: string }; export type ScopeInfo =
export type ScopeInfoExchange = { | { type: ScopeType.Global; currency: string }
type: ScopeType.Exchange; | { type: ScopeType.Exchange; currency: string; url: string }
currency: string; | { type: ScopeType.Auditor; currency: string; url: string };
url: string;
};
export type ScopeInfoAuditor = {
type: ScopeType.Auditor;
currency: string;
url: string;
};
export type ScopeInfo = ScopeInfoGlobal | ScopeInfoExchange | ScopeInfoAuditor;
export interface BalancesResponse { export interface BalancesResponse {
balances: Balance[]; balances: Balance[];
@ -662,7 +605,7 @@ export interface PrepareTipResult {
* *
* @deprecated use transactionId instead * @deprecated use transactionId instead
*/ */
walletRewardId: string; walletTipId: string;
/** /**
* Tip transaction ID. * Tip transaction ID.
@ -677,13 +620,13 @@ export interface PrepareTipResult {
/** /**
* Amount that the merchant gave. * Amount that the merchant gave.
*/ */
rewardAmountRaw: AmountString; tipAmountRaw: AmountString;
/** /**
* Amount that arrived at the wallet. * Amount that arrived at the wallet.
* Might be lower than the raw amount due to fees. * Might be lower than the raw amount due to fees.
*/ */
rewardAmountEffective: AmountString; tipAmountEffective: AmountString;
/** /**
* Base URL of the merchant backend giving then tip. * Base URL of the merchant backend giving then tip.
@ -711,14 +654,14 @@ export interface AcceptTipResponse {
export const codecForPrepareTipResult = (): Codec<PrepareTipResult> => export const codecForPrepareTipResult = (): Codec<PrepareTipResult> =>
buildCodecForObject<PrepareTipResult>() buildCodecForObject<PrepareTipResult>()
.property("accepted", codecForBoolean()) .property("accepted", codecForBoolean())
.property("rewardAmountRaw", codecForAmountString()) .property("tipAmountRaw", codecForAmountString())
.property("rewardAmountEffective", codecForAmountString()) .property("tipAmountEffective", codecForAmountString())
.property("exchangeBaseUrl", codecForString()) .property("exchangeBaseUrl", codecForString())
.property("merchantBaseUrl", codecForString()) .property("merchantBaseUrl", codecForString())
.property("expirationTimestamp", codecForTimestamp) .property("expirationTimestamp", codecForTimestamp)
.property("walletRewardId", codecForString()) .property("walletTipId", codecForString())
.property("transactionId", codecForString()) .property("transactionId", codecForString())
.build("PrepareRewardResult"); .build("PrepareTipResult");
export interface BenchmarkResult { export interface BenchmarkResult {
time: { [s: string]: number }; time: { [s: string]: number };
@ -1051,9 +994,6 @@ export interface ExchangeDetailedResponse {
} }
export interface WalletCoreVersion { export interface WalletCoreVersion {
/**
* @deprecated
*/
hash: string | undefined; hash: string | undefined;
version: string; version: string;
exchange: string; exchange: string;
@ -1990,23 +1930,23 @@ export const codecForStartRefundQueryRequest =
.property("transactionId", codecForTransactionIdStr()) .property("transactionId", codecForTransactionIdStr())
.build("StartRefundQueryRequest"); .build("StartRefundQueryRequest");
export interface PrepareRewardRequest { export interface PrepareTipRequest {
talerRewardUri: string; talerTipUri: string;
} }
export const codecForPrepareRewardRequest = (): Codec<PrepareRewardRequest> => export const codecForPrepareTipRequest = (): Codec<PrepareTipRequest> =>
buildCodecForObject<PrepareRewardRequest>() buildCodecForObject<PrepareTipRequest>()
.property("talerRewardUri", codecForString()) .property("talerTipUri", codecForString())
.build("PrepareRewardRequest"); .build("PrepareTipRequest");
export interface AcceptRewardRequest { export interface AcceptTipRequest {
walletRewardId: string; walletTipId: string;
} }
export const codecForAcceptTipRequest = (): Codec<AcceptRewardRequest> => export const codecForAcceptTipRequest = (): Codec<AcceptTipRequest> =>
buildCodecForObject<AcceptRewardRequest>() buildCodecForObject<AcceptTipRequest>()
.property("walletRewardId", codecForString()) .property("walletTipId", codecForString())
.build("AcceptRewardRequest"); .build("AcceptTipRequest");
export interface FailTransactionRequest { export interface FailTransactionRequest {
transactionId: TransactionIdStr; transactionId: TransactionIdStr;

View File

@ -651,13 +651,13 @@ walletCli
alwaysYes: args.handleUri.autoYes, alwaysYes: args.handleUri.autoYes,
}); });
break; break;
case TalerUriAction.Reward: { case TalerUriAction.Tip: {
const res = await wallet.client.call(WalletApiOperation.PrepareReward, { const res = await wallet.client.call(WalletApiOperation.PrepareTip, {
talerRewardUri: uri, talerTipUri: uri,
}); });
console.log("tip status", res); console.log("tip status", res);
await wallet.client.call(WalletApiOperation.AcceptReward, { await wallet.client.call(WalletApiOperation.AcceptTip, {
walletRewardId: res.walletRewardId, walletTipId: res.walletTipId,
}); });
break; break;
} }

View File

@ -1,3 +1,2 @@
/lib /lib
/coverage /coverage
/src/version.json

View File

@ -85,4 +85,4 @@
"lib/**/*test.*" "lib/**/*test.*"
] ]
} }
} }

View File

@ -28,6 +28,7 @@ import {
AgeCommitmentProof, AgeCommitmentProof,
AgeRestriction, AgeRestriction,
AmountJson, AmountJson,
AmountLike,
Amounts, Amounts,
AmountString, AmountString,
amountToBuffer, amountToBuffer,
@ -63,6 +64,7 @@ import {
hashCoinPub, hashCoinPub,
hashDenomPub, hashDenomPub,
hashTruncate32, hashTruncate32,
j2s,
kdf, kdf,
kdfKw, kdfKw,
keyExchangeEcdhEddsa, keyExchangeEcdhEddsa,
@ -79,13 +81,16 @@ import {
rsaVerify, rsaVerify,
setupTipPlanchet, setupTipPlanchet,
stringToBytes, stringToBytes,
TalerProtocolDuration,
TalerProtocolTimestamp, TalerProtocolTimestamp,
TalerSignaturePurpose, TalerSignaturePurpose,
timestampRoundedToBuffer, timestampRoundedToBuffer,
UnblindedSignature, UnblindedSignature,
validateIban,
WireFee, WireFee,
WithdrawalPlanchet, WithdrawalPlanchet,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import bigint from "big-integer";
// FIXME: Crypto should not use DB Types! // FIXME: Crypto should not use DB Types!
import { DenominationRecord } from "../db.js"; import { DenominationRecord } from "../db.js";
import { import {
@ -103,6 +108,7 @@ import {
EncryptContractForDepositResponse, EncryptContractForDepositResponse,
EncryptContractRequest, EncryptContractRequest,
EncryptContractResponse, EncryptContractResponse,
EncryptedContract,
SignDeletePurseRequest, SignDeletePurseRequest,
SignDeletePurseResponse, SignDeletePurseResponse,
SignPurseMergeRequest, SignPurseMergeRequest,
@ -720,10 +726,9 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
if (denomPub.age_mask) { if (denomPub.age_mask) {
const age = req.restrictAge || AgeRestriction.AGE_UNRESTRICTED; const age = req.restrictAge || AgeRestriction.AGE_UNRESTRICTED;
logger.info(`creating age-restricted planchet (age ${age})`); logger.info(`creating age-restricted planchet (age ${age})`);
maybeAcp = await AgeRestriction.restrictionCommitSeeded( maybeAcp = await AgeRestriction.restrictionCommit(
denomPub.age_mask, denomPub.age_mask,
age, age,
stringToBytes(req.secretSeed)
); );
maybeAgeCommitmentHash = AgeRestriction.hashCommitment( maybeAgeCommitmentHash = AgeRestriction.hashCommitment(
maybeAcp.commitment, maybeAcp.commitment,

View File

@ -657,7 +657,6 @@ export interface PlanchetRecord {
*/ */
coinIdx: number; coinIdx: number;
planchetStatus: PlanchetStatus; planchetStatus: PlanchetStatus;
lastError: TalerErrorDetail | undefined; lastError: TalerErrorDetail | undefined;
@ -672,19 +671,13 @@ export interface PlanchetRecord {
coinEvHash: string; coinEvHash: string;
/**
* Index into the kappa-many planchet commitments per coin
* for the age-withdraw operation.
*/
ageWithdrawIdx?: number;
ageCommitmentProof?: AgeCommitmentProof; ageCommitmentProof?: AgeCommitmentProof;
} }
export enum CoinSourceType { export enum CoinSourceType {
Withdraw = "withdraw", Withdraw = "withdraw",
Refresh = "refresh", Refresh = "refresh",
Reward = "reward", Tip = "tip",
} }
export interface WithdrawCoinSource { export interface WithdrawCoinSource {
@ -712,16 +705,13 @@ export interface RefreshCoinSource {
oldCoinPub: string; oldCoinPub: string;
} }
export interface RewardCoinSource { export interface TipCoinSource {
type: CoinSourceType.Reward; type: CoinSourceType.Tip;
walletRewardId: string; walletTipId: string;
coinIndex: number; coinIndex: number;
} }
export type CoinSource = export type CoinSource = WithdrawCoinSource | RefreshCoinSource | TipCoinSource;
| WithdrawCoinSource
| RefreshCoinSource
| RewardCoinSource;
/** /**
* CoinRecord as stored in the "coins" data store * CoinRecord as stored in the "coins" data store
@ -825,9 +815,9 @@ export interface CoinAllocation {
} }
/** /**
* Status of a reward we got from a merchant. * Status of a tip we got from a merchant.
*/ */
export interface RewardRecord { export interface TipRecord {
/** /**
* Has the user accepted the tip? Only after the tip has been accepted coins * Has the user accepted the tip? Only after the tip has been accepted coins
* withdrawn from the tip may be used. * withdrawn from the tip may be used.
@ -837,17 +827,17 @@ export interface RewardRecord {
/** /**
* The tipped amount. * The tipped amount.
*/ */
rewardAmountRaw: AmountString; tipAmountRaw: AmountString;
/** /**
* Effect on the balance (including fees etc). * Effect on the balance (including fees etc).
*/ */
rewardAmountEffective: AmountString; tipAmountEffective: AmountString;
/** /**
* Timestamp, the tip can't be picked up anymore after this deadline. * Timestamp, the tip can't be picked up anymore after this deadline.
*/ */
rewardExpiration: TalerProtocolTimestamp; tipExpiration: TalerProtocolTimestamp;
/** /**
* The exchange that will sign our coins, chosen by the merchant. * The exchange that will sign our coins, chosen by the merchant.
@ -873,7 +863,7 @@ export interface RewardRecord {
/** /**
* Tip ID chosen by the wallet. * Tip ID chosen by the wallet.
*/ */
walletRewardId: string; walletTipId: string;
/** /**
* Secret seed used to derive planchets for this tip. * Secret seed used to derive planchets for this tip.
@ -881,9 +871,9 @@ export interface RewardRecord {
secretSeed: string; secretSeed: string;
/** /**
* The merchant's identifier for this reward. * The merchant's identifier for this tip.
*/ */
merchantRewardId: string; merchantTipId: string;
createdTimestamp: TalerPreciseTimestamp; createdTimestamp: TalerPreciseTimestamp;
@ -898,10 +888,10 @@ export interface RewardRecord {
*/ */
pickedUpTimestamp: TalerPreciseTimestamp | undefined; pickedUpTimestamp: TalerPreciseTimestamp | undefined;
status: RewardRecordStatus; status: TipRecordStatus;
} }
export enum RewardRecordStatus { export enum TipRecordStatus {
PendingPickup = 10, PendingPickup = 10,
SuspendidPickup = 20, SuspendidPickup = 20,
@ -1430,7 +1420,7 @@ export interface KycPendingInfo {
} }
/** /**
* Group of withdrawal operations that need to be executed. * Group of withdrawal operations that need to be executed.
* (Either for a normal {single-|batch-|age-} withdrawal or from a reward.) * (Either for a normal withdrawal or from a tip.)
* *
* The withdrawal group record is only created after we know * The withdrawal group record is only created after we know
* the coin selection we want to withdraw. * the coin selection we want to withdraw.
@ -2490,12 +2480,12 @@ export const WalletStoresV1 = {
]), ]),
}, },
), ),
rewards: describeStore( tips: describeStore(
"rewards", "tips",
describeContents<RewardRecord>({ keyPath: "walletRewardId" }), describeContents<TipRecord>({ keyPath: "walletTipId" }),
{ {
byMerchantTipIdAndBaseUrl: describeIndex("byMerchantRewardIdAndBaseUrl", [ byMerchantTipIdAndBaseUrl: describeIndex("byMerchantTipIdAndBaseUrl", [
"merchantRewardId", "merchantTipId",
"merchantBaseUrl", "merchantBaseUrl",
]), ]),
byStatus: describeIndex("byStatus", "status", { byStatus: describeIndex("byStatus", "status", {
@ -2520,11 +2510,6 @@ export const WalletStoresV1 = {
"planchets", "planchets",
describeContents<PlanchetRecord>({ keyPath: "coinPub" }), describeContents<PlanchetRecord>({ keyPath: "coinPub" }),
{ {
byGroupAgeCoin: describeIndex("byGroupAgeCoin", [
"withdrawalGroupId",
"ageWithdrawIdx",
"coinIdx",
]),
byGroupAndIndex: describeIndex("byGroupAndIndex", [ byGroupAndIndex: describeIndex("byGroupAndIndex", [
"withdrawalGroupId", "withdrawalGroupId",
"coinIdx", "coinIdx",
@ -2950,6 +2935,22 @@ export const walletDbFixups: FixupDescription[] = [
}); });
}, },
}, },
{
name: "TipRecordRecord_status_add",
async fn(tx): Promise<void> {
await tx.tips.iter().forEachAsync(async (r) => {
// Remove legacy transactions that don't have the totalCost field yet.
if (r.status == null) {
if (r.pickedUpTimestamp) {
r.status = TipRecordStatus.Done;
} else {
r.status = TipRecordStatus.PendingPickup;
}
await tx.tips.put(r);
}
});
},
},
{ {
name: "CoinAvailabilityRecord_visibleCoinCount_add", name: "CoinAvailabilityRecord_visibleCoinCount_add",
async fn(tx): Promise<void> { async fn(tx): Promise<void> {

View File

@ -96,7 +96,7 @@ export async function exportBackup(
x.purchases, x.purchases,
x.refreshGroups, x.refreshGroups,
x.backupProviders, x.backupProviders,
x.rewards, x.tips,
x.recoupGroups, x.recoupGroups,
x.withdrawalGroups, x.withdrawalGroups,
]) ])
@ -184,12 +184,12 @@ export async function exportBackup(
}); });
}); });
await tx.rewards.iter().forEach((tip) => { await tx.tips.iter().forEach((tip) => {
backupTips.push({ backupTips.push({
exchange_base_url: tip.exchangeBaseUrl, exchange_base_url: tip.exchangeBaseUrl,
merchant_base_url: tip.merchantBaseUrl, merchant_base_url: tip.merchantBaseUrl,
merchant_tip_id: tip.merchantRewardId, merchant_tip_id: tip.merchantTipId,
wallet_tip_id: tip.walletRewardId, wallet_tip_id: tip.walletTipId,
next_url: tip.next_url, next_url: tip.next_url,
secret_seed: tip.secretSeed, secret_seed: tip.secretSeed,
selected_denoms: tip.denomsSel.selectedDenoms.map((x) => ({ selected_denoms: tip.denomsSel.selectedDenoms.map((x) => ({
@ -199,8 +199,8 @@ export async function exportBackup(
timestamp_finished: tip.pickedUpTimestamp, timestamp_finished: tip.pickedUpTimestamp,
timestamp_accepted: tip.acceptedTimestamp, timestamp_accepted: tip.acceptedTimestamp,
timestamp_created: tip.createdTimestamp, timestamp_created: tip.createdTimestamp,
timestamp_expiration: tip.rewardExpiration, timestamp_expiration: tip.tipExpiration,
tip_amount_raw: Amounts.stringify(tip.rewardAmountRaw), tip_amount_raw: Amounts.stringify(tip.tipAmountRaw),
selected_denoms_uid: tip.denomSelUid, selected_denoms_uid: tip.denomSelUid,
}); });
}); });
@ -244,11 +244,11 @@ export async function exportBackup(
refresh_group_id: coin.coinSource.refreshGroupId, refresh_group_id: coin.coinSource.refreshGroupId,
}; };
break; break;
case CoinSourceType.Reward: case CoinSourceType.Tip:
bcs = { bcs = {
type: BackupCoinSourceType.Reward, type: BackupCoinSourceType.Tip,
coin_index: coin.coinSource.coinIndex, coin_index: coin.coinSource.coinIndex,
wallet_tip_id: coin.coinSource.walletRewardId, wallet_tip_id: coin.coinSource.walletTipId,
}; };
break; break;
case CoinSourceType.Withdraw: case CoinSourceType.Withdraw:

View File

@ -56,7 +56,7 @@ import {
WithdrawalGroupStatus, WithdrawalGroupStatus,
WithdrawalRecordType, WithdrawalRecordType,
RefreshOperationStatus, RefreshOperationStatus,
RewardRecordStatus, TipRecordStatus,
} from "../../db.js"; } from "../../db.js";
import { InternalWalletState } from "../../internal-wallet-state.js"; import { InternalWalletState } from "../../internal-wallet-state.js";
import { assertUnreachable } from "../../util/assertUnreachable.js"; import { assertUnreachable } from "../../util/assertUnreachable.js";
@ -250,11 +250,11 @@ export async function importCoin(
refreshGroupId: backupCoin.coin_source.refresh_group_id, refreshGroupId: backupCoin.coin_source.refresh_group_id,
}; };
break; break;
case BackupCoinSourceType.Reward: case BackupCoinSourceType.Tip:
coinSource = { coinSource = {
type: CoinSourceType.Reward, type: CoinSourceType.Tip,
coinIndex: backupCoin.coin_source.coin_index, coinIndex: backupCoin.coin_source.coin_index,
walletRewardId: backupCoin.coin_source.wallet_tip_id, walletTipId: backupCoin.coin_source.wallet_tip_id,
}; };
break; break;
case BackupCoinSourceType.Withdraw: case BackupCoinSourceType.Withdraw:
@ -311,7 +311,7 @@ export async function importBackup(
x.purchases, x.purchases,
x.refreshGroups, x.refreshGroups,
x.backupProviders, x.backupProviders,
x.rewards, x.tips,
x.recoupGroups, x.recoupGroups,
x.withdrawalGroups, x.withdrawalGroups,
x.tombstones, x.tombstones,
@ -812,13 +812,13 @@ export async function importBackup(
for (const backupTip of backupBlob.tips) { for (const backupTip of backupBlob.tips) {
const ts = constructTombstone({ const ts = constructTombstone({
tag: TombstoneTag.DeleteReward, tag: TombstoneTag.DeleteTip,
walletTipId: backupTip.wallet_tip_id, walletTipId: backupTip.wallet_tip_id,
}); });
if (tombstoneSet.has(ts)) { if (tombstoneSet.has(ts)) {
continue; continue;
} }
const existingTip = await tx.rewards.get(backupTip.wallet_tip_id); const existingTip = await tx.tips.get(backupTip.wallet_tip_id);
if (!existingTip) { if (!existingTip) {
const tipAmountRaw = Amounts.parseOrThrow(backupTip.tip_amount_raw); const tipAmountRaw = Amounts.parseOrThrow(backupTip.tip_amount_raw);
const denomsSel = await getDenomSelStateFromBackup( const denomsSel = await getDenomSelStateFromBackup(
@ -827,22 +827,22 @@ export async function importBackup(
backupTip.exchange_base_url, backupTip.exchange_base_url,
backupTip.selected_denoms, backupTip.selected_denoms,
); );
await tx.rewards.put({ await tx.tips.put({
acceptedTimestamp: backupTip.timestamp_accepted, acceptedTimestamp: backupTip.timestamp_accepted,
createdTimestamp: backupTip.timestamp_created, createdTimestamp: backupTip.timestamp_created,
denomsSel, denomsSel,
next_url: backupTip.next_url, next_url: backupTip.next_url,
exchangeBaseUrl: backupTip.exchange_base_url, exchangeBaseUrl: backupTip.exchange_base_url,
merchantBaseUrl: backupTip.exchange_base_url, merchantBaseUrl: backupTip.exchange_base_url,
merchantRewardId: backupTip.merchant_tip_id, merchantTipId: backupTip.merchant_tip_id,
pickedUpTimestamp: backupTip.timestamp_finished, pickedUpTimestamp: backupTip.timestamp_finished,
secretSeed: backupTip.secret_seed, secretSeed: backupTip.secret_seed,
rewardAmountEffective: Amounts.stringify(denomsSel.totalCoinValue), tipAmountEffective: Amounts.stringify(denomsSel.totalCoinValue),
rewardAmountRaw: Amounts.stringify(tipAmountRaw), tipAmountRaw: Amounts.stringify(tipAmountRaw),
rewardExpiration: backupTip.timestamp_expiration, tipExpiration: backupTip.timestamp_expiration,
walletRewardId: backupTip.wallet_tip_id, walletTipId: backupTip.wallet_tip_id,
denomSelUid: backupTip.selected_denoms_uid, denomSelUid: backupTip.selected_denoms_uid,
status: RewardRecordStatus.Done, // FIXME! status: TipRecordStatus.Done, // FIXME!
}); });
} }
} }
@ -863,8 +863,8 @@ export async function importBackup(
} else if (type === TombstoneTag.DeleteRefund) { } else if (type === TombstoneTag.DeleteRefund) {
// Nothing required, will just prevent display // Nothing required, will just prevent display
// in the transactions list // in the transactions list
} else if (type === TombstoneTag.DeleteReward) { } else if (type === TombstoneTag.DeleteTip) {
await tx.rewards.delete(rest[0]); await tx.tips.delete(rest[0]);
} else if (type === TombstoneTag.DeleteWithdrawalGroup) { } else if (type === TombstoneTag.DeleteWithdrawalGroup) {
await tx.withdrawalGroups.delete(rest[0]); await tx.withdrawalGroups.delete(rest[0]);
} else { } else {

View File

@ -57,7 +57,7 @@ import {
PurchaseRecord, PurchaseRecord,
RecoupGroupRecord, RecoupGroupRecord,
RefreshGroupRecord, RefreshGroupRecord,
RewardRecord, TipRecord,
WithdrawalGroupRecord, WithdrawalGroupRecord,
} from "../db.js"; } from "../db.js";
import { makeErrorDetail, TalerError } from "@gnu-taler/taler-util"; import { makeErrorDetail, TalerError } from "@gnu-taler/taler-util";
@ -293,10 +293,10 @@ function convertTaskToTransactionId(
tag: TransactionType.Refresh, tag: TransactionType.Refresh,
refreshGroupId: parsedTaskId.refreshGroupId, refreshGroupId: parsedTaskId.refreshGroupId,
}); });
case PendingTaskType.RewardPickup: case PendingTaskType.TipPickup:
return constructTransactionIdentifier({ return constructTransactionIdentifier({
tag: TransactionType.Reward, tag: TransactionType.Tip,
walletRewardId: parsedTaskId.walletRewardId, walletTipId: parsedTaskId.walletTipId,
}); });
case PendingTaskType.PeerPushDebit: case PendingTaskType.PeerPushDebit:
return constructTransactionIdentifier({ return constructTransactionIdentifier({
@ -515,7 +515,7 @@ export enum TombstoneTag {
DeleteWithdrawalGroup = "delete-withdrawal-group", DeleteWithdrawalGroup = "delete-withdrawal-group",
DeleteReserve = "delete-reserve", DeleteReserve = "delete-reserve",
DeletePayment = "delete-payment", DeletePayment = "delete-payment",
DeleteReward = "delete-reward", DeleteTip = "delete-tip",
DeleteRefreshGroup = "delete-refresh-group", DeleteRefreshGroup = "delete-refresh-group",
DeleteDepositGroup = "delete-deposit-group", DeleteDepositGroup = "delete-deposit-group",
DeleteRefund = "delete-refund", DeleteRefund = "delete-refund",
@ -601,9 +601,7 @@ export function runLongpollAsync(
}; };
res = await reqFn(cts.token); res = await reqFn(cts.token);
} catch (e) { } catch (e) {
const errDetail = getErrorDetailFromException(e); await storePendingTaskError(ws, retryTag, getErrorDetailFromException(e));
logger.warn(`got error during long-polling: ${j2s(errDetail)}`);
await storePendingTaskError(ws, retryTag, errDetail);
return; return;
} finally { } finally {
delete ws.activeLongpoll[retryTag]; delete ws.activeLongpoll[retryTag];
@ -624,7 +622,7 @@ export type ParsedTombstone =
| { tag: TombstoneTag.DeleteRefund; refundGroupId: string } | { tag: TombstoneTag.DeleteRefund; refundGroupId: string }
| { tag: TombstoneTag.DeleteReserve; reservePub: string } | { tag: TombstoneTag.DeleteReserve; reservePub: string }
| { tag: TombstoneTag.DeleteRefreshGroup; refreshGroupId: string } | { tag: TombstoneTag.DeleteRefreshGroup; refreshGroupId: string }
| { tag: TombstoneTag.DeleteReward; walletTipId: string } | { tag: TombstoneTag.DeleteTip; walletTipId: string }
| { tag: TombstoneTag.DeletePayment; proposalId: string }; | { tag: TombstoneTag.DeletePayment; proposalId: string };
export function constructTombstone(p: ParsedTombstone): TombstoneIdStr { export function constructTombstone(p: ParsedTombstone): TombstoneIdStr {
@ -639,7 +637,7 @@ export function constructTombstone(p: ParsedTombstone): TombstoneIdStr {
return `tmb:${p.tag}:${p.proposalId}` as TombstoneIdStr; return `tmb:${p.tag}:${p.proposalId}` as TombstoneIdStr;
case TombstoneTag.DeleteRefreshGroup: case TombstoneTag.DeleteRefreshGroup:
return `tmb:${p.tag}:${p.refreshGroupId}` as TombstoneIdStr; return `tmb:${p.tag}:${p.refreshGroupId}` as TombstoneIdStr;
case TombstoneTag.DeleteReward: case TombstoneTag.DeleteTip:
return `tmb:${p.tag}:${p.walletTipId}` as TombstoneIdStr; return `tmb:${p.tag}:${p.walletTipId}` as TombstoneIdStr;
default: default:
assertUnreachable(p); assertUnreachable(p);
@ -812,7 +810,7 @@ export type ParsedTaskIdentifier =
| { tag: PendingTaskType.PeerPushDebit; pursePub: string } | { tag: PendingTaskType.PeerPushDebit; pursePub: string }
| { tag: PendingTaskType.Purchase; proposalId: string } | { tag: PendingTaskType.Purchase; proposalId: string }
| { tag: PendingTaskType.Recoup; recoupGroupId: string } | { tag: PendingTaskType.Recoup; recoupGroupId: string }
| { tag: PendingTaskType.RewardPickup; walletRewardId: string } | { tag: PendingTaskType.TipPickup; walletTipId: string }
| { tag: PendingTaskType.Refresh; refreshGroupId: string }; | { tag: PendingTaskType.Refresh; refreshGroupId: string };
export function parseTaskIdentifier(x: string): ParsedTaskIdentifier { export function parseTaskIdentifier(x: string): ParsedTaskIdentifier {
@ -846,8 +844,8 @@ export function parseTaskIdentifier(x: string): ParsedTaskIdentifier {
return { tag: type, recoupGroupId: rest[0] }; return { tag: type, recoupGroupId: rest[0] };
case PendingTaskType.Refresh: case PendingTaskType.Refresh:
return { tag: type, refreshGroupId: rest[0] }; return { tag: type, refreshGroupId: rest[0] };
case PendingTaskType.RewardPickup: case PendingTaskType.TipPickup:
return { tag: type, walletRewardId: rest[0] }; return { tag: type, walletTipId: rest[0] };
case PendingTaskType.Withdraw: case PendingTaskType.Withdraw:
return { tag: type, withdrawalGroupId: rest[0] }; return { tag: type, withdrawalGroupId: rest[0] };
default: default:
@ -879,8 +877,8 @@ export function constructTaskIdentifier(p: ParsedTaskIdentifier): TaskId {
return `${p.tag}:${p.recoupGroupId}` as TaskId; return `${p.tag}:${p.recoupGroupId}` as TaskId;
case PendingTaskType.Refresh: case PendingTaskType.Refresh:
return `${p.tag}:${p.refreshGroupId}` as TaskId; return `${p.tag}:${p.refreshGroupId}` as TaskId;
case PendingTaskType.RewardPickup: case PendingTaskType.TipPickup:
return `${p.tag}:${p.walletRewardId}` as TaskId; return `${p.tag}:${p.walletTipId}` as TaskId;
case PendingTaskType.Withdraw: case PendingTaskType.Withdraw:
return `${p.tag}:${p.withdrawalGroupId}` as TaskId; return `${p.tag}:${p.withdrawalGroupId}` as TaskId;
default: default:
@ -901,8 +899,8 @@ export namespace TaskIdentifiers {
export function forExchangeCheckRefresh(exch: ExchangeRecord): TaskId { export function forExchangeCheckRefresh(exch: ExchangeRecord): TaskId {
return `${PendingTaskType.ExchangeCheckRefresh}:${exch.baseUrl}` as TaskId; return `${PendingTaskType.ExchangeCheckRefresh}:${exch.baseUrl}` as TaskId;
} }
export function forTipPickup(tipRecord: RewardRecord): TaskId { export function forTipPickup(tipRecord: TipRecord): TaskId {
return `${PendingTaskType.RewardPickup}:${tipRecord.walletRewardId}` as TaskId; return `${PendingTaskType.TipPickup}:${tipRecord.walletTipId}` as TaskId;
} }
export function forRefresh(refreshGroupRecord: RefreshGroupRecord): TaskId { export function forRefresh(refreshGroupRecord: RefreshGroupRecord): TaskId {
return `${PendingTaskType.Refresh}:${refreshGroupRecord.refreshGroupId}` as TaskId; return `${PendingTaskType.Refresh}:${refreshGroupRecord.refreshGroupId}` as TaskId;

View File

@ -436,25 +436,16 @@ async function handlePeerPullCreditCreatePurse(
logger.info(`reserve merge response: ${j2s(resp)}`); logger.info(`reserve merge response: ${j2s(resp)}`);
const transactionId = constructTransactionIdentifier({ await ws.db
tag: TransactionType.PeerPullCredit,
pursePub: pullIni.pursePub,
});
const transitionInfo = await ws.db
.mktx((x) => [x.peerPullPaymentInitiations]) .mktx((x) => [x.peerPullPaymentInitiations])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const pi2 = await tx.peerPullPaymentInitiations.get(pursePub); const pi2 = await tx.peerPullPaymentInitiations.get(pursePub);
if (!pi2) { if (!pi2) {
return; return;
} }
const oldTxState = computePeerPullCreditTransactionState(pi2);
pi2.status = PeerPullPaymentInitiationStatus.PendingReady; pi2.status = PeerPullPaymentInitiationStatus.PendingReady;
await tx.peerPullPaymentInitiations.put(pi2); await tx.peerPullPaymentInitiations.put(pi2);
const newTxState = computePeerPullCreditTransactionState(pi2);
return { oldTxState, newTxState };
}); });
notifyTransition(ws, transactionId, transitionInfo);
return TaskRunResult.finished(); return TaskRunResult.finished();
} }

View File

@ -32,7 +32,7 @@ import {
PeerPushPaymentIncomingStatus, PeerPushPaymentIncomingStatus,
PeerPullPaymentInitiationStatus, PeerPullPaymentInitiationStatus,
WithdrawalGroupStatus, WithdrawalGroupStatus,
RewardRecordStatus, TipRecordStatus,
DepositOperationStatus, DepositOperationStatus,
} from "../db.js"; } from "../db.js";
import { import {
@ -232,17 +232,17 @@ async function gatherDepositPending(
async function gatherTipPending( async function gatherTipPending(
ws: InternalWalletState, ws: InternalWalletState,
tx: GetReadOnlyAccess<{ tx: GetReadOnlyAccess<{
rewards: typeof WalletStoresV1.rewards; tips: typeof WalletStoresV1.tips;
operationRetries: typeof WalletStoresV1.operationRetries; operationRetries: typeof WalletStoresV1.operationRetries;
}>, }>,
now: AbsoluteTime, now: AbsoluteTime,
resp: PendingOperationsResponse, resp: PendingOperationsResponse,
): Promise<void> { ): Promise<void> {
const range = GlobalIDB.KeyRange.bound( const range = GlobalIDB.KeyRange.bound(
RewardRecordStatus.PendingPickup, TipRecordStatus.PendingPickup,
RewardRecordStatus.PendingPickup, TipRecordStatus.PendingPickup,
); );
await tx.rewards.indexes.byStatus.iter(range).forEachAsync(async (tip) => { await tx.tips.indexes.byStatus.iter(range).forEachAsync(async (tip) => {
// FIXME: The tip record needs a proper status field! // FIXME: The tip record needs a proper status field!
if (tip.pickedUpTimestamp) { if (tip.pickedUpTimestamp) {
return; return;
@ -252,13 +252,13 @@ async function gatherTipPending(
const timestampDue = retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now(); const timestampDue = retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
if (tip.acceptedTimestamp) { if (tip.acceptedTimestamp) {
resp.pendingOperations.push({ resp.pendingOperations.push({
type: PendingTaskType.RewardPickup, type: PendingTaskType.TipPickup,
...getPendingCommon(ws, opId, timestampDue), ...getPendingCommon(ws, opId, timestampDue),
givesLifeness: true, givesLifeness: true,
timestampDue: retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now(), timestampDue: retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now(),
merchantBaseUrl: tip.merchantBaseUrl, merchantBaseUrl: tip.merchantBaseUrl,
tipId: tip.walletRewardId, tipId: tip.walletTipId,
merchantTipId: tip.merchantRewardId, merchantTipId: tip.merchantTipId,
}); });
} }
}); });
@ -494,7 +494,7 @@ export async function getPendingOperations(
x.refreshGroups, x.refreshGroups,
x.coins, x.coins,
x.withdrawalGroups, x.withdrawalGroups,
x.rewards, x.tips,
x.purchases, x.purchases,
x.planchets, x.planchets,
x.depositGroups, x.depositGroups,

View File

@ -82,7 +82,7 @@ async function putGroupAsFinished(
await tx.recoupGroups.put(recoupGroup); await tx.recoupGroups.put(recoupGroup);
} }
async function recoupRewardCoin( async function recoupTipCoin(
ws: InternalWalletState, ws: InternalWalletState,
recoupGroupId: string, recoupGroupId: string,
coinIdx: number, coinIdx: number,
@ -482,8 +482,8 @@ async function processRecoup(
const cs = coin.coinSource; const cs = coin.coinSource;
switch (cs.type) { switch (cs.type) {
case CoinSourceType.Reward: case CoinSourceType.Tip:
return recoupRewardCoin(ws, recoupGroupId, coinIdx, coin); return recoupTipCoin(ws, recoupGroupId, coinIdx, coin);
case CoinSourceType.Refresh: case CoinSourceType.Refresh:
return recoupRefreshCoin(ws, recoupGroupId, coinIdx, coin, cs); return recoupRefreshCoin(ws, recoupGroupId, coinIdx, coin, cs);
case CoinSourceType.Withdraw: case CoinSourceType.Withdraw:

View File

@ -31,7 +31,7 @@ import {
j2s, j2s,
Logger, Logger,
NotificationType, NotificationType,
parseRewardUri, parseTipUri,
PrepareTipResult, PrepareTipResult,
TalerErrorCode, TalerErrorCode,
TalerPreciseTimestamp, TalerPreciseTimestamp,
@ -48,8 +48,8 @@ import {
CoinRecord, CoinRecord,
CoinSourceType, CoinSourceType,
DenominationRecord, DenominationRecord,
RewardRecord, TipRecord,
RewardRecordStatus, TipRecordStatus,
} from "../db.js"; } from "../db.js";
import { makeErrorDetail } from "@gnu-taler/taler-util"; import { makeErrorDetail } from "@gnu-taler/taler-util";
import { InternalWalletState } from "../internal-wallet-state.js"; import { InternalWalletState } from "../internal-wallet-state.js";
@ -84,31 +84,31 @@ const logger = new Logger("operations/tip.ts");
/** /**
* Get the (DD37-style) transaction status based on the * Get the (DD37-style) transaction status based on the
* database record of a reward. * database record of a tip.
*/ */
export function computeRewardTransactionStatus( export function computeTipTransactionStatus(
tipRecord: RewardRecord, tipRecord: TipRecord,
): TransactionState { ): TransactionState {
switch (tipRecord.status) { switch (tipRecord.status) {
case RewardRecordStatus.Done: case TipRecordStatus.Done:
return { return {
major: TransactionMajorState.Done, major: TransactionMajorState.Done,
}; };
case RewardRecordStatus.Aborted: case TipRecordStatus.Aborted:
return { return {
major: TransactionMajorState.Aborted, major: TransactionMajorState.Aborted,
}; };
case RewardRecordStatus.PendingPickup: case TipRecordStatus.PendingPickup:
return { return {
major: TransactionMajorState.Pending, major: TransactionMajorState.Pending,
minor: TransactionMinorState.Pickup, minor: TransactionMinorState.Pickup,
}; };
case RewardRecordStatus.DialogAccept: case TipRecordStatus.DialogAccept:
return { return {
major: TransactionMajorState.Dialog, major: TransactionMajorState.Dialog,
minor: TransactionMinorState.Proposed, minor: TransactionMinorState.Proposed,
}; };
case RewardRecordStatus.SuspendidPickup: case TipRecordStatus.SuspendidPickup:
return { return {
major: TransactionMajorState.Pending, major: TransactionMajorState.Pending,
minor: TransactionMinorState.Pickup, minor: TransactionMinorState.Pickup,
@ -119,18 +119,18 @@ export function computeRewardTransactionStatus(
} }
export function computeTipTransactionActions( export function computeTipTransactionActions(
tipRecord: RewardRecord, tipRecord: TipRecord,
): TransactionAction[] { ): TransactionAction[] {
switch (tipRecord.status) { switch (tipRecord.status) {
case RewardRecordStatus.Done: case TipRecordStatus.Done:
return [TransactionAction.Delete]; return [TransactionAction.Delete];
case RewardRecordStatus.Aborted: case TipRecordStatus.Aborted:
return [TransactionAction.Delete]; return [TransactionAction.Delete];
case RewardRecordStatus.PendingPickup: case TipRecordStatus.PendingPickup:
return [TransactionAction.Suspend, TransactionAction.Fail]; return [TransactionAction.Suspend, TransactionAction.Fail];
case RewardRecordStatus.SuspendidPickup: case TipRecordStatus.SuspendidPickup:
return [TransactionAction.Resume, TransactionAction.Fail]; return [TransactionAction.Resume, TransactionAction.Fail];
case RewardRecordStatus.DialogAccept: case TipRecordStatus.DialogAccept:
return [TransactionAction.Abort]; return [TransactionAction.Abort];
default: default:
assertUnreachable(tipRecord.status); assertUnreachable(tipRecord.status);
@ -141,15 +141,15 @@ export async function prepareTip(
ws: InternalWalletState, ws: InternalWalletState,
talerTipUri: string, talerTipUri: string,
): Promise<PrepareTipResult> { ): Promise<PrepareTipResult> {
const res = parseRewardUri(talerTipUri); const res = parseTipUri(talerTipUri);
if (!res) { if (!res) {
throw Error("invalid taler://tip URI"); throw Error("invalid taler://tip URI");
} }
let tipRecord = await ws.db let tipRecord = await ws.db
.mktx((x) => [x.rewards]) .mktx((x) => [x.tips])
.runReadOnly(async (tx) => { .runReadOnly(async (tx) => {
return tx.rewards.indexes.byMerchantTipIdAndBaseUrl.get([ return tx.tips.indexes.byMerchantTipIdAndBaseUrl.get([
res.merchantTipId, res.merchantTipId,
res.merchantBaseUrl, res.merchantBaseUrl,
]); ]);
@ -194,44 +194,44 @@ export async function prepareTip(
const secretSeed = encodeCrock(getRandomBytes(64)); const secretSeed = encodeCrock(getRandomBytes(64));
const denomSelUid = encodeCrock(getRandomBytes(32)); const denomSelUid = encodeCrock(getRandomBytes(32));
const newTipRecord: RewardRecord = { const newTipRecord: TipRecord = {
walletRewardId: walletTipId, walletTipId: walletTipId,
acceptedTimestamp: undefined, acceptedTimestamp: undefined,
status: RewardRecordStatus.DialogAccept, status: TipRecordStatus.DialogAccept,
rewardAmountRaw: Amounts.stringify(amount), tipAmountRaw: Amounts.stringify(amount),
rewardExpiration: tipPickupStatus.expiration, tipExpiration: tipPickupStatus.expiration,
exchangeBaseUrl: tipPickupStatus.exchange_url, exchangeBaseUrl: tipPickupStatus.exchange_url,
next_url: tipPickupStatus.next_url, next_url: tipPickupStatus.next_url,
merchantBaseUrl: res.merchantBaseUrl, merchantBaseUrl: res.merchantBaseUrl,
createdTimestamp: TalerPreciseTimestamp.now(), createdTimestamp: TalerPreciseTimestamp.now(),
merchantRewardId: res.merchantTipId, merchantTipId: res.merchantTipId,
rewardAmountEffective: Amounts.stringify(selectedDenoms.totalCoinValue), tipAmountEffective: Amounts.stringify(selectedDenoms.totalCoinValue),
denomsSel: selectedDenoms, denomsSel: selectedDenoms,
pickedUpTimestamp: undefined, pickedUpTimestamp: undefined,
secretSeed, secretSeed,
denomSelUid, denomSelUid,
}; };
await ws.db await ws.db
.mktx((x) => [x.rewards]) .mktx((x) => [x.tips])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
await tx.rewards.put(newTipRecord); await tx.tips.put(newTipRecord);
}); });
tipRecord = newTipRecord; tipRecord = newTipRecord;
} }
const transactionId = constructTransactionIdentifier({ const transactionId = constructTransactionIdentifier({
tag: TransactionType.Reward, tag: TransactionType.Tip,
walletRewardId: tipRecord.walletRewardId, walletTipId: tipRecord.walletTipId,
}); });
const tipStatus: PrepareTipResult = { const tipStatus: PrepareTipResult = {
accepted: !!tipRecord && !!tipRecord.acceptedTimestamp, accepted: !!tipRecord && !!tipRecord.acceptedTimestamp,
rewardAmountRaw: Amounts.stringify(tipRecord.rewardAmountRaw), tipAmountRaw: Amounts.stringify(tipRecord.tipAmountRaw),
exchangeBaseUrl: tipRecord.exchangeBaseUrl, exchangeBaseUrl: tipRecord.exchangeBaseUrl,
merchantBaseUrl: tipRecord.merchantBaseUrl, merchantBaseUrl: tipRecord.merchantBaseUrl,
expirationTimestamp: tipRecord.rewardExpiration, expirationTimestamp: tipRecord.tipExpiration,
rewardAmountEffective: Amounts.stringify(tipRecord.rewardAmountEffective), tipAmountEffective: Amounts.stringify(tipRecord.tipAmountEffective),
walletRewardId: tipRecord.walletRewardId, walletTipId: tipRecord.walletTipId,
transactionId, transactionId,
}; };
@ -243,25 +243,25 @@ export async function processTip(
walletTipId: string, walletTipId: string,
): Promise<TaskRunResult> { ): Promise<TaskRunResult> {
const tipRecord = await ws.db const tipRecord = await ws.db
.mktx((x) => [x.rewards]) .mktx((x) => [x.tips])
.runReadOnly(async (tx) => { .runReadOnly(async (tx) => {
return tx.rewards.get(walletTipId); return tx.tips.get(walletTipId);
}); });
if (!tipRecord) { if (!tipRecord) {
return TaskRunResult.finished(); return TaskRunResult.finished();
} }
switch (tipRecord.status) { switch (tipRecord.status) {
case RewardRecordStatus.Aborted: case TipRecordStatus.Aborted:
case RewardRecordStatus.DialogAccept: case TipRecordStatus.DialogAccept:
case RewardRecordStatus.Done: case TipRecordStatus.Done:
case RewardRecordStatus.SuspendidPickup: case TipRecordStatus.SuspendidPickup:
return TaskRunResult.finished(); return TaskRunResult.finished();
} }
const transactionId = constructTransactionIdentifier({ const transactionId = constructTransactionIdentifier({
tag: TransactionType.Reward, tag: TransactionType.Tip,
walletRewardId: walletTipId, walletTipId,
}); });
const denomsForWithdraw = tipRecord.denomsSel; const denomsForWithdraw = tipRecord.denomsSel;
@ -300,7 +300,7 @@ export async function processTip(
} }
const tipStatusUrl = new URL( const tipStatusUrl = new URL(
`tips/${tipRecord.merchantRewardId}/pickup`, `tips/${tipRecord.merchantTipId}/pickup`,
tipRecord.merchantBaseUrl, tipRecord.merchantBaseUrl,
); );
@ -384,9 +384,9 @@ export async function processTip(
coinPriv: planchet.coinPriv, coinPriv: planchet.coinPriv,
coinPub: planchet.coinPub, coinPub: planchet.coinPub,
coinSource: { coinSource: {
type: CoinSourceType.Reward, type: CoinSourceType.Tip,
coinIndex: i, coinIndex: i,
walletRewardId: walletTipId, walletTipId: walletTipId,
}, },
sourceTransactionId: transactionId, sourceTransactionId: transactionId,
denomPubHash: denom.denomPubHash, denomPubHash: denom.denomPubHash,
@ -401,20 +401,20 @@ export async function processTip(
} }
const transitionInfo = await ws.db const transitionInfo = await ws.db
.mktx((x) => [x.coins, x.coinAvailability, x.denominations, x.rewards]) .mktx((x) => [x.coins, x.coinAvailability, x.denominations, x.tips])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const tr = await tx.rewards.get(walletTipId); const tr = await tx.tips.get(walletTipId);
if (!tr) { if (!tr) {
return; return;
} }
if (tr.status !== RewardRecordStatus.PendingPickup) { if (tr.status !== TipRecordStatus.PendingPickup) {
return; return;
} }
const oldTxState = computeRewardTransactionStatus(tr); const oldTxState = computeTipTransactionStatus(tr);
tr.pickedUpTimestamp = TalerPreciseTimestamp.now(); tr.pickedUpTimestamp = TalerPreciseTimestamp.now();
tr.status = RewardRecordStatus.Done; tr.status = TipRecordStatus.Done;
await tx.rewards.put(tr); await tx.tips.put(tr);
const newTxState = computeRewardTransactionStatus(tr); const newTxState = computeTipTransactionStatus(tr);
for (const cr of newCoinRecords) { for (const cr of newCoinRecords) {
await makeCoinAvailable(ws, tx, cr); await makeCoinAvailable(ws, tx, cr);
} }
@ -432,26 +432,26 @@ export async function acceptTip(
walletTipId: string, walletTipId: string,
): Promise<AcceptTipResponse> { ): Promise<AcceptTipResponse> {
const transactionId = constructTransactionIdentifier({ const transactionId = constructTransactionIdentifier({
tag: TransactionType.Reward, tag: TransactionType.Tip,
walletRewardId: walletTipId, walletTipId,
}); });
const dbRes = await ws.db const dbRes = await ws.db
.mktx((x) => [x.rewards]) .mktx((x) => [x.tips])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const tipRecord = await tx.rewards.get(walletTipId); const tipRecord = await tx.tips.get(walletTipId);
if (!tipRecord) { if (!tipRecord) {
logger.error("tip not found"); logger.error("tip not found");
return; return;
} }
if (tipRecord.status != RewardRecordStatus.DialogAccept) { if (tipRecord.status != TipRecordStatus.DialogAccept) {
logger.warn("Unable to accept tip in the current state"); logger.warn("Unable to accept tip in the current state");
return { tipRecord }; return { tipRecord };
} }
const oldTxState = computeRewardTransactionStatus(tipRecord); const oldTxState = computeTipTransactionStatus(tipRecord);
tipRecord.acceptedTimestamp = TalerPreciseTimestamp.now(); tipRecord.acceptedTimestamp = TalerPreciseTimestamp.now();
tipRecord.status = RewardRecordStatus.PendingPickup; tipRecord.status = TipRecordStatus.PendingPickup;
await tx.rewards.put(tipRecord); await tx.tips.put(tipRecord);
const newTxState = computeRewardTransactionStatus(tipRecord); const newTxState = computeTipTransactionStatus(tipRecord);
return { tipRecord, transitionInfo: { oldTxState, newTxState } }; return { tipRecord, transitionInfo: { oldTxState, newTxState } };
}); });
@ -465,53 +465,53 @@ export async function acceptTip(
return { return {
transactionId: constructTransactionIdentifier({ transactionId: constructTransactionIdentifier({
tag: TransactionType.Reward, tag: TransactionType.Tip,
walletRewardId: walletTipId, walletTipId: walletTipId,
}), }),
next_url: tipRecord.next_url, next_url: tipRecord.next_url,
}; };
} }
export async function suspendRewardTransaction( export async function suspendTipTransaction(
ws: InternalWalletState, ws: InternalWalletState,
walletRewardId: string, walletTipId: string,
): Promise<void> { ): Promise<void> {
const taskId = constructTaskIdentifier({ const taskId = constructTaskIdentifier({
tag: PendingTaskType.RewardPickup, tag: PendingTaskType.TipPickup,
walletRewardId: walletRewardId, walletTipId,
}); });
const transactionId = constructTransactionIdentifier({ const transactionId = constructTransactionIdentifier({
tag: TransactionType.Reward, tag: TransactionType.Tip,
walletRewardId: walletRewardId, walletTipId,
}); });
stopLongpolling(ws, taskId); stopLongpolling(ws, taskId);
const transitionInfo = await ws.db const transitionInfo = await ws.db
.mktx((x) => [x.rewards]) .mktx((x) => [x.tips])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const tipRec = await tx.rewards.get(walletRewardId); const tipRec = await tx.tips.get(walletTipId);
if (!tipRec) { if (!tipRec) {
logger.warn(`transaction tip ${walletRewardId} not found`); logger.warn(`transaction tip ${walletTipId} not found`);
return; return;
} }
let newStatus: RewardRecordStatus | undefined = undefined; let newStatus: TipRecordStatus | undefined = undefined;
switch (tipRec.status) { switch (tipRec.status) {
case RewardRecordStatus.Done: case TipRecordStatus.Done:
case RewardRecordStatus.SuspendidPickup: case TipRecordStatus.SuspendidPickup:
case RewardRecordStatus.Aborted: case TipRecordStatus.Aborted:
case RewardRecordStatus.DialogAccept: case TipRecordStatus.DialogAccept:
break; break;
case RewardRecordStatus.PendingPickup: case TipRecordStatus.PendingPickup:
newStatus = RewardRecordStatus.SuspendidPickup; newStatus = TipRecordStatus.SuspendidPickup;
break; break;
default: default:
assertUnreachable(tipRec.status); assertUnreachable(tipRec.status);
} }
if (newStatus != null) { if (newStatus != null) {
const oldTxState = computeRewardTransactionStatus(tipRec); const oldTxState = computeTipTransactionStatus(tipRec);
tipRec.status = newStatus; tipRec.status = newStatus;
const newTxState = computeRewardTransactionStatus(tipRec); const newTxState = computeTipTransactionStatus(tipRec);
await tx.rewards.put(tipRec); await tx.tips.put(tipRec);
return { return {
oldTxState, oldTxState,
newTxState, newTxState,
@ -525,43 +525,43 @@ export async function suspendRewardTransaction(
export async function resumeTipTransaction( export async function resumeTipTransaction(
ws: InternalWalletState, ws: InternalWalletState,
walletRewardId: string, walletTipId: string,
): Promise<void> { ): Promise<void> {
const taskId = constructTaskIdentifier({ const taskId = constructTaskIdentifier({
tag: PendingTaskType.RewardPickup, tag: PendingTaskType.TipPickup,
walletRewardId: walletRewardId, walletTipId,
}); });
const transactionId = constructTransactionIdentifier({ const transactionId = constructTransactionIdentifier({
tag: TransactionType.Reward, tag: TransactionType.Tip,
walletRewardId: walletRewardId, walletTipId,
}); });
stopLongpolling(ws, taskId); stopLongpolling(ws, taskId);
const transitionInfo = await ws.db const transitionInfo = await ws.db
.mktx((x) => [x.rewards]) .mktx((x) => [x.tips])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const rewardRec = await tx.rewards.get(walletRewardId); const tipRec = await tx.tips.get(walletTipId);
if (!rewardRec) { if (!tipRec) {
logger.warn(`transaction reward ${walletRewardId} not found`); logger.warn(`transaction tip ${walletTipId} not found`);
return; return;
} }
let newStatus: RewardRecordStatus | undefined = undefined; let newStatus: TipRecordStatus | undefined = undefined;
switch (rewardRec.status) { switch (tipRec.status) {
case RewardRecordStatus.Done: case TipRecordStatus.Done:
case RewardRecordStatus.PendingPickup: case TipRecordStatus.PendingPickup:
case RewardRecordStatus.Aborted: case TipRecordStatus.Aborted:
case RewardRecordStatus.DialogAccept: case TipRecordStatus.DialogAccept:
break; break;
case RewardRecordStatus.SuspendidPickup: case TipRecordStatus.SuspendidPickup:
newStatus = RewardRecordStatus.PendingPickup; newStatus = TipRecordStatus.PendingPickup;
break; break;
default: default:
assertUnreachable(rewardRec.status); assertUnreachable(tipRec.status);
} }
if (newStatus != null) { if (newStatus != null) {
const oldTxState = computeRewardTransactionStatus(rewardRec); const oldTxState = computeTipTransactionStatus(tipRec);
rewardRec.status = newStatus; tipRec.status = newStatus;
const newTxState = computeRewardTransactionStatus(rewardRec); const newTxState = computeTipTransactionStatus(tipRec);
await tx.rewards.put(rewardRec); await tx.tips.put(tipRec);
return { return {
oldTxState, oldTxState,
newTxState, newTxState,
@ -582,43 +582,43 @@ export async function failTipTransaction(
export async function abortTipTransaction( export async function abortTipTransaction(
ws: InternalWalletState, ws: InternalWalletState,
walletRewardId: string, walletTipId: string,
): Promise<void> { ): Promise<void> {
const taskId = constructTaskIdentifier({ const taskId = constructTaskIdentifier({
tag: PendingTaskType.RewardPickup, tag: PendingTaskType.TipPickup,
walletRewardId: walletRewardId, walletTipId,
}); });
const transactionId = constructTransactionIdentifier({ const transactionId = constructTransactionIdentifier({
tag: TransactionType.Reward, tag: TransactionType.Tip,
walletRewardId: walletRewardId, walletTipId,
}); });
stopLongpolling(ws, taskId); stopLongpolling(ws, taskId);
const transitionInfo = await ws.db const transitionInfo = await ws.db
.mktx((x) => [x.rewards]) .mktx((x) => [x.tips])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const tipRec = await tx.rewards.get(walletRewardId); const tipRec = await tx.tips.get(walletTipId);
if (!tipRec) { if (!tipRec) {
logger.warn(`transaction tip ${walletRewardId} not found`); logger.warn(`transaction tip ${walletTipId} not found`);
return; return;
} }
let newStatus: RewardRecordStatus | undefined = undefined; let newStatus: TipRecordStatus | undefined = undefined;
switch (tipRec.status) { switch (tipRec.status) {
case RewardRecordStatus.Done: case TipRecordStatus.Done:
case RewardRecordStatus.Aborted: case TipRecordStatus.Aborted:
case RewardRecordStatus.PendingPickup: case TipRecordStatus.PendingPickup:
case RewardRecordStatus.DialogAccept: case TipRecordStatus.DialogAccept:
break; break;
case RewardRecordStatus.SuspendidPickup: case TipRecordStatus.SuspendidPickup:
newStatus = RewardRecordStatus.Aborted; newStatus = TipRecordStatus.Aborted;
break; break;
default: default:
assertUnreachable(tipRec.status); assertUnreachable(tipRec.status);
} }
if (newStatus != null) { if (newStatus != null) {
const oldTxState = computeRewardTransactionStatus(tipRec); const oldTxState = computeTipTransactionStatus(tipRec);
tipRec.status = newStatus; tipRec.status = newStatus;
const newTxState = computeRewardTransactionStatus(tipRec); const newTxState = computeTipTransactionStatus(tipRec);
await tx.rewards.put(tipRec); await tx.tips.put(tipRec);
return { return {
oldTxState, oldTxState,
newTxState, newTxState,

View File

@ -58,7 +58,7 @@ import {
RefreshGroupRecord, RefreshGroupRecord,
RefreshOperationStatus, RefreshOperationStatus,
RefundGroupRecord, RefundGroupRecord,
RewardRecord, TipRecord,
WalletContractData, WalletContractData,
WithdrawalGroupRecord, WithdrawalGroupRecord,
WithdrawalGroupStatus, WithdrawalGroupStatus,
@ -107,11 +107,11 @@ import {
import { import {
abortTipTransaction, abortTipTransaction,
failTipTransaction, failTipTransaction,
computeRewardTransactionStatus, computeTipTransactionStatus,
resumeTipTransaction, resumeTipTransaction,
suspendRewardTransaction, suspendTipTransaction,
computeTipTransactionActions, computeTipTransactionActions,
} from "./reward.js"; } from "./tip.js";
import { import {
abortWithdrawalTransaction, abortWithdrawalTransaction,
augmentPaytoUrisForWithdrawal, augmentPaytoUrisForWithdrawal,
@ -187,7 +187,7 @@ function shouldSkipSearch(
*/ */
const txOrder: { [t in TransactionType]: number } = { const txOrder: { [t in TransactionType]: number } = {
[TransactionType.Withdrawal]: 1, [TransactionType.Withdrawal]: 1,
[TransactionType.Reward]: 2, [TransactionType.Tip]: 2,
[TransactionType.Payment]: 3, [TransactionType.Payment]: 3,
[TransactionType.PeerPullCredit]: 4, [TransactionType.PeerPullCredit]: 4,
[TransactionType.PeerPullDebit]: 5, [TransactionType.PeerPullDebit]: 5,
@ -284,12 +284,12 @@ export async function getTransactionById(
throw Error(`no tx for refresh`); throw Error(`no tx for refresh`);
} }
case TransactionType.Reward: { case TransactionType.Tip: {
const tipId = parsedTx.walletRewardId; const tipId = parsedTx.walletTipId;
return await ws.db return await ws.db
.mktx((x) => [x.rewards, x.operationRetries]) .mktx((x) => [x.tips, x.operationRetries])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const tipRecord = await tx.rewards.get(tipId); const tipRecord = await tx.tips.get(tipId);
if (!tipRecord) throw Error("not found"); if (!tipRecord) throw Error("not found");
const retries = await tx.operationRetries.get( const retries = await tx.operationRetries.get(
@ -818,21 +818,21 @@ function buildTransactionForDeposit(
} }
function buildTransactionForTip( function buildTransactionForTip(
tipRecord: RewardRecord, tipRecord: TipRecord,
ort?: OperationRetryRecord, ort?: OperationRetryRecord,
): Transaction { ): Transaction {
checkLogicInvariant(!!tipRecord.acceptedTimestamp); checkLogicInvariant(!!tipRecord.acceptedTimestamp);
return { return {
type: TransactionType.Reward, type: TransactionType.Tip,
txState: computeRewardTransactionStatus(tipRecord), txState: computeTipTransactionStatus(tipRecord),
txActions: computeTipTransactionActions(tipRecord), txActions: computeTipTransactionActions(tipRecord),
amountEffective: Amounts.stringify(tipRecord.rewardAmountEffective), amountEffective: Amounts.stringify(tipRecord.tipAmountEffective),
amountRaw: Amounts.stringify(tipRecord.rewardAmountRaw), amountRaw: Amounts.stringify(tipRecord.tipAmountRaw),
timestamp: tipRecord.acceptedTimestamp, timestamp: tipRecord.acceptedTimestamp,
transactionId: constructTransactionIdentifier({ transactionId: constructTransactionIdentifier({
tag: TransactionType.Reward, tag: TransactionType.Tip,
walletRewardId: tipRecord.walletRewardId, walletTipId: tipRecord.walletTipId,
}), }),
merchantBaseUrl: tipRecord.merchantBaseUrl, merchantBaseUrl: tipRecord.merchantBaseUrl,
...(ort?.lastError ? { error: ort.lastError } : {}), ...(ort?.lastError ? { error: ort.lastError } : {}),
@ -945,7 +945,7 @@ export async function getTransactions(
x.purchases, x.purchases,
x.contractTerms, x.contractTerms,
x.recoupGroups, x.recoupGroups,
x.rewards, x.tips,
x.tombstones, x.tombstones,
x.withdrawalGroups, x.withdrawalGroups,
x.refreshGroups, x.refreshGroups,
@ -1200,11 +1200,11 @@ export async function getTransactions(
); );
}); });
tx.rewards.iter().forEachAsync(async (tipRecord) => { tx.tips.iter().forEachAsync(async (tipRecord) => {
if ( if (
shouldSkipCurrency( shouldSkipCurrency(
transactionsRequest, transactionsRequest,
Amounts.parseOrThrow(tipRecord.rewardAmountRaw).currency, Amounts.parseOrThrow(tipRecord.tipAmountRaw).currency,
) )
) { ) {
return; return;
@ -1267,7 +1267,7 @@ export type ParsedTransactionIdentifier =
| { tag: TransactionType.PeerPushDebit; pursePub: string } | { tag: TransactionType.PeerPushDebit; pursePub: string }
| { tag: TransactionType.Refresh; refreshGroupId: string } | { tag: TransactionType.Refresh; refreshGroupId: string }
| { tag: TransactionType.Refund; refundGroupId: string } | { tag: TransactionType.Refund; refundGroupId: string }
| { tag: TransactionType.Reward; walletRewardId: string } | { tag: TransactionType.Tip; walletTipId: string }
| { tag: TransactionType.Withdrawal; withdrawalGroupId: string } | { tag: TransactionType.Withdrawal; withdrawalGroupId: string }
| { tag: TransactionType.InternalWithdrawal; withdrawalGroupId: string }; | { tag: TransactionType.InternalWithdrawal; withdrawalGroupId: string };
@ -1291,8 +1291,8 @@ export function constructTransactionIdentifier(
return `txn:${pTxId.tag}:${pTxId.refreshGroupId}` as TransactionIdStr; return `txn:${pTxId.tag}:${pTxId.refreshGroupId}` as TransactionIdStr;
case TransactionType.Refund: case TransactionType.Refund:
return `txn:${pTxId.tag}:${pTxId.refundGroupId}` as TransactionIdStr; return `txn:${pTxId.tag}:${pTxId.refundGroupId}` as TransactionIdStr;
case TransactionType.Reward: case TransactionType.Tip:
return `txn:${pTxId.tag}:${pTxId.walletRewardId}` as TransactionIdStr; return `txn:${pTxId.tag}:${pTxId.walletTipId}` as TransactionIdStr;
case TransactionType.Withdrawal: case TransactionType.Withdrawal:
return `txn:${pTxId.tag}:${pTxId.withdrawalGroupId}` as TransactionIdStr; return `txn:${pTxId.tag}:${pTxId.withdrawalGroupId}` as TransactionIdStr;
case TransactionType.InternalWithdrawal: case TransactionType.InternalWithdrawal:
@ -1346,10 +1346,10 @@ export function parseTransactionIdentifier(
tag: TransactionType.Refund, tag: TransactionType.Refund,
refundGroupId: rest[0], refundGroupId: rest[0],
}; };
case TransactionType.Reward: case TransactionType.Tip:
return { return {
tag: TransactionType.Reward, tag: TransactionType.Tip,
walletRewardId: rest[0], walletTipId: rest[0],
}; };
case TransactionType.Withdrawal: case TransactionType.Withdrawal:
return { return {
@ -1427,10 +1427,10 @@ export async function retryTransaction(
stopLongpolling(ws, taskId); stopLongpolling(ws, taskId);
break; break;
} }
case TransactionType.Reward: { case TransactionType.Tip: {
const taskId = constructTaskIdentifier({ const taskId = constructTaskIdentifier({
tag: PendingTaskType.RewardPickup, tag: PendingTaskType.TipPickup,
walletRewardId: parsedTx.walletRewardId, walletTipId: parsedTx.walletTipId,
}); });
await resetPendingTaskTimeout(ws, taskId); await resetPendingTaskTimeout(ws, taskId);
stopLongpolling(ws, taskId); stopLongpolling(ws, taskId);
@ -1522,8 +1522,8 @@ export async function suspendTransaction(
break; break;
case TransactionType.Refund: case TransactionType.Refund:
throw Error("refund transactions can't be suspended or resumed"); throw Error("refund transactions can't be suspended or resumed");
case TransactionType.Reward: case TransactionType.Tip:
await suspendRewardTransaction(ws, tx.walletRewardId); await suspendTipTransaction(ws, tx.walletTipId);
break; break;
default: default:
assertUnreachable(tx); assertUnreachable(tx);
@ -1551,8 +1551,8 @@ export async function failTransaction(
return; return;
case TransactionType.Refund: case TransactionType.Refund:
throw Error("can't do cancel-aborting on refund transaction"); throw Error("can't do cancel-aborting on refund transaction");
case TransactionType.Reward: case TransactionType.Tip:
await failTipTransaction(ws, tx.walletRewardId); await failTipTransaction(ws, tx.walletTipId);
return; return;
case TransactionType.Refresh: case TransactionType.Refresh:
await failRefreshGroup(ws, tx.refreshGroupId); await failRefreshGroup(ws, tx.refreshGroupId);
@ -1613,8 +1613,8 @@ export async function resumeTransaction(
break; break;
case TransactionType.Refund: case TransactionType.Refund:
throw Error("refund transactions can't be suspended or resumed"); throw Error("refund transactions can't be suspended or resumed");
case TransactionType.Reward: case TransactionType.Tip:
await resumeTipTransaction(ws, tx.walletRewardId); await resumeTipTransaction(ws, tx.walletTipId);
break; break;
} }
} }
@ -1763,16 +1763,16 @@ export async function deleteTransaction(
return; return;
} }
case TransactionType.Reward: { case TransactionType.Tip: {
const tipId = parsedTx.walletRewardId; const tipId = parsedTx.walletTipId;
await ws.db await ws.db
.mktx((x) => [x.rewards, x.tombstones]) .mktx((x) => [x.tips, x.tombstones])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const tipRecord = await tx.rewards.get(tipId); const tipRecord = await tx.tips.get(tipId);
if (tipRecord) { if (tipRecord) {
await tx.rewards.delete(tipId); await tx.tips.delete(tipId);
await tx.tombstones.put({ await tx.tombstones.put({
id: TombstoneTag.DeleteReward + ":" + tipId, id: TombstoneTag.DeleteTip + ":" + tipId,
}); });
} }
}); });
@ -1856,8 +1856,8 @@ export async function abortTransaction(
case TransactionType.Deposit: case TransactionType.Deposit:
await abortDepositGroup(ws, txId.depositGroupId); await abortDepositGroup(ws, txId.depositGroupId);
break; break;
case TransactionType.Reward: case TransactionType.Tip:
await abortTipTransaction(ws, txId.walletRewardId); await abortTipTransaction(ws, txId.walletTipId);
break; break;
case TransactionType.Refund: case TransactionType.Refund:
throw Error("can't abort refund transactions"); throw Error("can't abort refund transactions");

View File

@ -62,10 +62,6 @@ import {
ExchangeWithdrawResponse, ExchangeWithdrawResponse,
WithdrawUriInfoResponse, WithdrawUriInfoResponse,
ExchangeBatchWithdrawRequest, ExchangeBatchWithdrawRequest,
ExchangeAgeWithdrawRequest,
ExchangeAgeWithdrawRevealRequest,
ExchangeAgeWithdrawResponse,
ExchangeAgeWithdrawRevealResponse,
TransactionState, TransactionState,
TransactionMajorState, TransactionMajorState,
TransactionMinorState, TransactionMinorState,
@ -865,7 +861,6 @@ async function processPlanchetExchangeBatchRequest(
coinIdx < wgContext.numPlanchets; coinIdx < wgContext.numPlanchets;
coinIdx++ coinIdx++
) { ) {
// FIXME[oec]: Add lookup of planchet for age-withdraw here
let planchet = await tx.planchets.indexes.byGroupAndIndex.get([ let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
withdrawalGroup.withdrawalGroupId, withdrawalGroup.withdrawalGroupId,
coinIdx, coinIdx,
@ -928,8 +923,6 @@ async function processPlanchetExchangeBatchRequest(
// FIXME: handle individual error codes better! // FIXME: handle individual error codes better!
// FIXME[oec]: add age-withdraw-request here
if (args.useBatchRequest) { if (args.useBatchRequest) {
const reqUrl = new URL( const reqUrl = new URL(
`reserves/${withdrawalGroup.reservePub}/batch-withdraw`, `reserves/${withdrawalGroup.reservePub}/batch-withdraw`,

View File

@ -33,7 +33,7 @@ export enum PendingTaskType {
Purchase = "purchase", Purchase = "purchase",
Refresh = "refresh", Refresh = "refresh",
Recoup = "recoup", Recoup = "recoup",
RewardPickup = "reward-pickup", TipPickup = "tip-pickup",
Withdraw = "withdraw", Withdraw = "withdraw",
Deposit = "deposit", Deposit = "deposit",
Backup = "backup", Backup = "backup",
@ -144,7 +144,7 @@ export interface PendingRefreshTask {
* The wallet is picking up a tip that the user has accepted. * The wallet is picking up a tip that the user has accepted.
*/ */
export interface PendingTipPickupTask { export interface PendingTipPickupTask {
type: PendingTaskType.RewardPickup; type: PendingTaskType.TipPickup;
tipId: string; tipId: string;
merchantBaseUrl: string; merchantBaseUrl: string;
merchantTipId: string; merchantTipId: string;

View File

@ -34,11 +34,3 @@ export const WALLET_MERCHANT_PROTOCOL_VERSION = "2:0:1";
* Uses libtool's current:revision:age versioning. * Uses libtool's current:revision:age versioning.
*/ */
export const WALLET_BANK_INTEGRATION_PROTOCOL_VERSION = "0:0:0"; export const WALLET_BANK_INTEGRATION_PROTOCOL_VERSION = "0:0:0";
/**
* Semver of the wallet-core implementation.
* Will be replaced with the value from package.json in a
* post-compilation step (inside lib/).
*/
export const WALLET_CORE_IMPLEMENTATION_VERSION =
"__WALLET_CORE_IMPLEMENTATION_VERSION__";

View File

@ -29,7 +29,7 @@ import {
AcceptExchangeTosRequest, AcceptExchangeTosRequest,
AcceptManualWithdrawalRequest, AcceptManualWithdrawalRequest,
AcceptManualWithdrawalResult, AcceptManualWithdrawalResult,
AcceptRewardRequest, AcceptTipRequest,
AcceptTipResponse, AcceptTipResponse,
AcceptWithdrawalResponse, AcceptWithdrawalResponse,
AddExchangeRequest, AddExchangeRequest,
@ -85,8 +85,8 @@ import {
PreparePeerPushCreditRequest, PreparePeerPushCreditRequest,
PreparePeerPushCreditResponse, PreparePeerPushCreditResponse,
PrepareRefundRequest, PrepareRefundRequest,
PrepareRewardRequest as PrepareRewardRequest, PrepareTipRequest,
PrepareTipResult as PrepareRewardResult, PrepareTipResult,
RecoveryLoadRequest, RecoveryLoadRequest,
RetryTransactionRequest, RetryTransactionRequest,
SetCoinSuspendedRequest, SetCoinSuspendedRequest,
@ -114,8 +114,6 @@ import {
WithdrawUriInfoResponse, WithdrawUriInfoResponse,
SharePaymentRequest, SharePaymentRequest,
SharePaymentResult, SharePaymentResult,
GetCurrencyInfoRequest,
GetCurrencyInfoResponse,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { AuditorTrustRecord, WalletContractData } from "./db.js"; import { AuditorTrustRecord, WalletContractData } from "./db.js";
import { import {
@ -180,8 +178,8 @@ export enum WalletApiOperation {
DumpCoins = "dumpCoins", DumpCoins = "dumpCoins",
SetCoinSuspended = "setCoinSuspended", SetCoinSuspended = "setCoinSuspended",
ForceRefresh = "forceRefresh", ForceRefresh = "forceRefresh",
PrepareReward = "prepareReward", PrepareTip = "prepareTip",
AcceptReward = "acceptReward", AcceptTip = "acceptTip",
ExportBackup = "exportBackup", ExportBackup = "exportBackup",
AddBackupProvider = "addBackupProvider", AddBackupProvider = "addBackupProvider",
RemoveBackupProvider = "removeBackupProvider", RemoveBackupProvider = "removeBackupProvider",
@ -212,7 +210,6 @@ export enum WalletApiOperation {
ApplyDevExperiment = "applyDevExperiment", ApplyDevExperiment = "applyDevExperiment",
ValidateIban = "validateIban", ValidateIban = "validateIban",
TestingWaitTransactionsFinal = "testingWaitTransactionsFinal", TestingWaitTransactionsFinal = "testingWaitTransactionsFinal",
GetScopedCurrencyInfo = "getScopedCurrencyInfo",
} }
// group: Initialization // group: Initialization
@ -510,23 +507,23 @@ export type StartRefundQueryOp = {
response: EmptyObject; response: EmptyObject;
}; };
// group: Rewards // group: Tipping
/** /**
* Query and store information about a reward. * Query and store information about a tip.
*/ */
export type PrepareTipOp = { export type PrepareTipOp = {
op: WalletApiOperation.PrepareReward; op: WalletApiOperation.PrepareTip;
request: PrepareRewardRequest; request: PrepareTipRequest;
response: PrepareRewardResult; response: PrepareTipResult;
}; };
/** /**
* Accept a reward. * Accept a tip.
*/ */
export type AcceptTipOp = { export type AcceptTipOp = {
op: WalletApiOperation.AcceptReward; op: WalletApiOperation.AcceptTip;
request: AcceptRewardRequest; request: AcceptTipRequest;
response: AcceptTipResponse; response: AcceptTipResponse;
}; };
@ -604,12 +601,6 @@ export type ListCurrenciesOp = {
response: WalletCurrencyInfo; response: WalletCurrencyInfo;
}; };
export type GetScopedCurrencyInfoOp = {
op: WalletApiOperation.GetScopedCurrencyInfo;
request: GetCurrencyInfoRequest;
response: GetCurrencyInfoResponse;
};
// group: Deposits // group: Deposits
/** /**
@ -1032,8 +1023,8 @@ export type WalletOperations = {
[WalletApiOperation.ForceRefresh]: ForceRefreshOp; [WalletApiOperation.ForceRefresh]: ForceRefreshOp;
[WalletApiOperation.DeleteTransaction]: DeleteTransactionOp; [WalletApiOperation.DeleteTransaction]: DeleteTransactionOp;
[WalletApiOperation.RetryTransaction]: RetryTransactionOp; [WalletApiOperation.RetryTransaction]: RetryTransactionOp;
[WalletApiOperation.PrepareReward]: PrepareTipOp; [WalletApiOperation.PrepareTip]: PrepareTipOp;
[WalletApiOperation.AcceptReward]: AcceptTipOp; [WalletApiOperation.AcceptTip]: AcceptTipOp;
[WalletApiOperation.StartRefundQueryForUri]: StartRefundQueryForUriOp; [WalletApiOperation.StartRefundQueryForUri]: StartRefundQueryForUriOp;
[WalletApiOperation.StartRefundQuery]: StartRefundQueryOp; [WalletApiOperation.StartRefundQuery]: StartRefundQueryOp;
[WalletApiOperation.ListCurrencies]: ListCurrenciesOp; [WalletApiOperation.ListCurrencies]: ListCurrenciesOp;
@ -1081,7 +1072,6 @@ export type WalletOperations = {
[WalletApiOperation.ApplyDevExperiment]: ApplyDevExperimentOp; [WalletApiOperation.ApplyDevExperiment]: ApplyDevExperimentOp;
[WalletApiOperation.ValidateIban]: ValidateIbanOp; [WalletApiOperation.ValidateIban]: ValidateIbanOp;
[WalletApiOperation.TestingWaitTransactionsFinal]: TestingWaitTransactionsFinal; [WalletApiOperation.TestingWaitTransactionsFinal]: TestingWaitTransactionsFinal;
[WalletApiOperation.GetScopedCurrencyInfo]: GetScopedCurrencyInfoOp;
}; };
export type WalletCoreRequestType< export type WalletCoreRequestType<

View File

@ -93,7 +93,7 @@ import {
codecForPreparePeerPullPaymentRequest, codecForPreparePeerPullPaymentRequest,
codecForPreparePeerPushCreditRequest, codecForPreparePeerPushCreditRequest,
codecForPrepareRefundRequest, codecForPrepareRefundRequest,
codecForPrepareRewardRequest, codecForPrepareTipRequest,
codecForResumeTransaction, codecForResumeTransaction,
codecForRetryTransactionRequest, codecForRetryTransactionRequest,
codecForSetCoinSuspendedRequest, codecForSetCoinSuspendedRequest,
@ -118,8 +118,6 @@ import {
sampleWalletCoreTransactions, sampleWalletCoreTransactions,
validateIban, validateIban,
codecForSharePaymentRequest, codecForSharePaymentRequest,
GetCurrencyInfoResponse,
codecForGetCurrencyInfoRequest,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { import {
HttpRequestLibrary, HttpRequestLibrary,
@ -251,10 +249,10 @@ import {
} from "./operations/testing.js"; } from "./operations/testing.js";
import { import {
acceptTip, acceptTip,
computeRewardTransactionStatus, computeTipTransactionStatus,
prepareTip, prepareTip,
processTip, processTip,
} from "./operations/reward.js"; } from "./operations/tip.js";
import { import {
abortTransaction, abortTransaction,
deleteTransaction, deleteTransaction,
@ -302,7 +300,6 @@ import {
import { TimerAPI, TimerGroup } from "./util/timer.js"; import { TimerAPI, TimerGroup } from "./util/timer.js";
import { import {
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION, WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
WALLET_CORE_IMPLEMENTATION_VERSION,
WALLET_EXCHANGE_PROTOCOL_VERSION, WALLET_EXCHANGE_PROTOCOL_VERSION,
WALLET_MERCHANT_PROTOCOL_VERSION, WALLET_MERCHANT_PROTOCOL_VERSION,
} from "./versions.js"; } from "./versions.js";
@ -331,7 +328,7 @@ async function callOperationHandler(
return await processRefreshGroup(ws, pending.refreshGroupId); return await processRefreshGroup(ws, pending.refreshGroupId);
case PendingTaskType.Withdraw: case PendingTaskType.Withdraw:
return await processWithdrawalGroup(ws, pending.withdrawalGroupId); return await processWithdrawalGroup(ws, pending.withdrawalGroupId);
case PendingTaskType.RewardPickup: case PendingTaskType.TipPickup:
return await processTip(ws, pending.tipId); return await processTip(ws, pending.tipId);
case PendingTaskType.Purchase: case PendingTaskType.Purchase:
return await processPurchase(ws, pending.proposalId); return await processPurchase(ws, pending.proposalId);
@ -1019,6 +1016,12 @@ export async function getClientFromWalletState(
return client; return client;
} }
declare const __VERSION__: string;
declare const __GIT_HASH__: string;
const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : "dev";
const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
/** /**
* Implementation of the "wallet-core" API. * Implementation of the "wallet-core" API.
*/ */
@ -1352,9 +1355,9 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
refreshGroupId, refreshGroupId,
}; };
} }
case WalletApiOperation.PrepareReward: { case WalletApiOperation.PrepareTip: {
const req = codecForPrepareRewardRequest().decode(payload); const req = codecForPrepareTipRequest().decode(payload);
return await prepareTip(ws, req.talerRewardUri); return await prepareTip(ws, req.talerTipUri);
} }
case WalletApiOperation.StartRefundQueryForUri: { case WalletApiOperation.StartRefundQueryForUri: {
const req = codecForPrepareRefundRequest().decode(payload); const req = codecForPrepareRefundRequest().decode(payload);
@ -1372,9 +1375,9 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
await startQueryRefund(ws, txIdParsed.proposalId); await startQueryRefund(ws, txIdParsed.proposalId);
return {}; return {};
} }
case WalletApiOperation.AcceptReward: { case WalletApiOperation.AcceptTip: {
const req = codecForAcceptTipRequest().decode(payload); const req = codecForAcceptTipRequest().decode(payload);
return await acceptTip(ws, req.walletRewardId); return await acceptTip(ws, req.walletTipId);
} }
case WalletApiOperation.ExportBackupPlain: { case WalletApiOperation.ExportBackupPlain: {
return exportBackup(ws); return exportBackup(ws);
@ -1397,17 +1400,6 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
const resp = await getBackupRecovery(ws); const resp = await getBackupRecovery(ws);
return resp; return resp;
} }
case WalletApiOperation.GetScopedCurrencyInfo: {
// Ignore result, just validate in this mock implementation
codecForGetCurrencyInfoRequest().decode(payload);
const resp: GetCurrencyInfoResponse = {
decimalSeparator: ",",
isCurrencyNameLeading: false,
numFractionalDigits: 2,
numTinyDigits: 1,
};
return resp;
}
case WalletApiOperation.ImportBackupRecovery: { case WalletApiOperation.ImportBackupRecovery: {
const req = codecForAny().decode(payload); const req = codecForAny().decode(payload);
await loadBackupRecovery(ws, req); await loadBackupRecovery(ws, req);
@ -1598,15 +1590,15 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
} }
export function getVersion(ws: InternalWalletState): WalletCoreVersion { export function getVersion(ws: InternalWalletState): WalletCoreVersion {
const result: WalletCoreVersion = { const version: WalletCoreVersion = {
hash: undefined, hash: GIT_HASH,
version: WALLET_CORE_IMPLEMENTATION_VERSION, version: VERSION,
exchange: WALLET_EXCHANGE_PROTOCOL_VERSION, exchange: WALLET_EXCHANGE_PROTOCOL_VERSION,
merchant: WALLET_MERCHANT_PROTOCOL_VERSION, merchant: WALLET_MERCHANT_PROTOCOL_VERSION,
bank: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION, bank: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
devMode: false, devMode: false,
}; };
return result; return version;
} }
/** /**
@ -1897,12 +1889,12 @@ class InternalWalletStateImpl implements InternalWalletState {
} }
return computeRefreshTransactionState(rec); return computeRefreshTransactionState(rec);
} }
case TransactionType.Reward: { case TransactionType.Tip: {
const rec = await tx.rewards.get(parsedTxId.walletRewardId); const rec = await tx.tips.get(parsedTxId.walletTipId);
if (!rec) { if (!rec) {
return undefined; return undefined;
} }
return computeRewardTransactionStatus(rec); return computeTipTransactionStatus(rec);
} }
default: default:
assertUnreachable(parsedTxId); assertUnreachable(parsedTxId);

View File

@ -7,10 +7,8 @@
"target": "ES2017", "target": "ES2017",
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Node16", "moduleResolution": "Node16",
"resolveJsonModule": true,
"sourceMap": true, "sourceMap": true,
"lib": ["es6"], "lib": ["es6"],
"resolvePackageJsonImports": true,
"types": ["node"], "types": ["node"],
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
@ -33,8 +31,5 @@
"path": "../taler-util/" "path": "../taler-util/"
} }
], ],
"include": [ "include": ["src/**/*"]
"src/**/*",
"src/*.json"
]
} }

View File

@ -36,7 +36,6 @@
"@gnu-taler/idb-bridge": "workspace:*", "@gnu-taler/idb-bridge": "workspace:*",
"@gnu-taler/taler-util": "workspace:*", "@gnu-taler/taler-util": "workspace:*",
"@gnu-taler/taler-wallet-core": "workspace:*", "@gnu-taler/taler-wallet-core": "workspace:*",
"@gnu-taler/anastasis-core": "workspace:*",
"tslib": "^2.5.3" "tslib": "^2.5.3"
} }
} }

View File

@ -41,15 +41,6 @@ import {
Wallet, Wallet,
WalletApiOperation, WalletApiOperation,
} from "@gnu-taler/taler-wallet-core"; } from "@gnu-taler/taler-wallet-core";
import {
reduceAction,
getBackupStartState,
getRecoveryStartState,
discoverPolicies,
mergeDiscoveryAggregate,
ReducerState,
} from "@gnu-taler/anastasis-core";
import { userIdentifierDerive } from "@gnu-taler/anastasis-core/lib/crypto.js";
setGlobalLogLevelFromString("trace"); setGlobalLogLevelFromString("trace");
@ -178,53 +169,6 @@ class NativeWalletMessageHandler {
} }
} }
/**
* Handle an Anastasis request from the native app.
*/
async function handleAnastasisRequest(
operation: string,
id: string,
args: any,
): Promise<CoreApiResponse> {
const wrapSuccessResponse = (result: unknown): CoreApiResponseSuccess => {
return {
type: "response",
id,
operation,
result,
};
};
let req = args ?? {};
switch (operation) {
case "anastasisReduce":
// TODO: do some input validation here
let reduceRes = await reduceAction(req.state, req.action, req.args ?? {});
// For now, this will return "success" even if the wrapped Anastasis
// response is a ReducerStateError.
return wrapSuccessResponse(reduceRes);
case "anastasisStartBackup":
return wrapSuccessResponse(await getBackupStartState());
case "anastasisStartRecovery":
return wrapSuccessResponse(await getRecoveryStartState());
case "anastasisDiscoverPolicies":
let discoverRes = await discoverPolicies(req.state, req.cursor);
let aggregatedPolicies = mergeDiscoveryAggregate(
discoverRes.policies ?? [],
req.state.discoveryState?.aggregatedPolicies ?? [],
);
return wrapSuccessResponse({
...req.state,
discoveryState: {
state: "finished",
aggregatedPolicies,
cursor: discoverRes.cursor,
},
});
}
}
export function installNativeWalletListener(): void { export function installNativeWalletListener(): void {
setGlobalLogLevelFromString("trace"); setGlobalLogLevelFromString("trace");
const handler = new NativeWalletMessageHandler(); const handler = new NativeWalletMessageHandler();
@ -246,11 +190,7 @@ export function installNativeWalletListener(): void {
let respMsg: CoreApiResponse; let respMsg: CoreApiResponse;
try { try {
if (msg.operation.startsWith("anastasis")) { respMsg = await handler.handleMessage(operation, id, msg.args ?? {});
respMsg = await handleAnastasisRequest(operation, id, msg.args ?? {});
} else {
respMsg = await handler.handleMessage(operation, id, msg.args ?? {});
}
} catch (e) { } catch (e) {
respMsg = { respMsg = {
type: "error", type: "error",
@ -325,36 +265,7 @@ export async function testWithLocal() {
w.wallet.stop(); w.wallet.stop();
} }
export async function testArgon2id() {
const userIdVector = {
input_id_data: {
name: "Fleabag",
ssn: "AB123",
},
input_server_salt: "FZ48EFS7WS3R2ZR4V53A3GFFY4",
output_id:
"YS45R6CGJV84K1NN7T14ZBCPVTZ6H15XJSM1FV0R748MHPV82SM0126EBZKBAAGCR34Q9AFKPEW1HRT2Q9GQ5JRA3642AB571DKZS18",
};
if (
(await userIdentifierDerive(
userIdVector.input_id_data,
userIdVector.input_server_salt,
)) != userIdVector.output_id
) {
throw Error("argon2id is not working!");
}
console.log("argon2id is working!");
}
// @ts-ignore // @ts-ignore
globalThis.testWithGv = testWithGv; globalThis.testWithGv = testWithGv;
// @ts-ignore // @ts-ignore
globalThis.testWithLocal = testWithLocal; globalThis.testWithLocal = testWithLocal;
// @ts-ignore
globalThis.testArgon2id = testArgon2id;
// @ts-ignore
globalThis.testReduceAction = reduceAction;
// @ts-ignore
globalThis.testDiscoverPolicies = discoverPolicies;

View File

@ -146,7 +146,7 @@ const talerUriActionToPageName: {
} = { } = {
[TalerUriAction.Withdraw]: "ctaWithdraw", [TalerUriAction.Withdraw]: "ctaWithdraw",
[TalerUriAction.Pay]: "ctaPay", [TalerUriAction.Pay]: "ctaPay",
[TalerUriAction.Reward]: "ctaTips", [TalerUriAction.Tip]: "ctaTips",
[TalerUriAction.Refund]: "ctaRefund", [TalerUriAction.Refund]: "ctaRefund",
[TalerUriAction.PayPull]: "ctaInvoicePay", [TalerUriAction.PayPull]: "ctaInvoicePay",
[TalerUriAction.PayPush]: "ctaTransferPickup", [TalerUriAction.PayPush]: "ctaTransferPickup",

View File

@ -134,7 +134,7 @@ export function HistoryItem(props: { tx: Transaction }): VNode {
} }
/> />
); );
case TransactionType.Reward: case TransactionType.Tip:
return ( return (
<Layout <Layout
id={tx.transactionId} id={tx.transactionId}

Some files were not shown because too many files have changed in this diff Show More