Compare commits
50 Commits
0b60602833
...
70fca92e78
Author | SHA1 | Date | |
---|---|---|---|
70fca92e78 | |||
819949d7f2 | |||
|
adb0e70f15 | ||
ed33c4dfce | |||
|
73ad4154f6 | ||
|
58e00418cf | ||
|
598a8ec8df | ||
|
2e68d21e36 | ||
d59a23885e | |||
e6c0689806 | |||
|
d33b70b069 | ||
|
a204105b5b | ||
|
b1cea84ca8 | ||
|
7d53aa2755 | ||
|
ef148b1501 | ||
|
9f776d3fb0 | ||
|
7d1621767c | ||
|
8eb0183c78 | ||
|
37d0f9438e | ||
44aeaba7b4 | |||
0547df9538 | |||
6286699f26 | |||
308a4282cb | |||
|
951d1b66fa | ||
|
acf110dd78 | ||
|
6f4548c892 | ||
c12a366d49 | |||
cf49af2bb9 | |||
77ea209ddb | |||
2b9faf3d4e | |||
ee47aa4837 | |||
57e86b759e | |||
0b5d0cdc71 | |||
3d7df63c59 | |||
fdbd55d2bd | |||
0fe4840ca2 | |||
5422f679b3 | |||
475fdb502b | |||
|
21f678ead9 | ||
|
72b8a70da2 | ||
|
60929c34f3 | ||
|
22a3017d52 | ||
|
efed6b32c5 | ||
e3460de331 | |||
|
0ac5dba088 | ||
|
0f6310bba4 | ||
|
5a91ec2da6 | ||
72a0da7bbe | |||
|
e0f32dc899 | ||
|
ef51ba983f |
9
Makefile
9
Makefile
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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 };
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
41
packages/anastasis-cli/Makefile
Normal file
41
packages/anastasis-cli/Makefile
Normal 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
|
4
packages/anastasis-cli/README.md
Normal file
4
packages/anastasis-cli/README.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# anastasis-cli
|
||||||
|
|
||||||
|
This package provides `anastasis-cli`, the command-line interface for the
|
||||||
|
Anastasis backup system.
|
20
packages/anastasis-cli/bin/anastasis-cli.mjs
Executable file
20
packages/anastasis-cli/bin/anastasis-cli.mjs
Executable 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();
|
70
packages/anastasis-cli/build-node.mjs
Executable file
70
packages/anastasis-cli/build-node.mjs
Executable 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);
|
||||||
|
});
|
44
packages/anastasis-cli/package.json
Normal file
44
packages/anastasis-cli/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
2
packages/anastasis-cli/src/import-meta-url.js
Normal file
2
packages/anastasis-cli/src/import-meta-url.js
Normal 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);
|
87
packages/anastasis-cli/src/index.ts
Normal file
87
packages/anastasis-cli/src/index.ts
Normal 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();
|
||||||
|
}
|
33
packages/anastasis-cli/tsconfig.json
Normal file
33
packages/anastasis-cli/tsconfig.json
Normal 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/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -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": {
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
import { reducerCliMain } from "./cli.js";
|
|
||||||
|
|
||||||
async function r() {
|
|
||||||
reducerCliMain();
|
|
||||||
}
|
|
||||||
|
|
||||||
r();
|
|
@ -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();
|
|
||||||
}
|
|
@ -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),
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
export * from "./index.js";
|
|
||||||
export { reducerCliMain } from "./cli.js";
|
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
@ -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 () => {
|
||||||
|
@ -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) ||
|
||||||
|
@ -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 />
|
||||||
|
@ -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
|
||||||
|
@ -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 "{data.paytoUri}"</div>
|
<div>Payto from server is not valid "{data.paytoUri}"</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>
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -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={() => {
|
||||||
|
@ -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(
|
||||||
|
@ -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()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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`,
|
||||||
|
@ -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.`,
|
||||||
|
@ -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
|
||||||
*/}
|
*/}
|
||||||
|
@ -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>
|
||||||
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -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>
|
||||||
|
@ -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">
|
||||||
|
@ -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>"{instance}"</b>.{" "}
|
You are viewing the instance <b>"{instance}"</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>
|
||||||
);
|
);
|
||||||
|
@ -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;
|
||||||
|
@ -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],
|
||||||
|
@ -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];
|
||||||
}
|
}
|
||||||
|
59
packages/merchant-backoffice-ui/src/hooks/useSettings.ts
Normal file
59
packages/merchant-backoffice-ui/src/hooks/useSettings.ts
Normal 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];
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
}}
|
}}
|
||||||
|
@ -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.`}
|
||||||
/>
|
/>
|
||||||
|
@ -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
|
||||||
|
@ -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`}
|
||||||
|
@ -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't exist.</p>
|
<p>That page doesn't exist.</p>
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<h4>Back to Home</h4>
|
<h4>Back to Home</h4>
|
||||||
|
77
packages/merchant-backoffice-ui/src/paths/settings/index.tsx
Normal file
77
packages/merchant-backoffice-ui/src/paths/settings/index.tsx
Normal 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 />
|
||||||
|
|
||||||
|
{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>
|
||||||
|
}
|
@ -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%);
|
||||||
}
|
}
|
||||||
|
51
packages/merchant-backoffice-ui/src/scss/toggle.scss
Normal file
51
packages/merchant-backoffice-ui/src/scss/toggle.scss
Normal 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;
|
||||||
|
}
|
@ -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),
|
||||||
|
@ -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();
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 &&
|
||||||
|
@ -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,
|
||||||
|
@ -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`);
|
||||||
|
@ -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": [
|
||||||
|
10
packages/taler-util/src/argon2-impl.missing.ts
Normal file
10
packages/taler-util/src/argon2-impl.missing.ts
Normal 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.");
|
||||||
|
}
|
19
packages/taler-util/src/argon2-impl.node.ts
Normal file
19
packages/taler-util/src/argon2-impl.node.ts
Normal 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",
|
||||||
|
});
|
||||||
|
}
|
18
packages/taler-util/src/argon2.ts
Normal file
18
packages/taler-util/src/argon2.ts
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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,
|
||||||
|
@ -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",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
1
packages/taler-wallet-core/.gitignore
vendored
1
packages/taler-wallet-core/.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
/lib
|
/lib
|
||||||
/coverage
|
/coverage
|
||||||
|
/src/version.json
|
||||||
|
@ -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,
|
||||||
|
@ -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> {
|
||||||
|
@ -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:
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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:
|
||||||
|
@ -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,
|
@ -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");
|
||||||
|
@ -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`,
|
||||||
|
@ -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;
|
||||||
|
@ -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__";
|
||||||
|
@ -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<
|
||||||
|
@ -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);
|
||||||
|
@ -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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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;
|
@ -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",
|
||||||
|
@ -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
Loading…
Reference in New Issue
Block a user