Compare commits

...

50 Commits

Author SHA1 Message Date
70fca92e78
[age-withdraw] WiP: first types and adjustments 2023-08-15 13:48:37 +02:00
819949d7f2
Merge branch 'master' into age-withdraw 2023-08-15 13:47:29 +02:00
Krystian Baran
adb0e70f15
Translated using Weblate (Italian)
Currently translated at 0.7% (4 of 531 strings)

Translation: GNU Taler/Merchant Backoffice
Translate-URL: https://weblate.taler.net/projects/gnu-taler/merchant-backoffice/it/
2023-08-15 09:28:43 +02:00
ed33c4dfce
Translated using Weblate (German)
Currently translated at 0.1% (1 of 531 strings)

Translation: GNU Taler/Merchant Backoffice
Translate-URL: https://weblate.taler.net/projects/gnu-taler/merchant-backoffice/de/
2023-08-15 09:28:42 +02:00
Krystian Baran
73ad4154f6
Translated using Weblate (Italian)
Currently translated at 24.1% (22 of 91 strings)

Translation: GNU Taler/Taler Bank SPA
Translate-URL: https://weblate.taler.net/projects/gnu-taler/taler-bank-spa/it/
2023-08-15 09:28:42 +02:00
Krystian Baran
58e00418cf
Translated using Weblate (Italian)
Currently translated at 2.3% (9 of 379 strings)

Translation: GNU Taler/webextensions
Translate-URL: https://weblate.taler.net/projects/gnu-taler/webextensions/it/
2023-08-15 09:28:42 +02:00
Javier Sepulveda
598a8ec8df
Translated using Weblate (Spanish)
Currently translated at 52.9% (281 of 531 strings)

Translation: GNU Taler/Merchant Backoffice
Translate-URL: https://weblate.taler.net/projects/gnu-taler/merchant-backoffice/es/
2023-08-13 12:14:14 +02:00
Javier Sepulveda
2e68d21e36
Translated using Weblate (Spanish)
Currently translated at 100.0% (379 of 379 strings)

Translation: GNU Taler/webextensions
Translate-URL: https://weblate.taler.net/projects/gnu-taler/webextensions/es/
2023-08-13 12:14:14 +02:00
d59a23885e
anastasis: policy discovery CLI 2023-08-10 15:24:43 +02:00
e6c0689806
embedded: expose anastasis reducer for testing 2023-08-09 19:43:36 +02:00
Iván Ávalos
d33b70b069
wallet-core-embedded: added Anastasis policy discovery 2023-08-07 23:47:22 +02:00
Sebastian
a204105b5b
show advance mode in settings view 2023-08-07 08:14:46 -03:00
Sebastian
b1cea84ca8
show next expiration 2023-08-07 08:14:46 -03:00
Sebastian
7d53aa2755
show simple order creation unless advance mode is selected 2023-08-07 08:14:46 -03:00
Sebastian
ef148b1501
use stringify taler util 2023-08-07 08:14:45 -03:00
Sebastian
9f776d3fb0
fix date query parameter 2023-08-07 08:14:45 -03:00
Sebastian
7d1621767c
ui settings view 2023-08-07 08:14:44 -03:00
Sebastian
8eb0183c78
fix: support for empty strings 2023-08-07 08:14:43 -03:00
Sebastian
37d0f9438e
accesstoken in memory and better login when switching between accounts 2023-08-07 08:14:43 -03:00
44aeaba7b4
wallet-core: introduce tiny digits 2023-08-05 23:56:07 +02:00
0547df9538
-remove logging 2023-08-05 23:36:32 +02:00
6286699f26
-validation 2023-08-05 23:34:37 +02:00
308a4282cb
wallet-core: mock implementation of GetCurrencyInfo 2023-08-05 23:34:37 +02:00
L
951d1b66fa
Added translation using Weblate (Dutch) 2023-08-05 13:05:17 +02:00
Sebastian
acf110dd78
added new continue after wire transfer confirmed 2023-08-04 10:10:30 -03:00
Sebastian
6f4548c892
fix signature for aml decision 2023-08-04 09:09:00 -03:00
c12a366d49
adjustment of age-commitment and -proof generation
age-withdraw requires that the public keys in the age groups that
are too large for the commitment are derived from a published public
key.
2023-08-03 21:47:59 +02:00
cf49af2bb9
harness: allow overriding the test timeout via env variable 2023-08-03 21:44:43 +02:00
77ea209ddb
Merge branch 'master' into age-withdraw 2023-08-03 19:35:52 +02:00
2b9faf3d4e
derive the age commitment from seed 2023-08-03 19:28:10 +02:00
ee47aa4837
fix integration tests
Instead of using the deprecated runUntilDone, we now wait
for specific notifications.

The old way doesn't work, since p2p push transactions are not considered
done until the counterparty has accepted the payment.
2023-08-03 19:24:04 +02:00
57e86b759e
rename tip->reward in URI 2023-08-03 19:03:24 +02:00
0b5d0cdc71
fix version substitution 2023-08-03 18:58:44 +02:00
3d7df63c59
fix taleruri test failure 2023-08-03 18:40:25 +02:00
fdbd55d2bd
-towards tip->reward rename 2023-08-03 18:35:07 +02:00
0fe4840ca2
-fix Makefile 2023-08-03 13:20:38 +02:00
5422f679b3 -fix Makefile 2023-08-03 13:18:07 +02:00
475fdb502b
organize imports 2023-08-02 16:27:59 +02:00
Sebastian
21f678ead9
fix #7800 2023-08-02 10:59:19 -03:00
Sebastian
72b8a70da2
fix #7830 2023-08-02 10:41:14 -03:00
Sebastian
60929c34f3
use local port 2023-08-02 10:30:40 -03:00
Sebastian
22a3017d52
removing import assert since it breaks with linaria 2023-08-02 09:49:14 -03:00
Sebastian
efed6b32c5
fix #7717 2023-08-02 07:40:28 -03:00
e3460de331
-comments 2023-07-31 15:57:12 +02:00
Iván Ávalos
0ac5dba088
taler-wallet-embedded: add argon2id test 2023-07-26 12:09:17 -06:00
Iván Ávalos
0f6310bba4
wallet-core-embedded: add initial states to Anastasis handler 2023-07-26 12:09:17 -06:00
Iván Ávalos
5a91ec2da6
Move anastasis CLI into separate package 2023-07-26 12:09:17 -06:00
72a0da7bbe
anastasis-core: tsconfig 2023-07-26 12:09:17 -06:00
Iván Ávalos
e0f32dc899
taler-util: import hash-wasm only when building for Node 2023-07-26 12:09:17 -06:00
Iván Ávalos
ef51ba983f
WIP: initial work for Anastasis in qtart 2023-07-26 12:09:17 -06:00
119 changed files with 4195 additions and 969 deletions

View File

@ -20,6 +20,7 @@ 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 \
@ -121,15 +122,17 @@ 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 and taler-harness # Install taler-wallet-cli, anastasis-cli and taler-harness
install-tools: install-tools:
pnpm install --frozen-lockfile --filter @gnu-taler/taler-wallet-cli... --filter @gnu-taler/taler-harness... pnpm install --frozen-lockfile --filter @gnu-taler/taler-wallet-cli... --filter @gnu-taler/anastasis-cli... --filter @gnu-taler/taler-harness...
pnpm run --filter @gnu-taler/taler-wallet-cli... --filter @gnu-taler/taler-harness... compile pnpm run --filter @gnu-taler/taler-wallet-cli... --filter @gnu-taler/anastasis-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,6 +27,7 @@ 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,17 +61,20 @@ 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)
.put(hash(stringToBytes(decision.justification))) //TODO: new need the null terminator, also in the exchange
// .put(timestampRoundedToBuffer(decision.decision_time)) .put(hash(stringToBytes(decision.justification)))//check null
.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(hash(stringToBytes(decision.kyc_requirements))) .put(zero) //kyc_requirement
.put(bufferForUint32(decision.new_state)) .put(bufferForUint32(decision.new_state))
.build(); .build();

View File

@ -85,7 +85,6 @@ 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("ARS:1000"), threshold: Amounts.parseOrThrow("KUDOS:1000"),
}; };
const api = useAmlCasesAPI() const api = useAmlCasesAPI()

View File

@ -0,0 +1,41 @@
# 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

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

View File

@ -0,0 +1,20 @@
#!/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

@ -0,0 +1,70 @@
#!/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

@ -0,0 +1,44 @@
{
"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

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

View File

@ -0,0 +1,87 @@
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

@ -0,0 +1,33 @@
{
"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,7 +23,6 @@
"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

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

View File

@ -1,64 +0,0 @@
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,15 +71,13 @@ 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 argon2id({ const result = await hashArgon2id(
hashLength: 64, hashInput, // password
iterations: 3, decodeCrock(serverSalt), // salt
memorySize: 1024 /* kibibytes */, 3, // iterations
parallelism: 1, 1024, // memoryLimit (kibibytes)
password: hashInput, 64, // hashLength
salt: decodeCrock(serverSalt), );
outputType: "binary",
});
return encodeCrock(result); return encodeCrock(result);
} }
@ -153,7 +151,11 @@ 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));
@ -343,15 +345,13 @@ export async function secureAnswerHash(
truthUuid: TruthUuid, truthUuid: TruthUuid,
questionSalt: TruthSalt, questionSalt: TruthSalt,
): Promise<SecureAnswerHash> { ): Promise<SecureAnswerHash> {
const powResult = await argon2id({ const powResult = await hashArgon2id(
hashLength: 64, stringToBytes(answer), // password
iterations: 3, decodeCrock(questionSalt), // salt
memorySize: 1024 /* kibibytes */, 3, // iterations
parallelism: 1, 1024, // memorySize (kibibytes)
password: stringToBytes(answer), 64, // hashLength
salt: decodeCrock(questionSalt), );
outputType: "binary",
});
const kdfResult = kdfKw({ const kdfResult = kdfKw({
outputLength: 64, outputLength: 64,
salt: decodeCrock(truthUuid), salt: decodeCrock(truthUuid),

View File

@ -1,2 +0,0 @@
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": "ES2018", "target": "ES2020",
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Node16", "moduleResolution": "Node16",
"sourceMap": true, "sourceMap": true,
"lib": ["es6"], "lib": ["ES2020"],
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"strict": true, "strict": true,

View File

@ -136,6 +136,7 @@ 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

@ -48,6 +48,7 @@ export function ContinentSelectionScreen(): VNode {
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,
}); });
@ -56,6 +57,7 @@ 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,6 +228,8 @@ 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,9 +21,11 @@ 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 />
@ -33,7 +35,7 @@ export function Loading(): VNode {
export function Spinner(): VNode { export function Spinner(): VNode {
return ( return (
<div class="lds-ring"> <div class="lds-ring" style={{margin:"auto"}}>
<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: 2022-12-26 23:30+0000\n" "PO-Revision-Date: 2023-08-15 07:28+0000\n"
"Last-Translator: Stefan Kügel <skuegel@web.de>\n" "Last-Translator: Krystian Baran <kiszkot@murena.io>\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
#, fuzzy, c-format #, c-format
msgid "Withdraw" msgid "Withdraw"
msgstr "Conferma il ritiro" msgstr "Prelevare"
#: 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 "" msgstr "Data"
#: src/pages/home/Transactions.tsx:70 #: src/pages/home/Transactions.tsx:70
#, c-format #, c-format
msgid "Amount" msgid "Amount"
msgstr "Somma" msgstr "Importo"
#: 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 "Causale" msgstr "Soggetto"
#: 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 } from "@gnu-taler/taler-util"; import { Amounts, HttpStatusCode, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util";
import { import {
ErrorType, ErrorType,
HttpResponsePaginated, HttpResponsePaginated,
@ -27,6 +27,7 @@ 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;
@ -34,6 +35,60 @@ 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
*/ */
@ -66,7 +121,6 @@ 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
@ -76,8 +130,7 @@ export function AccountPage({ account, onLoadNotOk }: Props): VNode {
<div> <div>
<h1 class="nav welcome-text"> <h1 class="nav welcome-text">
<i18n.Translate> <i18n.Translate>
Welcome, {accountNumber ? `${account} (${accountNumber})` : account} Welcome, {account} (<a href={stringifyPaytoUri(payto)}>{payto.iban}</a>)! <CopyButton getContent={() => stringifyPaytoUri(payto)} />
!
</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,
onAbort, onContinue,
}: { }: {
operationId: string; operationId: string;
onLoadNotOk: () => void; onLoadNotOk: () => void;
onAbort: () => void; onContinue: () => 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,12 +99,6 @@ 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}"`,
@ -115,10 +109,7 @@ export function WithdrawalOperationPage({
return ( return (
<WithdrawalQRCode <WithdrawalQRCode
withdrawUri={parsedUri} withdrawUri={parsedUri}
onConfirmed={() => { onContinue={onContinue}
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}
onAbort={() => { onContinue={() => {
route("/account"); route("/account");
}} }}
onLoadNotOk={() => { onLoadNotOk={() => {

View File

@ -33,7 +33,6 @@ 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;
} }
@ -42,7 +41,6 @@ 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 {
@ -119,7 +117,6 @@ 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,6 +24,7 @@ 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";
@ -32,8 +33,7 @@ const logger = new Logger("WithdrawalQRCode");
interface Props { interface Props {
withdrawUri: WithdrawUriResult; withdrawUri: WithdrawUriResult;
onAborted: () => void; onContinue: () => void;
onConfirmed: () => void;
onLoadNotOk: () => void; onLoadNotOk: () => void;
} }
/** /**
@ -43,10 +43,14 @@ interface Props {
*/ */
export function WithdrawalQRCode({ export function WithdrawalQRCode({
withdrawUri, withdrawUri,
onConfirmed, onContinue,
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) {
@ -64,13 +68,64 @@ export function WithdrawalQRCode({
} }
const { data } = result; const { data } = result;
logger.trace("withdrawal status", data); if (data.aborted) {
if (data.aborted || data.confirmation_done) { return <section id="main" class="content">
// signal that this withdrawal is aborted <h1 class="nav">{i18n.str`Operation aborted`}</h1>
// will redirect to account info <section>
notifyInfo(i18n.str`Operation completed`); <p>
onAborted(); <i18n.Translate>
return <Loading />; The wire transfer to the GNU Taler Exchange bank's account was aborted, your balance
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) {
@ -79,24 +134,20 @@ export function WithdrawalQRCode({
withdrawUri={withdrawUri} withdrawUri={withdrawUri}
onAborted={() => { onAborted={() => {
notifyInfo(i18n.str`Operation canceled`); notifyInfo(i18n.str`Operation canceled`);
onAborted(); clearCurrentWithdrawal()
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`);
onAborted(); clearCurrentWithdrawal()
onContinue()
}} }}
/> />
); );

View File

@ -315,3 +315,39 @@ h1.nav {
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 } from "preact/hooks"; import { useMemo, useState } 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,6 +42,7 @@ 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 (
@ -70,10 +71,19 @@ 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!" /> <NotYetReadyAppMenu title="Welcome!" onShowSettings={() => setShowSettings(true)} />
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} /> <LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
</Fragment> </Fragment>
); );
@ -87,7 +97,7 @@ function ApplicationStatusRoutes(): VNode {
) { ) {
return ( return (
<Fragment> <Fragment>
<NotYetReadyAppMenu title="Login" /> <NotYetReadyAppMenu title="Login" onShowSettings={() => setShowSettings(true)} />
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} /> <LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
</Fragment> </Fragment>
); );
@ -98,7 +108,7 @@ function ApplicationStatusRoutes(): VNode {
) { ) {
return ( return (
<Fragment> <Fragment>
<NotYetReadyAppMenu title="Error" /> <NotYetReadyAppMenu title="Error" onShowSettings={() => setShowSettings(true)} />
<NotificationCard <NotificationCard
notification={{ notification={{
message: i18n.str`Server not found`, message: i18n.str`Server not found`,
@ -112,7 +122,7 @@ function ApplicationStatusRoutes(): VNode {
} }
if (result.type === ErrorType.SERVER) { if (result.type === ErrorType.SERVER) {
<Fragment> <Fragment>
<NotYetReadyAppMenu title="Error" /> <NotYetReadyAppMenu title="Error" onShowSettings={() => setShowSettings(true)} />
<NotificationCard <NotificationCard
notification={{ notification={{
message: i18n.str`Server response with an error code`, message: i18n.str`Server response with an error code`,
@ -125,7 +135,7 @@ function ApplicationStatusRoutes(): VNode {
} }
if (result.type === ErrorType.UNREADABLE) { if (result.type === ErrorType.UNREADABLE) {
<Fragment> <Fragment>
<NotYetReadyAppMenu title="Error" /> <NotYetReadyAppMenu title="Error" onShowSettings={() => setShowSettings(true)} />
<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}`,
@ -138,7 +148,7 @@ function ApplicationStatusRoutes(): VNode {
} }
return ( return (
<Fragment> <Fragment>
<NotYetReadyAppMenu title="Error" /> <NotYetReadyAppMenu title="Error" onShowSettings={() => setShowSettings(true)} />
<NotificationCard <NotificationCard
notification={{ notification={{
message: i18n.str`Unexpected Error`, message: i18n.str`Unexpected Error`,

View File

@ -33,6 +33,7 @@ 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();
@ -48,8 +49,15 @@ export function ApplicationReadyRoutes(): VNode {
clearAllTokens(); clearAllTokens();
route("/"); route("/");
}; };
const [showSettings, setShowSettings] = useState(false)
if (result.loading) return <NotYetReadyAppMenu title="Loading..." />; if (showSettings) {
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;
@ -61,7 +69,7 @@ export function ApplicationReadyRoutes(): VNode {
) { ) {
return ( return (
<Fragment> <Fragment>
<NotYetReadyAppMenu title="Login" onLogout={clearTokenAndGoToRoot} /> <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Login" onLogout={clearTokenAndGoToRoot} />
<NotificationCard <NotificationCard
notification={{ notification={{
message: i18n.str`Access denied`, message: i18n.str`Access denied`,
@ -81,7 +89,7 @@ export function ApplicationReadyRoutes(): VNode {
// does not match our pattern // does not match our pattern
return ( return (
<Fragment> <Fragment>
<NotYetReadyAppMenu title="Error" onLogout={clearTokenAndGoToRoot} /> <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} 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,6 +68,7 @@ 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 = '/',
@ -100,6 +101,8 @@ 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
@ -240,6 +243,9 @@ 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}
@ -558,6 +564,7 @@ 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 { h, VNode } from "preact"; import { ComponentChildren, 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:${encodeURIComponent(r)}`; return `secret-token:${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 } = useInstanceContext(); const { admin, token: instanceToken, id } = useInstanceContext();
const testLogin = useCredentialsChecker(); const testLogin = useCredentialsChecker();
const currentToken = getTokenValuePart( const currentToken = getTokenValuePart(
(!admin ? baseToken : instanceToken) ?? "", (!admin ? baseToken : instanceToken) ?? "",
@ -63,6 +63,78 @@ 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 ">
@ -137,8 +209,7 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode {
borderTop: 0, borderTop: 0,
}} }}
> >
<button <AsyncButton
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);
@ -150,10 +221,24 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode {
}} }}
> >
<i18n.Translate>Confirm</i18n.Translate> <i18n.Translate>Confirm</i18n.Translate>
</button> </AsyncButton>
</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 } = useField<T>(name); const { error, value, onChange, required } = 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,7 +58,7 @@ 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"> <div class="field has-icons-right">
<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"}
@ -78,8 +78,14 @@ 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

@ -0,0 +1,91 @@
/*
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,7 +20,6 @@
*/ */
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 {
@ -65,7 +64,6 @@ 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,6 +31,7 @@ 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;
@ -40,6 +41,7 @@ interface Props {
export function Sidebar({ export function Sidebar({
mobile, mobile,
instance, instance,
onShowSettings,
onLogout, onLogout,
admin, admin,
mimic, mimic,
@ -78,20 +80,7 @@ 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>
<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> <li>
<a href={"/orders"} class="has-icon"> <a href={"/orders"} class="has-icon">
<span class="icon"> <span class="icon">
@ -132,6 +121,31 @@ 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">
@ -150,16 +164,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> </ul>
</Fragment> </Fragment>
) : undefined} ) : undefined}
@ -167,6 +171,18 @@ 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,6 +75,7 @@ interface MenuProps {
instance: string; instance: string;
admin?: boolean; admin?: boolean;
onLogout?: () => void; onLogout?: () => void;
onShowSettings: () => void;
setInstanceName: (s: string) => void; setInstanceName: (s: string) => void;
} }
@ -93,6 +94,7 @@ function WithTitle({
export function Menu({ export function Menu({
onLogout, onLogout,
onShowSettings,
title, title,
instance, instance,
path, path,
@ -121,6 +123,7 @@ export function Menu({
{onLogout && ( {onLogout && (
<Sidebar <Sidebar
onShowSettings={onShowSettings}
onLogout={onLogout} onLogout={onLogout}
admin={admin} admin={admin}
mimic={mimic} mimic={mimic}
@ -130,7 +133,12 @@ export function Menu({
)} )}
{mimic && ( {mimic && (
<nav class="level"> <nav class="level" style={{
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>.{" "}
@ -154,6 +162,7 @@ export function Menu({
interface NotYetReadyAppMenuProps { interface NotYetReadyAppMenuProps {
title: string; title: string;
onLogout?: () => void; onLogout?: () => void;
onShowSettings: () => void;
} }
interface NotifProps { interface NotifProps {
@ -194,6 +203,7 @@ 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);
@ -212,7 +222,7 @@ export function NotYetReadyAppMenu({
title={title} title={title}
/> />
{onLogout && ( {onLogout && (
<Sidebar onLogout={onLogout} instance="" mobile={mobileOpen} /> <Sidebar onShowSettings={onShowSettings} onLogout={onLogout} instance="" mobile={mobileOpen} />
)} )}
</div> </div>
); );

View File

@ -1331,12 +1331,13 @@ 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: string; event_type: MerchantWebhookType;
// 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_ms = const date_s =
delta && delta < 0 && searchDate delta && delta < 0 && searchDate
? searchDate.getTime() + 1 ? (searchDate.getTime() / 1000) + 1
: searchDate?.getTime(); : searchDate !== undefined ? (searchDate.getTime() / 1000) : undefined;
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_ms !== undefined) params.date_ms = date_ms; if (date_s !== undefined) params.date_s = date_s;
return requestHandler<T>(baseUrl, endpoint, { params, token }); return requestHandler<T>(baseUrl, endpoint, { params, token });
}, },
[baseUrl, token], [baseUrl, token],

View File

@ -21,6 +21,7 @@
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 =
@ -52,14 +53,17 @@ export function useBackendURL(
export function useBackendDefaultToken( export function useBackendDefaultToken(
initialValue?: string, initialValue?: string,
): [string | undefined, StateUpdater<string | undefined>] { ): [string | undefined, ((d: string | undefined) => void)] {
return useLocalStorage("backend-token", initialValue); // uncomment for testing
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, StateUpdater<string | undefined>] { ): [string | undefined, ((d: string | undefined) => void)] {
const [token, setToken] = useLocalStorage(`backend-token-${id}`); const { update: setToken, value: token, reset } = useMemoryStorage(`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
@ -67,15 +71,16 @@ export function useBackendInstanceToken(
return [defaultToken, defaultSetToken]; return [defaultToken, defaultSetToken];
} }
function updateToken( function updateToken(
value: value: (string | undefined)
| (string | undefined)
| ((s: string | undefined) => string | undefined),
): void { ): void {
setToken((p) => { console.log("seeting token", value)
const toStore = value instanceof Function ? value(p) : value; if (value === undefined) {
return toStore; reset()
}); } else {
setToken(value)
} }
}
console.log("token", token)
return [token, updateToken]; return [token, updateToken];
} }

View File

@ -0,0 +1,59 @@
/*
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,20 +12,21 @@
# 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: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: 2023-08-15 07:28+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: Stefan Kügel <skuegel@web.de>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: German <https://weblate.taler.net/projects/gnu-taler/"
"Language: \n" "merchant-backoffice/de/>\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
@ -1252,7 +1253,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 "" msgstr "Rückerstattet"
#: 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-04-24 06:43+0000\n" "PO-Revision-Date: 2023-08-13 10:14+0000\n"
"Last-Translator: Stefan Kügel <skuegel@web.de>\n" "Last-Translator: Javier Sepulveda <javier.sepulveda@uv.es>\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 "Reembolzado" msgstr "Reembolsado"
#: src/paths/instance/orders/list/ListPage.tsx:152 #: src/paths/instance/orders/list/ListPage.tsx:152
#, c-format #, c-format

View File

@ -12,20 +12,21 @@
# 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: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: 2023-08-15 07:28+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: Krystian Baran <kiszkot@murena.io>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: Italian <https://weblate.taler.net/projects/gnu-taler/"
"Language: \n" "merchant-backoffice/it/>\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
@ -439,7 +440,7 @@ msgstr ""
#: src/components/form/InputTaxes.tsx:119 #: src/components/form/InputTaxes.tsx:119
#, c-format #, c-format
msgid "Amount" msgid "Amount"
msgstr "" msgstr "Importo"
#: src/components/form/InputTaxes.tsx:120 #: src/components/form/InputTaxes.tsx:120
#, c-format #, c-format
@ -887,7 +888,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 "" msgstr "Data"
#: src/paths/instance/orders/list/Table.tsx:200 #: src/paths/instance/orders/list/Table.tsx:200
#, c-format #, c-format
@ -1252,7 +1253,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 "" msgstr "Rimborsato"
#: src/paths/instance/orders/list/ListPage.tsx:152 #: src/paths/instance/orders/list/ListPage.tsx:152
#, c-format #, c-format
@ -1633,7 +1634,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 "" msgstr "Soggetto"
#: src/paths/instance/reserves/details/DetailPage.tsx:130 #: src/paths/instance/reserves/details/DetailPage.tsx:130
#, c-format #, c-format

View File

@ -43,6 +43,7 @@ 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;
@ -138,7 +139,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 || {});
@ -374,6 +375,7 @@ export function CreatePage({
inventory={instanceInventory} inventory={instanceInventory}
/> />
{settings.advanceOrderMode &&
<NonInventoryProductFrom <NonInventoryProductFrom
productToEdit={editingProduct} productToEdit={editingProduct}
onAddProduct={(p) => { onAddProduct={(p) => {
@ -381,6 +383,7 @@ export function CreatePage({
return addNewProduct(p); return addNewProduct(p);
}} }}
/> />
}
{allProducts.length > 0 && ( {allProducts.length > 0 && (
<ProductList <ProductList
@ -445,6 +448,7 @@ 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`}
@ -470,7 +474,9 @@ export function CreatePage({
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`}
@ -528,7 +534,9 @@ export function CreatePage({
} }
/> />
</InputGroup> </InputGroup>
}
{settings.advanceOrderMode &&
<InputGroup <InputGroup
name="extra" name="extra"
label={i18n.str`Additional information`} label={i18n.str`Additional information`}
@ -541,6 +549,7 @@ export function CreatePage({
tooltip={i18n.str`You must enter a value in JavaScript Object Notation (JSON).`} tooltip={i18n.str`You must enter a value in JavaScript Object Notation (JSON).`}
/> />
</InputGroup> </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 } from "date-fns"; import { format, formatDistance } 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,6 +223,7 @@ 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">
@ -419,6 +420,11 @@ 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
@ -504,22 +510,13 @@ function PaidPage({
whiteSpace: "nowrap", whiteSpace: "nowrap",
overflow: "hidden", overflow: "hidden",
textOverflow: "ellipsis", textOverflow: "ellipsis",
// maxWidth: '100%',
}} }}
> >
<p> <p>
<a <i18n.Translate>Next event in </i18n.Translate> {formatDistance(
href={order.contract_terms.fulfillment_url} nextEvent!.when,
rel="nofollow" new Date(),
target="new" // "yyyy/MM/dd HH:mm:ss",
>
{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>

View File

@ -67,7 +67,7 @@ export function Timeline({ events: e }: Props) {
); );
case "start": case "start":
return ( return (
<div class="timeline-marker is-icon is-success"> <div class="timeline-marker is-icon">
<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">
<p class="heading">{format(e.when, "yyyy/MM/dd HH:mm:ss")}</p> {e.description !== "now" && <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>

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" onClick={() => onSelectDate(undefined)}> <a class="button is-fullwidth" 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" class="button is-fullwidth"
onClick={() => { onClick={() => {
setPickDate(true); setPickDate(true);
}} }}

View File

@ -139,7 +139,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
> >
<InputWithAddon<Entity> <InputWithAddon<Entity>
name="template_id" name="template_id"
addonBefore={`${backend.url}/instances/templates/`} help={`${backend.url}/instances/templates/${state.template_id ?? ""}`}
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,6 +34,7 @@ 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;
@ -64,28 +65,29 @@ 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 params = new URLSearchParams(); const templateParams: Record<string, string> = {}
if (!fixedAmount) { if (!fixedAmount) {
if (state.amount) { if (state.amount) {
params.append("amount", state.amount); templateParams.amount = state.amount
} else { } else {
params.append("amount", config.currency); templateParams.amount = config.currency
} }
} }
if (!fixedSummary) { if (!fixedSummary) {
params.append("summary", state.summary ?? ""); templateParams.summary = state.summary ?? ""
} }
const paramsStr = fixedAmount && fixedSummary ? "" : "?" + params.toString(); const merchantBaseUrl = new URL(backendUrl).href;
const merchantURL = new URL(backendUrl);
const talerProto = const payTemplateUri = stringifyPayTemplateUri({
merchantURL.protocol === "http:" ? "taler+http:" : "taler:"; merchantBaseUrl,
templateId,
const payTemplateUri = `${talerProto}//pay-template/${merchantURL.hostname}/${templateId}${paramsStr}`; templateParams
})
const issuer = encodeURIComponent( const issuer = encodeURIComponent(
`${new URL(backendUrl).hostname}/${instanceId}`, `${new URL(backendUrl).host}/${instanceId}`,
); );
const oauthUri = !template.pos_algorithm const oauthUri = !template.pos_algorithm
? undefined ? undefined

View File

@ -33,6 +33,7 @@ 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;
@ -50,7 +51,9 @@ 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` : undefined, event_type: !state.event_type ? i18n.str`required`
: 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)
@ -84,16 +87,30 @@ 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`}
/> />
<Input<Entity> <InputSelector
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`}
/> />
<Input<Entity> <InputSelector
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,7 +25,6 @@ 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

@ -0,0 +1,77 @@
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,6 +52,8 @@ $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;
} }
@ -92,6 +94,7 @@ 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;
@ -125,6 +128,7 @@ 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%);
} }

View File

@ -0,0 +1,51 @@
$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,
TipCreateConfirmation, RewardCreateConfirmation,
TipCreateRequest, RewardCreateRequest,
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: TipCreateRequest, req: RewardCreateRequest,
): Promise<TipCreateConfirmation> { ): Promise<RewardCreateConfirmation> {
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.PrepareTip, { const ptr = await walletTipping.client.call(WalletApiOperation.PrepareReward, {
talerTipUri: tip.taler_tip_uri, talerRewardUri: tip.taler_reward_uri,
}); });
await walletTipping.client.call(WalletApiOperation.AcceptTip, { await walletTipping.client.call(WalletApiOperation.AcceptReward, {
walletTipId: ptr.walletTipId, walletRewardId: ptr.walletRewardId,
}); });
await walletTipping.runUntilDone(); await walletTipping.runUntilDone();

View File

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

View File

@ -23,6 +23,7 @@ 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";
@ -46,12 +47,14 @@ 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);
}, },
@ -89,6 +92,15 @@ 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,
{ {
@ -98,8 +110,6 @@ 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.PrepareTip, { const ptr = await wallet.client.call(WalletApiOperation.PrepareReward, {
talerTipUri: tip.taler_tip_uri, talerRewardUri: tip.taler_reward_uri,
}); });
console.log(ptr); console.log(ptr);
t.assertAmountEquals(ptr.tipAmountRaw, "TESTKUDOS:5"); t.assertAmountEquals(ptr.rewardAmountRaw, "TESTKUDOS:5");
t.assertAmountEquals(ptr.tipAmountEffective, "TESTKUDOS:4.85"); t.assertAmountEquals(ptr.rewardAmountEffective, "TESTKUDOS:4.85");
await wallet.client.call(WalletApiOperation.AcceptTip, { await wallet.client.call(WalletApiOperation.AcceptReward, {
walletTipId: ptr.walletTipId, walletRewardId: ptr.walletRewardId,
}); });
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, "tip"); t.assertDeepEqual(txns.transactions[0].type, "reward");
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,8 +338,17 @@ export async function runTests(spec: TestRunSpec) {
currentChild.stdout?.pipe(harnessLogStream); currentChild.stdout?.pipe(harnessLogStream);
currentChild.stderr?.pipe(harnessLogStream); currentChild.stderr?.pipe(harnessLogStream);
const defaultTimeout = 60000; // Default timeout when the test doesn't override it.
const testTimeoutMs = testCase.timeoutMs ?? defaultTimeout; let defaultTimeout = 60000;
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,6 +49,10 @@
"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": {
@ -69,7 +73,8 @@
"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": [

View File

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

View File

@ -0,0 +1,19 @@
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

@ -0,0 +1,18 @@
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",
Tip = "tip", Reward = "reward",
} }
/** /**
@ -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.Tip; type: BackupCoinSourceType.Reward;
/** /**
* Wallet's identifier for the tip that this coin * Wallet's identifier for the tip that this coin

View File

@ -183,7 +183,16 @@ export class HttpLibImpl implements HttpRequestLibrary {
resolve(resp); resolve(resp);
}); });
res.on("error", (e) => { res.on("error", (e) => {
reject(e); const err = TalerError.fromDetail(
TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
{
requestUrl: url,
requestMethod: method,
httpStatusCode: 0,
},
`Error in HTTP response handler: ${e.message}`,
);
reject(err);
}); });
}; };
@ -197,7 +206,16 @@ export class HttpLibImpl implements HttpRequestLibrary {
} }
req.on("error", (e: Error) => { req.on("error", (e: Error) => {
reject(e); const err = TalerError.fromDetail(
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 TipCreateConfirmation { export interface RewardCreateConfirmation {
// Unique tip identifier for the tip that was created. // Unique tip identifier for the tip that was created.
tip_id: string; reward_id: string;
// taler://tip URI for the tip // taler://tip URI for the tip
taler_tip_uri: string; taler_reward_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
tip_status_url: string; reward_status_url: string;
// when does the tip expire // when does the reward expire
tip_expiration: AbsoluteTime; reward_expiration: AbsoluteTime;
} }
export interface TipCreateRequest { export interface RewardCreateRequest {
// Amount that the customer should be tipped // Amount that the customer should be tipped
amount: AmountString; amount: AmountString;

View File

@ -24,6 +24,7 @@
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,
@ -69,6 +70,13 @@ 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;
@ -253,6 +261,31 @@ 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);
@ -1221,31 +1254,13 @@ export namespace AgeRestriction {
age: number, age: number,
): Promise<AgeCommitmentProof> { ): Promise<AgeCommitmentProof> {
invariant((ageMask & 1) === 1); invariant((ageMask & 1) === 1);
const numPubs = countAgeGroups(ageMask) - 1; const seed = getRandomBytes(32);
const numPrivs = getAgeGroupIndex(ageMask, age);
const pubs: Edx25519PublicKey[] = []; return restrictionCommitSeeded(ageMask, age, seed);
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 { const PublishedAgeRestrictionBaseKey: Edx25519PublicKey = decodeCrock(
commitment: { "CH0VKFDZ2GWRWHQBBGEK9MWV5YDQVJ0RXEE0KYT3NMB69F0R96TG");
mask: ageMask,
publicKeys: pubs.map((x) => encodeCrock(x)),
},
proof: {
privateKeys: privs.map((x) => encodeCrock(x)),
},
};
}
export async function restrictionCommitSeeded( export async function restrictionCommitSeeded(
ageMask: number, ageMask: number,
@ -1259,19 +1274,32 @@ export namespace AgeRestriction {
const pubs: Edx25519PublicKey[] = []; const pubs: Edx25519PublicKey[] = [];
const privs: Edx25519PrivateKey[] = []; const privs: Edx25519PrivateKey[] = [];
for (let i = 0; i < numPubs; i++) { for (let i = 0; i < numPrivs; i++) {
const privSeed = await kdfKw({ const privSeed = await kdfKw({
outputLength: 32, outputLength: 32,
ikm: seed, ikm: seed,
info: stringToBytes("age-restriction-commit"), info: stringToBytes("age-commitment"),
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);
if (i < numPrivs) {
privs.push(priv); 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 {
@ -1571,7 +1599,9 @@ export function amountToBuffer(amount: AmountLike): Uint8Array {
return u8buf; return u8buf;
} }
export function timestampRoundedToBuffer(ts: TalerProtocolTimestamp): Uint8Array { export function timestampRoundedToBuffer(
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,6 +1788,89 @@ 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,
parseTipUri, parseRewardUri,
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 = parseTipUri(url1); const r1 = parseRewardUri(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 = parseTipUri(url1); const r1 = parseRewardUri(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 = parseTipUri(url1); const r1 = parseRewardUri(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/JFX1NE38C65A5XT8VSNQXX7R7BBG4GNZ63F5T7Y6859V4J8KBKF0?a=KUDOS%3A19", "taler://withdraw-exchange/exchange.demo.taler.net/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0?a=KUDOS%3A19",
); );
}); });

View File

@ -26,7 +26,7 @@ export type TalerUri =
| PayPushUriResult | PayPushUriResult
| BackupRestoreUri | BackupRestoreUri
| RefundUriResult | RefundUriResult
| TipUriResult | RewardUriResult
| WithdrawUriResult | WithdrawUriResult
| ExchangeUri | ExchangeUri
| WithdrawExchangeUri | WithdrawExchangeUri
@ -60,8 +60,8 @@ export interface RefundUriResult {
orderId: string; orderId: string;
} }
export interface TipUriResult { export interface RewardUriResult {
type: TalerUriAction.Tip; type: TalerUriAction.Reward;
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",
Tip = "tip", Reward = "reward",
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.Tip]: parseTipUri, [TalerUriAction.Reward]: parseRewardUri,
[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.Tip: { case TalerUriAction.Reward: {
return stringifyTipUri(uri); return stringifyRewardUri(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]://tip URI. * Parse a taler[+http]://reward URI.
* Return undefined if not passed a valid URI. * Return undefined if not passed a valid URI.
*/ */
export function parseTipUri(s: string): TipUriResult | undefined { export function parseRewardUri(s: string): RewardUriResult | undefined {
const pi = parseProtoInfo(s, "tip"); const pi = parseProtoInfo(s, "reward");
if (!pi) { if (!pi) {
return undefined; return undefined;
} }
@ -416,7 +416,7 @@ export function parseTipUri(s: string): TipUriResult | undefined {
); );
return { return {
type: TalerUriAction.Tip, type: TalerUriAction.Reward,
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 stringifyTipUri({ export function stringifyRewardUri({
merchantBaseUrl, merchantBaseUrl,
merchantTipId, merchantTipId,
}: Omit<TipUriResult, "type">): string { }: Omit<RewardUriResult, "type">): string {
const { proto, path } = getUrlInfo(merchantBaseUrl); const { proto, path } = getUrlInfo(merchantBaseUrl);
return `${proto}://tip/${path}${merchantTipId}`; return `${proto}://reward/${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) { if (value !== undefined) {
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
| TransactionTip | TransactionReward
| 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",
Tip = "tip", Reward = "reward",
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 TransactionTip extends TransactionCommon { export interface TransactionReward extends TransactionCommon {
type: TransactionType.Tip; type: TransactionType.Reward;
// 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,6 +379,54 @@ 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;
} }
@ -393,10 +441,19 @@ export enum ScopeType {
Auditor = "auditor", Auditor = "auditor",
} }
export type ScopeInfo = export type ScopeInfoGlobal = { type: ScopeType.Global; currency: string };
| { type: ScopeType.Global; currency: string } export type ScopeInfoExchange = {
| { type: ScopeType.Exchange; currency: string; url: string } type: ScopeType.Exchange;
| { type: ScopeType.Auditor; currency: string; url: string }; currency: 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[];
@ -605,7 +662,7 @@ export interface PrepareTipResult {
* *
* @deprecated use transactionId instead * @deprecated use transactionId instead
*/ */
walletTipId: string; walletRewardId: string;
/** /**
* Tip transaction ID. * Tip transaction ID.
@ -620,13 +677,13 @@ export interface PrepareTipResult {
/** /**
* Amount that the merchant gave. * Amount that the merchant gave.
*/ */
tipAmountRaw: AmountString; rewardAmountRaw: 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.
*/ */
tipAmountEffective: AmountString; rewardAmountEffective: AmountString;
/** /**
* Base URL of the merchant backend giving then tip. * Base URL of the merchant backend giving then tip.
@ -654,14 +711,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("tipAmountRaw", codecForAmountString()) .property("rewardAmountRaw", codecForAmountString())
.property("tipAmountEffective", codecForAmountString()) .property("rewardAmountEffective", codecForAmountString())
.property("exchangeBaseUrl", codecForString()) .property("exchangeBaseUrl", codecForString())
.property("merchantBaseUrl", codecForString()) .property("merchantBaseUrl", codecForString())
.property("expirationTimestamp", codecForTimestamp) .property("expirationTimestamp", codecForTimestamp)
.property("walletTipId", codecForString()) .property("walletRewardId", codecForString())
.property("transactionId", codecForString()) .property("transactionId", codecForString())
.build("PrepareTipResult"); .build("PrepareRewardResult");
export interface BenchmarkResult { export interface BenchmarkResult {
time: { [s: string]: number }; time: { [s: string]: number };
@ -994,6 +1051,9 @@ export interface ExchangeDetailedResponse {
} }
export interface WalletCoreVersion { export interface WalletCoreVersion {
/**
* @deprecated
*/
hash: string | undefined; hash: string | undefined;
version: string; version: string;
exchange: string; exchange: string;
@ -1930,23 +1990,23 @@ export const codecForStartRefundQueryRequest =
.property("transactionId", codecForTransactionIdStr()) .property("transactionId", codecForTransactionIdStr())
.build("StartRefundQueryRequest"); .build("StartRefundQueryRequest");
export interface PrepareTipRequest { export interface PrepareRewardRequest {
talerTipUri: string; talerRewardUri: string;
} }
export const codecForPrepareTipRequest = (): Codec<PrepareTipRequest> => export const codecForPrepareRewardRequest = (): Codec<PrepareRewardRequest> =>
buildCodecForObject<PrepareTipRequest>() buildCodecForObject<PrepareRewardRequest>()
.property("talerTipUri", codecForString()) .property("talerRewardUri", codecForString())
.build("PrepareTipRequest"); .build("PrepareRewardRequest");
export interface AcceptTipRequest { export interface AcceptRewardRequest {
walletTipId: string; walletRewardId: string;
} }
export const codecForAcceptTipRequest = (): Codec<AcceptTipRequest> => export const codecForAcceptTipRequest = (): Codec<AcceptRewardRequest> =>
buildCodecForObject<AcceptTipRequest>() buildCodecForObject<AcceptRewardRequest>()
.property("walletTipId", codecForString()) .property("walletRewardId", codecForString())
.build("AcceptTipRequest"); .build("AcceptRewardRequest");
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.Tip: { case TalerUriAction.Reward: {
const res = await wallet.client.call(WalletApiOperation.PrepareTip, { const res = await wallet.client.call(WalletApiOperation.PrepareReward, {
talerTipUri: uri, talerRewardUri: uri,
}); });
console.log("tip status", res); console.log("tip status", res);
await wallet.client.call(WalletApiOperation.AcceptTip, { await wallet.client.call(WalletApiOperation.AcceptReward, {
walletTipId: res.walletTipId, walletRewardId: res.walletRewardId,
}); });
break; break;
} }

View File

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

View File

@ -28,7 +28,6 @@ import {
AgeCommitmentProof, AgeCommitmentProof,
AgeRestriction, AgeRestriction,
AmountJson, AmountJson,
AmountLike,
Amounts, Amounts,
AmountString, AmountString,
amountToBuffer, amountToBuffer,
@ -64,7 +63,6 @@ import {
hashCoinPub, hashCoinPub,
hashDenomPub, hashDenomPub,
hashTruncate32, hashTruncate32,
j2s,
kdf, kdf,
kdfKw, kdfKw,
keyExchangeEcdhEddsa, keyExchangeEcdhEddsa,
@ -81,16 +79,13 @@ 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 {
@ -108,7 +103,6 @@ import {
EncryptContractForDepositResponse, EncryptContractForDepositResponse,
EncryptContractRequest, EncryptContractRequest,
EncryptContractResponse, EncryptContractResponse,
EncryptedContract,
SignDeletePurseRequest, SignDeletePurseRequest,
SignDeletePurseResponse, SignDeletePurseResponse,
SignPurseMergeRequest, SignPurseMergeRequest,
@ -726,9 +720,10 @@ 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.restrictionCommit( maybeAcp = await AgeRestriction.restrictionCommitSeeded(
denomPub.age_mask, denomPub.age_mask,
age, age,
stringToBytes(req.secretSeed)
); );
maybeAgeCommitmentHash = AgeRestriction.hashCommitment( maybeAgeCommitmentHash = AgeRestriction.hashCommitment(
maybeAcp.commitment, maybeAcp.commitment,

View File

@ -657,6 +657,7 @@ export interface PlanchetRecord {
*/ */
coinIdx: number; coinIdx: number;
planchetStatus: PlanchetStatus; planchetStatus: PlanchetStatus;
lastError: TalerErrorDetail | undefined; lastError: TalerErrorDetail | undefined;
@ -671,13 +672,19 @@ 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",
Tip = "tip", Reward = "reward",
} }
export interface WithdrawCoinSource { export interface WithdrawCoinSource {
@ -705,13 +712,16 @@ export interface RefreshCoinSource {
oldCoinPub: string; oldCoinPub: string;
} }
export interface TipCoinSource { export interface RewardCoinSource {
type: CoinSourceType.Tip; type: CoinSourceType.Reward;
walletTipId: string; walletRewardId: string;
coinIndex: number; coinIndex: number;
} }
export type CoinSource = WithdrawCoinSource | RefreshCoinSource | TipCoinSource; export type CoinSource =
| WithdrawCoinSource
| RefreshCoinSource
| RewardCoinSource;
/** /**
* CoinRecord as stored in the "coins" data store * CoinRecord as stored in the "coins" data store
@ -815,9 +825,9 @@ export interface CoinAllocation {
} }
/** /**
* Status of a tip we got from a merchant. * Status of a reward we got from a merchant.
*/ */
export interface TipRecord { export interface RewardRecord {
/** /**
* 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.
@ -827,17 +837,17 @@ export interface TipRecord {
/** /**
* The tipped amount. * The tipped amount.
*/ */
tipAmountRaw: AmountString; rewardAmountRaw: AmountString;
/** /**
* Effect on the balance (including fees etc). * Effect on the balance (including fees etc).
*/ */
tipAmountEffective: AmountString; rewardAmountEffective: 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.
*/ */
tipExpiration: TalerProtocolTimestamp; rewardExpiration: TalerProtocolTimestamp;
/** /**
* The exchange that will sign our coins, chosen by the merchant. * The exchange that will sign our coins, chosen by the merchant.
@ -863,7 +873,7 @@ export interface TipRecord {
/** /**
* Tip ID chosen by the wallet. * Tip ID chosen by the wallet.
*/ */
walletTipId: string; walletRewardId: string;
/** /**
* Secret seed used to derive planchets for this tip. * Secret seed used to derive planchets for this tip.
@ -871,9 +881,9 @@ export interface TipRecord {
secretSeed: string; secretSeed: string;
/** /**
* The merchant's identifier for this tip. * The merchant's identifier for this reward.
*/ */
merchantTipId: string; merchantRewardId: string;
createdTimestamp: TalerPreciseTimestamp; createdTimestamp: TalerPreciseTimestamp;
@ -888,10 +898,10 @@ export interface TipRecord {
*/ */
pickedUpTimestamp: TalerPreciseTimestamp | undefined; pickedUpTimestamp: TalerPreciseTimestamp | undefined;
status: TipRecordStatus; status: RewardRecordStatus;
} }
export enum TipRecordStatus { export enum RewardRecordStatus {
PendingPickup = 10, PendingPickup = 10,
SuspendidPickup = 20, SuspendidPickup = 20,
@ -1420,7 +1430,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 withdrawal or from a tip.) * (Either for a normal {single-|batch-|age-} withdrawal or from a reward.)
* *
* 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.
@ -2480,12 +2490,12 @@ export const WalletStoresV1 = {
]), ]),
}, },
), ),
tips: describeStore( rewards: describeStore(
"tips", "rewards",
describeContents<TipRecord>({ keyPath: "walletTipId" }), describeContents<RewardRecord>({ keyPath: "walletRewardId" }),
{ {
byMerchantTipIdAndBaseUrl: describeIndex("byMerchantTipIdAndBaseUrl", [ byMerchantTipIdAndBaseUrl: describeIndex("byMerchantRewardIdAndBaseUrl", [
"merchantTipId", "merchantRewardId",
"merchantBaseUrl", "merchantBaseUrl",
]), ]),
byStatus: describeIndex("byStatus", "status", { byStatus: describeIndex("byStatus", "status", {
@ -2510,6 +2520,11 @@ 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",
@ -2935,22 +2950,6 @@ 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.tips, x.rewards,
x.recoupGroups, x.recoupGroups,
x.withdrawalGroups, x.withdrawalGroups,
]) ])
@ -184,12 +184,12 @@ export async function exportBackup(
}); });
}); });
await tx.tips.iter().forEach((tip) => { await tx.rewards.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.merchantTipId, merchant_tip_id: tip.merchantRewardId,
wallet_tip_id: tip.walletTipId, wallet_tip_id: tip.walletRewardId,
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.tipExpiration, timestamp_expiration: tip.rewardExpiration,
tip_amount_raw: Amounts.stringify(tip.tipAmountRaw), tip_amount_raw: Amounts.stringify(tip.rewardAmountRaw),
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.Tip: case CoinSourceType.Reward:
bcs = { bcs = {
type: BackupCoinSourceType.Tip, type: BackupCoinSourceType.Reward,
coin_index: coin.coinSource.coinIndex, coin_index: coin.coinSource.coinIndex,
wallet_tip_id: coin.coinSource.walletTipId, wallet_tip_id: coin.coinSource.walletRewardId,
}; };
break; break;
case CoinSourceType.Withdraw: case CoinSourceType.Withdraw:

View File

@ -56,7 +56,7 @@ import {
WithdrawalGroupStatus, WithdrawalGroupStatus,
WithdrawalRecordType, WithdrawalRecordType,
RefreshOperationStatus, RefreshOperationStatus,
TipRecordStatus, RewardRecordStatus,
} 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.Tip: case BackupCoinSourceType.Reward:
coinSource = { coinSource = {
type: CoinSourceType.Tip, type: CoinSourceType.Reward,
coinIndex: backupCoin.coin_source.coin_index, coinIndex: backupCoin.coin_source.coin_index,
walletTipId: backupCoin.coin_source.wallet_tip_id, walletRewardId: 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.tips, x.rewards,
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.DeleteTip, tag: TombstoneTag.DeleteReward,
walletTipId: backupTip.wallet_tip_id, walletTipId: backupTip.wallet_tip_id,
}); });
if (tombstoneSet.has(ts)) { if (tombstoneSet.has(ts)) {
continue; continue;
} }
const existingTip = await tx.tips.get(backupTip.wallet_tip_id); const existingTip = await tx.rewards.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.tips.put({ await tx.rewards.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,
merchantTipId: backupTip.merchant_tip_id, merchantRewardId: backupTip.merchant_tip_id,
pickedUpTimestamp: backupTip.timestamp_finished, pickedUpTimestamp: backupTip.timestamp_finished,
secretSeed: backupTip.secret_seed, secretSeed: backupTip.secret_seed,
tipAmountEffective: Amounts.stringify(denomsSel.totalCoinValue), rewardAmountEffective: Amounts.stringify(denomsSel.totalCoinValue),
tipAmountRaw: Amounts.stringify(tipAmountRaw), rewardAmountRaw: Amounts.stringify(tipAmountRaw),
tipExpiration: backupTip.timestamp_expiration, rewardExpiration: backupTip.timestamp_expiration,
walletTipId: backupTip.wallet_tip_id, walletRewardId: backupTip.wallet_tip_id,
denomSelUid: backupTip.selected_denoms_uid, denomSelUid: backupTip.selected_denoms_uid,
status: TipRecordStatus.Done, // FIXME! status: RewardRecordStatus.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.DeleteTip) { } else if (type === TombstoneTag.DeleteReward) {
await tx.tips.delete(rest[0]); await tx.rewards.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,
TipRecord, RewardRecord,
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.TipPickup: case PendingTaskType.RewardPickup:
return constructTransactionIdentifier({ return constructTransactionIdentifier({
tag: TransactionType.Tip, tag: TransactionType.Reward,
walletTipId: parsedTaskId.walletTipId, walletRewardId: parsedTaskId.walletRewardId,
}); });
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",
DeleteTip = "delete-tip", DeleteReward = "delete-reward",
DeleteRefreshGroup = "delete-refresh-group", DeleteRefreshGroup = "delete-refresh-group",
DeleteDepositGroup = "delete-deposit-group", DeleteDepositGroup = "delete-deposit-group",
DeleteRefund = "delete-refund", DeleteRefund = "delete-refund",
@ -601,7 +601,9 @@ export function runLongpollAsync(
}; };
res = await reqFn(cts.token); res = await reqFn(cts.token);
} catch (e) { } catch (e) {
await storePendingTaskError(ws, retryTag, getErrorDetailFromException(e)); const errDetail = 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];
@ -622,7 +624,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.DeleteTip; walletTipId: string } | { tag: TombstoneTag.DeleteReward; walletTipId: string }
| { tag: TombstoneTag.DeletePayment; proposalId: string }; | { tag: TombstoneTag.DeletePayment; proposalId: string };
export function constructTombstone(p: ParsedTombstone): TombstoneIdStr { export function constructTombstone(p: ParsedTombstone): TombstoneIdStr {
@ -637,7 +639,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.DeleteTip: case TombstoneTag.DeleteReward:
return `tmb:${p.tag}:${p.walletTipId}` as TombstoneIdStr; return `tmb:${p.tag}:${p.walletTipId}` as TombstoneIdStr;
default: default:
assertUnreachable(p); assertUnreachable(p);
@ -810,7 +812,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.TipPickup; walletTipId: string } | { tag: PendingTaskType.RewardPickup; walletRewardId: string }
| { tag: PendingTaskType.Refresh; refreshGroupId: string }; | { tag: PendingTaskType.Refresh; refreshGroupId: string };
export function parseTaskIdentifier(x: string): ParsedTaskIdentifier { export function parseTaskIdentifier(x: string): ParsedTaskIdentifier {
@ -844,8 +846,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.TipPickup: case PendingTaskType.RewardPickup:
return { tag: type, walletTipId: rest[0] }; return { tag: type, walletRewardId: rest[0] };
case PendingTaskType.Withdraw: case PendingTaskType.Withdraw:
return { tag: type, withdrawalGroupId: rest[0] }; return { tag: type, withdrawalGroupId: rest[0] };
default: default:
@ -877,8 +879,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.TipPickup: case PendingTaskType.RewardPickup:
return `${p.tag}:${p.walletTipId}` as TaskId; return `${p.tag}:${p.walletRewardId}` as TaskId;
case PendingTaskType.Withdraw: case PendingTaskType.Withdraw:
return `${p.tag}:${p.withdrawalGroupId}` as TaskId; return `${p.tag}:${p.withdrawalGroupId}` as TaskId;
default: default:
@ -899,8 +901,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: TipRecord): TaskId { export function forTipPickup(tipRecord: RewardRecord): TaskId {
return `${PendingTaskType.TipPickup}:${tipRecord.walletTipId}` as TaskId; return `${PendingTaskType.RewardPickup}:${tipRecord.walletRewardId}` 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,16 +436,25 @@ async function handlePeerPullCreditCreatePurse(
logger.info(`reserve merge response: ${j2s(resp)}`); logger.info(`reserve merge response: ${j2s(resp)}`);
await ws.db const transactionId = constructTransactionIdentifier({
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,
TipRecordStatus, RewardRecordStatus,
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<{
tips: typeof WalletStoresV1.tips; rewards: typeof WalletStoresV1.rewards;
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(
TipRecordStatus.PendingPickup, RewardRecordStatus.PendingPickup,
TipRecordStatus.PendingPickup, RewardRecordStatus.PendingPickup,
); );
await tx.tips.indexes.byStatus.iter(range).forEachAsync(async (tip) => { await tx.rewards.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.TipPickup, type: PendingTaskType.RewardPickup,
...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.walletTipId, tipId: tip.walletRewardId,
merchantTipId: tip.merchantTipId, merchantTipId: tip.merchantRewardId,
}); });
} }
}); });
@ -494,7 +494,7 @@ export async function getPendingOperations(
x.refreshGroups, x.refreshGroups,
x.coins, x.coins,
x.withdrawalGroups, x.withdrawalGroups,
x.tips, x.rewards,
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 recoupTipCoin( async function recoupRewardCoin(
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.Tip: case CoinSourceType.Reward:
return recoupTipCoin(ws, recoupGroupId, coinIdx, coin); return recoupRewardCoin(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,
parseTipUri, parseRewardUri,
PrepareTipResult, PrepareTipResult,
TalerErrorCode, TalerErrorCode,
TalerPreciseTimestamp, TalerPreciseTimestamp,
@ -48,8 +48,8 @@ import {
CoinRecord, CoinRecord,
CoinSourceType, CoinSourceType,
DenominationRecord, DenominationRecord,
TipRecord, RewardRecord,
TipRecordStatus, RewardRecordStatus,
} 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 tip. * database record of a reward.
*/ */
export function computeTipTransactionStatus( export function computeRewardTransactionStatus(
tipRecord: TipRecord, tipRecord: RewardRecord,
): TransactionState { ): TransactionState {
switch (tipRecord.status) { switch (tipRecord.status) {
case TipRecordStatus.Done: case RewardRecordStatus.Done:
return { return {
major: TransactionMajorState.Done, major: TransactionMajorState.Done,
}; };
case TipRecordStatus.Aborted: case RewardRecordStatus.Aborted:
return { return {
major: TransactionMajorState.Aborted, major: TransactionMajorState.Aborted,
}; };
case TipRecordStatus.PendingPickup: case RewardRecordStatus.PendingPickup:
return { return {
major: TransactionMajorState.Pending, major: TransactionMajorState.Pending,
minor: TransactionMinorState.Pickup, minor: TransactionMinorState.Pickup,
}; };
case TipRecordStatus.DialogAccept: case RewardRecordStatus.DialogAccept:
return { return {
major: TransactionMajorState.Dialog, major: TransactionMajorState.Dialog,
minor: TransactionMinorState.Proposed, minor: TransactionMinorState.Proposed,
}; };
case TipRecordStatus.SuspendidPickup: case RewardRecordStatus.SuspendidPickup:
return { return {
major: TransactionMajorState.Pending, major: TransactionMajorState.Pending,
minor: TransactionMinorState.Pickup, minor: TransactionMinorState.Pickup,
@ -119,18 +119,18 @@ export function computeTipTransactionStatus(
} }
export function computeTipTransactionActions( export function computeTipTransactionActions(
tipRecord: TipRecord, tipRecord: RewardRecord,
): TransactionAction[] { ): TransactionAction[] {
switch (tipRecord.status) { switch (tipRecord.status) {
case TipRecordStatus.Done: case RewardRecordStatus.Done:
return [TransactionAction.Delete]; return [TransactionAction.Delete];
case TipRecordStatus.Aborted: case RewardRecordStatus.Aborted:
return [TransactionAction.Delete]; return [TransactionAction.Delete];
case TipRecordStatus.PendingPickup: case RewardRecordStatus.PendingPickup:
return [TransactionAction.Suspend, TransactionAction.Fail]; return [TransactionAction.Suspend, TransactionAction.Fail];
case TipRecordStatus.SuspendidPickup: case RewardRecordStatus.SuspendidPickup:
return [TransactionAction.Resume, TransactionAction.Fail]; return [TransactionAction.Resume, TransactionAction.Fail];
case TipRecordStatus.DialogAccept: case RewardRecordStatus.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 = parseTipUri(talerTipUri); const res = parseRewardUri(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.tips]) .mktx((x) => [x.rewards])
.runReadOnly(async (tx) => { .runReadOnly(async (tx) => {
return tx.tips.indexes.byMerchantTipIdAndBaseUrl.get([ return tx.rewards.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: TipRecord = { const newTipRecord: RewardRecord = {
walletTipId: walletTipId, walletRewardId: walletTipId,
acceptedTimestamp: undefined, acceptedTimestamp: undefined,
status: TipRecordStatus.DialogAccept, status: RewardRecordStatus.DialogAccept,
tipAmountRaw: Amounts.stringify(amount), rewardAmountRaw: Amounts.stringify(amount),
tipExpiration: tipPickupStatus.expiration, rewardExpiration: 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(),
merchantTipId: res.merchantTipId, merchantRewardId: res.merchantTipId,
tipAmountEffective: Amounts.stringify(selectedDenoms.totalCoinValue), rewardAmountEffective: 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.tips]) .mktx((x) => [x.rewards])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
await tx.tips.put(newTipRecord); await tx.rewards.put(newTipRecord);
}); });
tipRecord = newTipRecord; tipRecord = newTipRecord;
} }
const transactionId = constructTransactionIdentifier({ const transactionId = constructTransactionIdentifier({
tag: TransactionType.Tip, tag: TransactionType.Reward,
walletTipId: tipRecord.walletTipId, walletRewardId: tipRecord.walletRewardId,
}); });
const tipStatus: PrepareTipResult = { const tipStatus: PrepareTipResult = {
accepted: !!tipRecord && !!tipRecord.acceptedTimestamp, accepted: !!tipRecord && !!tipRecord.acceptedTimestamp,
tipAmountRaw: Amounts.stringify(tipRecord.tipAmountRaw), rewardAmountRaw: Amounts.stringify(tipRecord.rewardAmountRaw),
exchangeBaseUrl: tipRecord.exchangeBaseUrl, exchangeBaseUrl: tipRecord.exchangeBaseUrl,
merchantBaseUrl: tipRecord.merchantBaseUrl, merchantBaseUrl: tipRecord.merchantBaseUrl,
expirationTimestamp: tipRecord.tipExpiration, expirationTimestamp: tipRecord.rewardExpiration,
tipAmountEffective: Amounts.stringify(tipRecord.tipAmountEffective), rewardAmountEffective: Amounts.stringify(tipRecord.rewardAmountEffective),
walletTipId: tipRecord.walletTipId, walletRewardId: tipRecord.walletRewardId,
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.tips]) .mktx((x) => [x.rewards])
.runReadOnly(async (tx) => { .runReadOnly(async (tx) => {
return tx.tips.get(walletTipId); return tx.rewards.get(walletTipId);
}); });
if (!tipRecord) { if (!tipRecord) {
return TaskRunResult.finished(); return TaskRunResult.finished();
} }
switch (tipRecord.status) { switch (tipRecord.status) {
case TipRecordStatus.Aborted: case RewardRecordStatus.Aborted:
case TipRecordStatus.DialogAccept: case RewardRecordStatus.DialogAccept:
case TipRecordStatus.Done: case RewardRecordStatus.Done:
case TipRecordStatus.SuspendidPickup: case RewardRecordStatus.SuspendidPickup:
return TaskRunResult.finished(); return TaskRunResult.finished();
} }
const transactionId = constructTransactionIdentifier({ const transactionId = constructTransactionIdentifier({
tag: TransactionType.Tip, tag: TransactionType.Reward,
walletTipId, walletRewardId: 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.merchantTipId}/pickup`, `tips/${tipRecord.merchantRewardId}/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.Tip, type: CoinSourceType.Reward,
coinIndex: i, coinIndex: i,
walletTipId: walletTipId, walletRewardId: 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.tips]) .mktx((x) => [x.coins, x.coinAvailability, x.denominations, x.rewards])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const tr = await tx.tips.get(walletTipId); const tr = await tx.rewards.get(walletTipId);
if (!tr) { if (!tr) {
return; return;
} }
if (tr.status !== TipRecordStatus.PendingPickup) { if (tr.status !== RewardRecordStatus.PendingPickup) {
return; return;
} }
const oldTxState = computeTipTransactionStatus(tr); const oldTxState = computeRewardTransactionStatus(tr);
tr.pickedUpTimestamp = TalerPreciseTimestamp.now(); tr.pickedUpTimestamp = TalerPreciseTimestamp.now();
tr.status = TipRecordStatus.Done; tr.status = RewardRecordStatus.Done;
await tx.tips.put(tr); await tx.rewards.put(tr);
const newTxState = computeTipTransactionStatus(tr); const newTxState = computeRewardTransactionStatus(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.Tip, tag: TransactionType.Reward,
walletTipId, walletRewardId: walletTipId,
}); });
const dbRes = await ws.db const dbRes = await ws.db
.mktx((x) => [x.tips]) .mktx((x) => [x.rewards])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const tipRecord = await tx.tips.get(walletTipId); const tipRecord = await tx.rewards.get(walletTipId);
if (!tipRecord) { if (!tipRecord) {
logger.error("tip not found"); logger.error("tip not found");
return; return;
} }
if (tipRecord.status != TipRecordStatus.DialogAccept) { if (tipRecord.status != RewardRecordStatus.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 = computeTipTransactionStatus(tipRecord); const oldTxState = computeRewardTransactionStatus(tipRecord);
tipRecord.acceptedTimestamp = TalerPreciseTimestamp.now(); tipRecord.acceptedTimestamp = TalerPreciseTimestamp.now();
tipRecord.status = TipRecordStatus.PendingPickup; tipRecord.status = RewardRecordStatus.PendingPickup;
await tx.tips.put(tipRecord); await tx.rewards.put(tipRecord);
const newTxState = computeTipTransactionStatus(tipRecord); const newTxState = computeRewardTransactionStatus(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.Tip, tag: TransactionType.Reward,
walletTipId: walletTipId, walletRewardId: walletTipId,
}), }),
next_url: tipRecord.next_url, next_url: tipRecord.next_url,
}; };
} }
export async function suspendTipTransaction( export async function suspendRewardTransaction(
ws: InternalWalletState, ws: InternalWalletState,
walletTipId: string, walletRewardId: string,
): Promise<void> { ): Promise<void> {
const taskId = constructTaskIdentifier({ const taskId = constructTaskIdentifier({
tag: PendingTaskType.TipPickup, tag: PendingTaskType.RewardPickup,
walletTipId, walletRewardId: walletRewardId,
}); });
const transactionId = constructTransactionIdentifier({ const transactionId = constructTransactionIdentifier({
tag: TransactionType.Tip, tag: TransactionType.Reward,
walletTipId, walletRewardId: walletRewardId,
}); });
stopLongpolling(ws, taskId); stopLongpolling(ws, taskId);
const transitionInfo = await ws.db const transitionInfo = await ws.db
.mktx((x) => [x.tips]) .mktx((x) => [x.rewards])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const tipRec = await tx.tips.get(walletTipId); const tipRec = await tx.rewards.get(walletRewardId);
if (!tipRec) { if (!tipRec) {
logger.warn(`transaction tip ${walletTipId} not found`); logger.warn(`transaction tip ${walletRewardId} not found`);
return; return;
} }
let newStatus: TipRecordStatus | undefined = undefined; let newStatus: RewardRecordStatus | undefined = undefined;
switch (tipRec.status) { switch (tipRec.status) {
case TipRecordStatus.Done: case RewardRecordStatus.Done:
case TipRecordStatus.SuspendidPickup: case RewardRecordStatus.SuspendidPickup:
case TipRecordStatus.Aborted: case RewardRecordStatus.Aborted:
case TipRecordStatus.DialogAccept: case RewardRecordStatus.DialogAccept:
break; break;
case TipRecordStatus.PendingPickup: case RewardRecordStatus.PendingPickup:
newStatus = TipRecordStatus.SuspendidPickup; newStatus = RewardRecordStatus.SuspendidPickup;
break; break;
default: default:
assertUnreachable(tipRec.status); assertUnreachable(tipRec.status);
} }
if (newStatus != null) { if (newStatus != null) {
const oldTxState = computeTipTransactionStatus(tipRec); const oldTxState = computeRewardTransactionStatus(tipRec);
tipRec.status = newStatus; tipRec.status = newStatus;
const newTxState = computeTipTransactionStatus(tipRec); const newTxState = computeRewardTransactionStatus(tipRec);
await tx.tips.put(tipRec); await tx.rewards.put(tipRec);
return { return {
oldTxState, oldTxState,
newTxState, newTxState,
@ -525,43 +525,43 @@ export async function suspendTipTransaction(
export async function resumeTipTransaction( export async function resumeTipTransaction(
ws: InternalWalletState, ws: InternalWalletState,
walletTipId: string, walletRewardId: string,
): Promise<void> { ): Promise<void> {
const taskId = constructTaskIdentifier({ const taskId = constructTaskIdentifier({
tag: PendingTaskType.TipPickup, tag: PendingTaskType.RewardPickup,
walletTipId, walletRewardId: walletRewardId,
}); });
const transactionId = constructTransactionIdentifier({ const transactionId = constructTransactionIdentifier({
tag: TransactionType.Tip, tag: TransactionType.Reward,
walletTipId, walletRewardId: walletRewardId,
}); });
stopLongpolling(ws, taskId); stopLongpolling(ws, taskId);
const transitionInfo = await ws.db const transitionInfo = await ws.db
.mktx((x) => [x.tips]) .mktx((x) => [x.rewards])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const tipRec = await tx.tips.get(walletTipId); const rewardRec = await tx.rewards.get(walletRewardId);
if (!tipRec) { if (!rewardRec) {
logger.warn(`transaction tip ${walletTipId} not found`); logger.warn(`transaction reward ${walletRewardId} not found`);
return; return;
} }
let newStatus: TipRecordStatus | undefined = undefined; let newStatus: RewardRecordStatus | undefined = undefined;
switch (tipRec.status) { switch (rewardRec.status) {
case TipRecordStatus.Done: case RewardRecordStatus.Done:
case TipRecordStatus.PendingPickup: case RewardRecordStatus.PendingPickup:
case TipRecordStatus.Aborted: case RewardRecordStatus.Aborted:
case TipRecordStatus.DialogAccept: case RewardRecordStatus.DialogAccept:
break; break;
case TipRecordStatus.SuspendidPickup: case RewardRecordStatus.SuspendidPickup:
newStatus = TipRecordStatus.PendingPickup; newStatus = RewardRecordStatus.PendingPickup;
break; break;
default: default:
assertUnreachable(tipRec.status); assertUnreachable(rewardRec.status);
} }
if (newStatus != null) { if (newStatus != null) {
const oldTxState = computeTipTransactionStatus(tipRec); const oldTxState = computeRewardTransactionStatus(rewardRec);
tipRec.status = newStatus; rewardRec.status = newStatus;
const newTxState = computeTipTransactionStatus(tipRec); const newTxState = computeRewardTransactionStatus(rewardRec);
await tx.tips.put(tipRec); await tx.rewards.put(rewardRec);
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,
walletTipId: string, walletRewardId: string,
): Promise<void> { ): Promise<void> {
const taskId = constructTaskIdentifier({ const taskId = constructTaskIdentifier({
tag: PendingTaskType.TipPickup, tag: PendingTaskType.RewardPickup,
walletTipId, walletRewardId: walletRewardId,
}); });
const transactionId = constructTransactionIdentifier({ const transactionId = constructTransactionIdentifier({
tag: TransactionType.Tip, tag: TransactionType.Reward,
walletTipId, walletRewardId: walletRewardId,
}); });
stopLongpolling(ws, taskId); stopLongpolling(ws, taskId);
const transitionInfo = await ws.db const transitionInfo = await ws.db
.mktx((x) => [x.tips]) .mktx((x) => [x.rewards])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const tipRec = await tx.tips.get(walletTipId); const tipRec = await tx.rewards.get(walletRewardId);
if (!tipRec) { if (!tipRec) {
logger.warn(`transaction tip ${walletTipId} not found`); logger.warn(`transaction tip ${walletRewardId} not found`);
return; return;
} }
let newStatus: TipRecordStatus | undefined = undefined; let newStatus: RewardRecordStatus | undefined = undefined;
switch (tipRec.status) { switch (tipRec.status) {
case TipRecordStatus.Done: case RewardRecordStatus.Done:
case TipRecordStatus.Aborted: case RewardRecordStatus.Aborted:
case TipRecordStatus.PendingPickup: case RewardRecordStatus.PendingPickup:
case TipRecordStatus.DialogAccept: case RewardRecordStatus.DialogAccept:
break; break;
case TipRecordStatus.SuspendidPickup: case RewardRecordStatus.SuspendidPickup:
newStatus = TipRecordStatus.Aborted; newStatus = RewardRecordStatus.Aborted;
break; break;
default: default:
assertUnreachable(tipRec.status); assertUnreachable(tipRec.status);
} }
if (newStatus != null) { if (newStatus != null) {
const oldTxState = computeTipTransactionStatus(tipRec); const oldTxState = computeRewardTransactionStatus(tipRec);
tipRec.status = newStatus; tipRec.status = newStatus;
const newTxState = computeTipTransactionStatus(tipRec); const newTxState = computeRewardTransactionStatus(tipRec);
await tx.tips.put(tipRec); await tx.rewards.put(tipRec);
return { return {
oldTxState, oldTxState,
newTxState, newTxState,

View File

@ -58,7 +58,7 @@ import {
RefreshGroupRecord, RefreshGroupRecord,
RefreshOperationStatus, RefreshOperationStatus,
RefundGroupRecord, RefundGroupRecord,
TipRecord, RewardRecord,
WalletContractData, WalletContractData,
WithdrawalGroupRecord, WithdrawalGroupRecord,
WithdrawalGroupStatus, WithdrawalGroupStatus,
@ -107,11 +107,11 @@ import {
import { import {
abortTipTransaction, abortTipTransaction,
failTipTransaction, failTipTransaction,
computeTipTransactionStatus, computeRewardTransactionStatus,
resumeTipTransaction, resumeTipTransaction,
suspendTipTransaction, suspendRewardTransaction,
computeTipTransactionActions, computeTipTransactionActions,
} from "./tip.js"; } from "./reward.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.Tip]: 2, [TransactionType.Reward]: 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.Tip: { case TransactionType.Reward: {
const tipId = parsedTx.walletTipId; const tipId = parsedTx.walletRewardId;
return await ws.db return await ws.db
.mktx((x) => [x.tips, x.operationRetries]) .mktx((x) => [x.rewards, x.operationRetries])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const tipRecord = await tx.tips.get(tipId); const tipRecord = await tx.rewards.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: TipRecord, tipRecord: RewardRecord,
ort?: OperationRetryRecord, ort?: OperationRetryRecord,
): Transaction { ): Transaction {
checkLogicInvariant(!!tipRecord.acceptedTimestamp); checkLogicInvariant(!!tipRecord.acceptedTimestamp);
return { return {
type: TransactionType.Tip, type: TransactionType.Reward,
txState: computeTipTransactionStatus(tipRecord), txState: computeRewardTransactionStatus(tipRecord),
txActions: computeTipTransactionActions(tipRecord), txActions: computeTipTransactionActions(tipRecord),
amountEffective: Amounts.stringify(tipRecord.tipAmountEffective), amountEffective: Amounts.stringify(tipRecord.rewardAmountEffective),
amountRaw: Amounts.stringify(tipRecord.tipAmountRaw), amountRaw: Amounts.stringify(tipRecord.rewardAmountRaw),
timestamp: tipRecord.acceptedTimestamp, timestamp: tipRecord.acceptedTimestamp,
transactionId: constructTransactionIdentifier({ transactionId: constructTransactionIdentifier({
tag: TransactionType.Tip, tag: TransactionType.Reward,
walletTipId: tipRecord.walletTipId, walletRewardId: tipRecord.walletRewardId,
}), }),
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.tips, x.rewards,
x.tombstones, x.tombstones,
x.withdrawalGroups, x.withdrawalGroups,
x.refreshGroups, x.refreshGroups,
@ -1200,11 +1200,11 @@ export async function getTransactions(
); );
}); });
tx.tips.iter().forEachAsync(async (tipRecord) => { tx.rewards.iter().forEachAsync(async (tipRecord) => {
if ( if (
shouldSkipCurrency( shouldSkipCurrency(
transactionsRequest, transactionsRequest,
Amounts.parseOrThrow(tipRecord.tipAmountRaw).currency, Amounts.parseOrThrow(tipRecord.rewardAmountRaw).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.Tip; walletTipId: string } | { tag: TransactionType.Reward; walletRewardId: 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.Tip: case TransactionType.Reward:
return `txn:${pTxId.tag}:${pTxId.walletTipId}` as TransactionIdStr; return `txn:${pTxId.tag}:${pTxId.walletRewardId}` 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.Tip: case TransactionType.Reward:
return { return {
tag: TransactionType.Tip, tag: TransactionType.Reward,
walletTipId: rest[0], walletRewardId: 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.Tip: { case TransactionType.Reward: {
const taskId = constructTaskIdentifier({ const taskId = constructTaskIdentifier({
tag: PendingTaskType.TipPickup, tag: PendingTaskType.RewardPickup,
walletTipId: parsedTx.walletTipId, walletRewardId: parsedTx.walletRewardId,
}); });
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.Tip: case TransactionType.Reward:
await suspendTipTransaction(ws, tx.walletTipId); await suspendRewardTransaction(ws, tx.walletRewardId);
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.Tip: case TransactionType.Reward:
await failTipTransaction(ws, tx.walletTipId); await failTipTransaction(ws, tx.walletRewardId);
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.Tip: case TransactionType.Reward:
await resumeTipTransaction(ws, tx.walletTipId); await resumeTipTransaction(ws, tx.walletRewardId);
break; break;
} }
} }
@ -1763,16 +1763,16 @@ export async function deleteTransaction(
return; return;
} }
case TransactionType.Tip: { case TransactionType.Reward: {
const tipId = parsedTx.walletTipId; const tipId = parsedTx.walletRewardId;
await ws.db await ws.db
.mktx((x) => [x.tips, x.tombstones]) .mktx((x) => [x.rewards, x.tombstones])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const tipRecord = await tx.tips.get(tipId); const tipRecord = await tx.rewards.get(tipId);
if (tipRecord) { if (tipRecord) {
await tx.tips.delete(tipId); await tx.rewards.delete(tipId);
await tx.tombstones.put({ await tx.tombstones.put({
id: TombstoneTag.DeleteTip + ":" + tipId, id: TombstoneTag.DeleteReward + ":" + 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.Tip: case TransactionType.Reward:
await abortTipTransaction(ws, txId.walletTipId); await abortTipTransaction(ws, txId.walletRewardId);
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,6 +62,10 @@ import {
ExchangeWithdrawResponse, ExchangeWithdrawResponse,
WithdrawUriInfoResponse, WithdrawUriInfoResponse,
ExchangeBatchWithdrawRequest, ExchangeBatchWithdrawRequest,
ExchangeAgeWithdrawRequest,
ExchangeAgeWithdrawRevealRequest,
ExchangeAgeWithdrawResponse,
ExchangeAgeWithdrawRevealResponse,
TransactionState, TransactionState,
TransactionMajorState, TransactionMajorState,
TransactionMinorState, TransactionMinorState,
@ -861,6 +865,7 @@ 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,
@ -923,6 +928,8 @@ 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",
TipPickup = "tip-pickup", RewardPickup = "reward-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.TipPickup; type: PendingTaskType.RewardPickup;
tipId: string; tipId: string;
merchantBaseUrl: string; merchantBaseUrl: string;
merchantTipId: string; merchantTipId: string;

View File

@ -34,3 +34,11 @@ 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,
AcceptTipRequest, AcceptRewardRequest,
AcceptTipResponse, AcceptTipResponse,
AcceptWithdrawalResponse, AcceptWithdrawalResponse,
AddExchangeRequest, AddExchangeRequest,
@ -85,8 +85,8 @@ import {
PreparePeerPushCreditRequest, PreparePeerPushCreditRequest,
PreparePeerPushCreditResponse, PreparePeerPushCreditResponse,
PrepareRefundRequest, PrepareRefundRequest,
PrepareTipRequest, PrepareRewardRequest as PrepareRewardRequest,
PrepareTipResult, PrepareTipResult as PrepareRewardResult,
RecoveryLoadRequest, RecoveryLoadRequest,
RetryTransactionRequest, RetryTransactionRequest,
SetCoinSuspendedRequest, SetCoinSuspendedRequest,
@ -114,6 +114,8 @@ 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 {
@ -178,8 +180,8 @@ export enum WalletApiOperation {
DumpCoins = "dumpCoins", DumpCoins = "dumpCoins",
SetCoinSuspended = "setCoinSuspended", SetCoinSuspended = "setCoinSuspended",
ForceRefresh = "forceRefresh", ForceRefresh = "forceRefresh",
PrepareTip = "prepareTip", PrepareReward = "prepareReward",
AcceptTip = "acceptTip", AcceptReward = "acceptReward",
ExportBackup = "exportBackup", ExportBackup = "exportBackup",
AddBackupProvider = "addBackupProvider", AddBackupProvider = "addBackupProvider",
RemoveBackupProvider = "removeBackupProvider", RemoveBackupProvider = "removeBackupProvider",
@ -210,6 +212,7 @@ export enum WalletApiOperation {
ApplyDevExperiment = "applyDevExperiment", ApplyDevExperiment = "applyDevExperiment",
ValidateIban = "validateIban", ValidateIban = "validateIban",
TestingWaitTransactionsFinal = "testingWaitTransactionsFinal", TestingWaitTransactionsFinal = "testingWaitTransactionsFinal",
GetScopedCurrencyInfo = "getScopedCurrencyInfo",
} }
// group: Initialization // group: Initialization
@ -507,23 +510,23 @@ export type StartRefundQueryOp = {
response: EmptyObject; response: EmptyObject;
}; };
// group: Tipping // group: Rewards
/** /**
* Query and store information about a tip. * Query and store information about a reward.
*/ */
export type PrepareTipOp = { export type PrepareTipOp = {
op: WalletApiOperation.PrepareTip; op: WalletApiOperation.PrepareReward;
request: PrepareTipRequest; request: PrepareRewardRequest;
response: PrepareTipResult; response: PrepareRewardResult;
}; };
/** /**
* Accept a tip. * Accept a reward.
*/ */
export type AcceptTipOp = { export type AcceptTipOp = {
op: WalletApiOperation.AcceptTip; op: WalletApiOperation.AcceptReward;
request: AcceptTipRequest; request: AcceptRewardRequest;
response: AcceptTipResponse; response: AcceptTipResponse;
}; };
@ -601,6 +604,12 @@ export type ListCurrenciesOp = {
response: WalletCurrencyInfo; response: WalletCurrencyInfo;
}; };
export type GetScopedCurrencyInfoOp = {
op: WalletApiOperation.GetScopedCurrencyInfo;
request: GetCurrencyInfoRequest;
response: GetCurrencyInfoResponse;
};
// group: Deposits // group: Deposits
/** /**
@ -1023,8 +1032,8 @@ export type WalletOperations = {
[WalletApiOperation.ForceRefresh]: ForceRefreshOp; [WalletApiOperation.ForceRefresh]: ForceRefreshOp;
[WalletApiOperation.DeleteTransaction]: DeleteTransactionOp; [WalletApiOperation.DeleteTransaction]: DeleteTransactionOp;
[WalletApiOperation.RetryTransaction]: RetryTransactionOp; [WalletApiOperation.RetryTransaction]: RetryTransactionOp;
[WalletApiOperation.PrepareTip]: PrepareTipOp; [WalletApiOperation.PrepareReward]: PrepareTipOp;
[WalletApiOperation.AcceptTip]: AcceptTipOp; [WalletApiOperation.AcceptReward]: AcceptTipOp;
[WalletApiOperation.StartRefundQueryForUri]: StartRefundQueryForUriOp; [WalletApiOperation.StartRefundQueryForUri]: StartRefundQueryForUriOp;
[WalletApiOperation.StartRefundQuery]: StartRefundQueryOp; [WalletApiOperation.StartRefundQuery]: StartRefundQueryOp;
[WalletApiOperation.ListCurrencies]: ListCurrenciesOp; [WalletApiOperation.ListCurrencies]: ListCurrenciesOp;
@ -1072,6 +1081,7 @@ 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,
codecForPrepareTipRequest, codecForPrepareRewardRequest,
codecForResumeTransaction, codecForResumeTransaction,
codecForRetryTransactionRequest, codecForRetryTransactionRequest,
codecForSetCoinSuspendedRequest, codecForSetCoinSuspendedRequest,
@ -118,6 +118,8 @@ import {
sampleWalletCoreTransactions, sampleWalletCoreTransactions,
validateIban, validateIban,
codecForSharePaymentRequest, codecForSharePaymentRequest,
GetCurrencyInfoResponse,
codecForGetCurrencyInfoRequest,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { import {
HttpRequestLibrary, HttpRequestLibrary,
@ -249,10 +251,10 @@ import {
} from "./operations/testing.js"; } from "./operations/testing.js";
import { import {
acceptTip, acceptTip,
computeTipTransactionStatus, computeRewardTransactionStatus,
prepareTip, prepareTip,
processTip, processTip,
} from "./operations/tip.js"; } from "./operations/reward.js";
import { import {
abortTransaction, abortTransaction,
deleteTransaction, deleteTransaction,
@ -300,6 +302,7 @@ 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";
@ -328,7 +331,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.TipPickup: case PendingTaskType.RewardPickup:
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);
@ -1016,12 +1019,6 @@ 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.
*/ */
@ -1355,9 +1352,9 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
refreshGroupId, refreshGroupId,
}; };
} }
case WalletApiOperation.PrepareTip: { case WalletApiOperation.PrepareReward: {
const req = codecForPrepareTipRequest().decode(payload); const req = codecForPrepareRewardRequest().decode(payload);
return await prepareTip(ws, req.talerTipUri); return await prepareTip(ws, req.talerRewardUri);
} }
case WalletApiOperation.StartRefundQueryForUri: { case WalletApiOperation.StartRefundQueryForUri: {
const req = codecForPrepareRefundRequest().decode(payload); const req = codecForPrepareRefundRequest().decode(payload);
@ -1375,9 +1372,9 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
await startQueryRefund(ws, txIdParsed.proposalId); await startQueryRefund(ws, txIdParsed.proposalId);
return {}; return {};
} }
case WalletApiOperation.AcceptTip: { case WalletApiOperation.AcceptReward: {
const req = codecForAcceptTipRequest().decode(payload); const req = codecForAcceptTipRequest().decode(payload);
return await acceptTip(ws, req.walletTipId); return await acceptTip(ws, req.walletRewardId);
} }
case WalletApiOperation.ExportBackupPlain: { case WalletApiOperation.ExportBackupPlain: {
return exportBackup(ws); return exportBackup(ws);
@ -1400,6 +1397,17 @@ 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);
@ -1590,15 +1598,15 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
} }
export function getVersion(ws: InternalWalletState): WalletCoreVersion { export function getVersion(ws: InternalWalletState): WalletCoreVersion {
const version: WalletCoreVersion = { const result: WalletCoreVersion = {
hash: GIT_HASH, hash: undefined,
version: VERSION, version: WALLET_CORE_IMPLEMENTATION_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 version; return result;
} }
/** /**
@ -1889,12 +1897,12 @@ class InternalWalletStateImpl implements InternalWalletState {
} }
return computeRefreshTransactionState(rec); return computeRefreshTransactionState(rec);
} }
case TransactionType.Tip: { case TransactionType.Reward: {
const rec = await tx.tips.get(parsedTxId.walletTipId); const rec = await tx.rewards.get(parsedTxId.walletRewardId);
if (!rec) { if (!rec) {
return undefined; return undefined;
} }
return computeTipTransactionStatus(rec); return computeRewardTransactionStatus(rec);
} }
default: default:
assertUnreachable(parsedTxId); assertUnreachable(parsedTxId);

View File

@ -7,8 +7,10 @@
"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,
@ -31,5 +33,8 @@
"path": "../taler-util/" "path": "../taler-util/"
} }
], ],
"include": ["src/**/*"] "include": [
"src/**/*",
"src/*.json"
]
} }

View File

@ -36,6 +36,7 @@
"@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,6 +41,15 @@ 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");
@ -169,6 +178,53 @@ 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();
@ -190,7 +246,11 @@ export function installNativeWalletListener(): void {
let respMsg: CoreApiResponse; let respMsg: CoreApiResponse;
try { try {
if (msg.operation.startsWith("anastasis")) {
respMsg = await handleAnastasisRequest(operation, id, msg.args ?? {});
} else {
respMsg = await handler.handleMessage(operation, id, msg.args ?? {}); respMsg = await handler.handleMessage(operation, id, msg.args ?? {});
}
} catch (e) { } catch (e) {
respMsg = { respMsg = {
type: "error", type: "error",
@ -265,7 +325,36 @@ 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.Tip]: "ctaTips", [TalerUriAction.Reward]: "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.Tip: case TransactionType.Reward:
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