Compare commits

..

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

119 changed files with 969 additions and 4195 deletions

View File

@ -20,7 +20,6 @@ dist:
$(git-archive-all) \
--include ./configure \
--include ./packages/taler-wallet-cli/configure \
--include ./packages/anastasis-cli/configure \
--include ./packages/demobank-ui/configure \
--include ./packages/taler-harness/configure \
--include ./packages/merchant-backoffice-ui/configure \
@ -122,17 +121,15 @@ install:
pnpm install --frozen-lockfile
pnpm run compile
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/demobank-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
.PHONY: install-tools
# Install taler-wallet-cli, anastasis-cli and taler-harness
# Install taler-wallet-cli and taler-harness
install-tools:
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/anastasis-cli... --filter @gnu-taler/taler-harness... compile
pnpm install --frozen-lockfile --filter @gnu-taler/taler-wallet-cli... --filter @gnu-taler/taler-harness...
pnpm run --filter @gnu-taler/taler-wallet-cli... --filter @gnu-taler/taler-harness... compile
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

View File

@ -27,7 +27,6 @@ copy_configure() {
our_configure=build-system/taler-build-scripts/configure
copy_configure "$our_configure" ./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/merchant-backoffice-ui/configure
copy_configure "$our_configure" ./packages/taler-harness/configure

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,20 +0,0 @@
#!/usr/bin/env node
/*
This file is part of GNU Taler
(C) 2022 Taler Systems SA
TALER is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
TALER is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { reducerCliMain } from '../dist/anastasis-cli-bundled.cjs';
reducerCliMain();

View File

@ -1,70 +0,0 @@
#!/usr/bin/env node
/*
This file is part of GNU Taler
(C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import esbuild from "esbuild";
import path from "path";
import fs from "fs";
const BASE = process.cwd();
let GIT_ROOT = BASE;
while (!fs.existsSync(path.join(GIT_ROOT, ".git")) && GIT_ROOT !== "/") {
GIT_ROOT = path.join(GIT_ROOT, "../");
}
if (GIT_ROOT === "/") {
console.log("not found");
process.exit(1);
}
const GIT_HASH = GIT_ROOT === "/" ? undefined : git_hash();
let _package = JSON.parse(fs.readFileSync(path.join(BASE, "package.json")));
function git_hash() {
const rev = fs
.readFileSync(path.join(GIT_ROOT, ".git", "HEAD"))
.toString()
.trim()
.split(/.*[: ]/)
.slice(-1)[0];
if (rev.indexOf("/") === -1) {
return rev;
} else {
return fs.readFileSync(path.join(GIT_ROOT, ".git", rev)).toString().trim();
}
}
export const buildConfig = {
entryPoints: ["src/index.ts"],
outfile: "dist/anastasis-cli-bundled.cjs",
bundle: true,
minify: false,
target: ["es2020"],
format: "cjs",
platform: "node",
sourcemap: true,
inject: ["src/import-meta-url.js"],
define: {
__VERSION__: `"${_package.version}"`,
__GIT_HASH__: `"${GIT_HASH}"`,
["import.meta.url"]: "import_meta_url",
},
};
esbuild.build(buildConfig).catch((e) => {
console.log(e);
process.exit(1);
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { Amounts, HttpStatusCode, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util";
import { Amounts, HttpStatusCode, parsePaytoUri } from "@gnu-taler/taler-util";
import {
ErrorType,
HttpResponsePaginated,
@ -27,7 +27,6 @@ import { useAccountDetails } from "../hooks/access.js";
import { LoginForm } from "./LoginForm.js";
import { PaymentOptions } from "./PaymentOptions.js";
import { notifyError } from "../hooks/notification.js";
import { useEffect, useState } from "preact/hooks";
interface Props {
account: string;
@ -35,60 +34,6 @@ interface Props {
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
) => 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
*/
@ -121,6 +66,7 @@ export function AccountPage({ account, onLoadNotOk }: Props): VNode {
<div>Payto from server is not valid &quot;{data.paytoUri}&quot;</div>
);
}
const accountNumber = payto.iban;
const balanceIsDebit = data.balance.credit_debit_indicator == "debit";
const limit = balanceIsDebit
? Amounts.sub(debitThreshold, balance).amount
@ -130,7 +76,8 @@ export function AccountPage({ account, onLoadNotOk }: Props): VNode {
<div>
<h1 class="nav welcome-text">
<i18n.Translate>
Welcome, {account} (<a href={stringifyPaytoUri(payto)}>{payto.iban}</a>)! <CopyButton getContent={() => stringifyPaytoUri(payto)} />
Welcome, {accountNumber ? `${account} (${accountNumber})` : account}
!
</i18n.Translate>
</h1>
</div>

View File

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

View File

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

View File

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

View File

@ -24,7 +24,6 @@ import { Fragment, VNode, h } from "preact";
import { Loading } from "../components/Loading.js";
import { useWithdrawalDetails } from "../hooks/access.js";
import { notifyInfo } from "../hooks/notification.js";
import { useSettings } from "../hooks/settings.js";
import { handleNotOkResult } from "./HomePage.js";
import { QrCodeSection } from "./QrCodeSection.js";
import { WithdrawalConfirmationQuestion } from "./WithdrawalConfirmationQuestion.js";
@ -33,7 +32,8 @@ const logger = new Logger("WithdrawalQRCode");
interface Props {
withdrawUri: WithdrawUriResult;
onContinue: () => void;
onAborted: () => void;
onConfirmed: () => void;
onLoadNotOk: () => void;
}
/**
@ -43,14 +43,10 @@ interface Props {
*/
export function WithdrawalQRCode({
withdrawUri,
onContinue,
onConfirmed,
onAborted,
onLoadNotOk,
}: Props): VNode {
const [settings, updateSettings] = useSettings();
function clearCurrentWithdrawal(): void {
updateSettings("currentWithdrawalOperationId", undefined);
onContinue();
}
const { i18n } = useTranslationContext();
const result = useWithdrawalDetails(withdrawUri.withdrawalOperationId);
if (!result.ok) {
@ -68,64 +64,13 @@ export function WithdrawalQRCode({
}
const { data } = result;
if (data.aborted) {
return <section id="main" class="content">
<h1 class="nav">{i18n.str`Operation aborted`}</h1>
<section>
<p>
<i18n.Translate>
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>
logger.trace("withdrawal status", data);
if (data.aborted || data.confirmation_done) {
// signal that this withdrawal is aborted
// will redirect to account info
notifyInfo(i18n.str`Operation completed`);
onAborted();
return <Loading />;
}
if (!data.selection_done) {
@ -134,20 +79,24 @@ export function WithdrawalQRCode({
withdrawUri={withdrawUri}
onAborted={() => {
notifyInfo(i18n.str`Operation canceled`);
clearCurrentWithdrawal()
onContinue()
onAborted();
}}
/>
);
}
// Wallet POSTed the withdrawal details! Ask the
// user to authorize the operation (here CAPTCHA).
return (
<WithdrawalConfirmationQuestion
withdrawUri={withdrawUri}
onConfirmed={() => {
notifyInfo(i18n.str`Operation confirmed`);
onConfirmed();
}}
onAborted={() => {
notifyInfo(i18n.str`Operation canceled`);
clearCurrentWithdrawal()
onContinue()
onAborted();
}}
/>
);

View File

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

View File

@ -134,7 +134,7 @@ export function buildRequestErrorMessage(
specialCases.onClientError && specialCases.onClientError(cause.status);
result = {
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),
};
break;
@ -146,7 +146,7 @@ export function buildRequestErrorMessage(
title: title
? title
: i18n.str`The server had problems processing the request`,
description: cause?.payload?.error?.description,
description: cause.payload.error.description,
debug: JSON.stringify(cause),
};
break;
@ -154,7 +154,7 @@ export function buildRequestErrorMessage(
case ErrorType.UNREADABLE: {
result = {
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),
};
break;

View File

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

View File

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

View File

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

View File

@ -20,7 +20,7 @@
*/
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { ComponentChildren, h, VNode } from "preact";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { useBackendContext } from "../../context/backend.js";
import { useInstanceContext } from "../../context/instance.js";
@ -40,7 +40,7 @@ function getTokenValuePart(t: string): string {
}
function normalizeToken(r: string): string {
return `secret-token:${r}`;
return `secret-token:${encodeURIComponent(r)}`;
}
function cleanUp(s: string): string {
@ -53,7 +53,7 @@ function cleanUp(s: string): string {
export function LoginModal({ onConfirm, withMessage }: Props): VNode {
const { url: backendUrl, token: baseToken } = useBackendContext();
const { admin, token: instanceToken, id } = useInstanceContext();
const { admin, token: instanceToken } = useInstanceContext();
const testLogin = useCredentialsChecker();
const currentToken = getTokenValuePart(
(!admin ? baseToken : instanceToken) ?? "",
@ -63,78 +63,6 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode {
const [url, setURL] = useState(cleanUp(backendUrl));
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 (
<div class="columns is-centered" style={{ margin: "auto" }}>
<div class="column is-two-thirds ">
@ -209,7 +137,8 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode {
borderTop: 0,
}}
>
<AsyncButton
<button
class="button is-info"
onClick={async () => {
const secretToken = normalizeToken(token);
const { valid, cause } = await testLogin(url, secretToken);
@ -221,24 +150,10 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode {
}}
>
<i18n.Translate>Confirm</i18n.Translate>
</AsyncButton>
</button>
</footer>
</div>
</div>
</div>
);
}
function AsyncButton({ onClick, children }: { onClick: () => Promise<void>, children: ComponentChildren }): VNode {
const [running, setRunning] = useState(false)
return <button class="button is-info" disabled={running} onClick={() => {
setRunning(true)
onClick().then(() => {
setRunning(false)
}).catch(() => {
setRunning(false)
})
}}>
{children}
</button>
}

View File

@ -44,7 +44,7 @@ export function InputSelector<T>({
fromStr = defaultFromString,
toStr = defaultToString,
}: Props<keyof T>): VNode {
const { error, value, onChange, required } = useField<T>(name);
const { error, value, onChange } = useField<T>(name);
return (
<div class="field is-horizontal">
<div class="field-label is-normal">
@ -58,7 +58,7 @@ export function InputSelector<T>({
</label>
</div>
<div class="field-body is-flex-grow-3">
<div class="field has-icons-right">
<div class="field">
<p class={expand ? "control is-expanded select" : "control select"}>
<select
class={error ? "select is-danger" : "select"}
@ -78,14 +78,8 @@ export function InputSelector<T>({
);
})}
</select>
{help}
</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>}
</div>
</div>

View File

@ -1,91 +0,0 @@
/*
This file is part of GNU Taler
(C) 2021-2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { h, VNode } from "preact";
import { InputProps, useField } from "./useField.js";
interface Props<T> extends InputProps<T> {
name: T;
readonly?: boolean;
expand?: boolean;
threeState?: boolean;
toBoolean?: (v?: any) => boolean | undefined;
fromBoolean?: (s: boolean | undefined) => any;
}
const defaultToBoolean = (f?: any): boolean | undefined => f || "";
const defaultFromBoolean = (v: boolean | undefined): any => v as any;
export function InputToggle<T>({
name,
readonly,
placeholder,
tooltip,
label,
help,
threeState,
expand,
fromBoolean = defaultFromBoolean,
toBoolean = defaultToBoolean,
}: Props<keyof T>): VNode {
const { error, value, onChange } = useField<T>(name);
const onCheckboxClick = (): void => {
const c = toBoolean(value);
if (c === false && threeState) return onChange(undefined as any);
return onChange(fromBoolean(!c));
};
return (
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label" style={{ width: 200 }}>
{label}
{tooltip && (
<span class="icon has-tooltip-right" data-tooltip={tooltip}>
<i class="mdi mdi-information" />
</span>
)}
</label>
</div>
<div class="field-body is-flex-grow-1">
<div class="field">
<p class={expand ? "control is-expanded" : "control"}>
<label class="toggle" style={{ marginLeft: 4, marginTop: 0 }}>
<input
type="checkbox"
class={toBoolean(value) === undefined ? "is-indeterminate" : "toggle-checkbox"}
checked={toBoolean(value)}
placeholder={placeholder}
readonly={readonly}
name={String(name)}
disabled={readonly}
onChange={onCheckboxClick}
/>
<div class="toggle-switch"></div>
</label>
{help}
</p>
{error && <p class="help is-danger">{error}</p>}
</div>
</div>
</div>
);
}

View File

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

View File

@ -31,7 +31,6 @@ const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
interface Props {
onLogout: () => void;
onShowSettings: () => void;
mobile?: boolean;
instance: string;
admin?: boolean;
@ -41,7 +40,6 @@ interface Props {
export function Sidebar({
mobile,
instance,
onShowSettings,
onLogout,
admin,
mimic,
@ -80,7 +78,20 @@ export function Sidebar({
<div class="menu is-menu-main">
{instance ? (
<Fragment>
<p class="menu-label">
<i18n.Translate>Instance</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>Settings</i18n.Translate>
</span>
</a>
</li>
<li>
<a href={"/orders"} class="has-icon">
<span class="icon">
@ -121,31 +132,6 @@ export function Sidebar({
</span>
</a>
</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>
<a href={"/reserves"} class="has-icon">
<span class="icon">
@ -164,6 +150,16 @@ export function Sidebar({
</span>
</a>
</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>
</Fragment>
) : undefined}
@ -171,18 +167,6 @@ export function Sidebar({
<i18n.Translate>Connection</i18n.Translate>
</p>
<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>
<div>
<span style={{ width: "3rem" }} class="icon">

View File

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

View File

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

View File

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

View File

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

View File

@ -1,59 +0,0 @@
/*
This file is part of GNU Taler
(C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
import {
Codec,
buildCodecForObject,
codecForBoolean,
} from "@gnu-taler/taler-util";
function parse_json_or_undefined<T>(str: string | undefined): T | undefined {
if (str === undefined) return undefined;
try {
return JSON.parse(str);
} catch {
return undefined;
}
}
export interface Settings {
advanceOrderMode: boolean
}
const defaultSettings: Settings = {
advanceOrderMode: false,
}
export const codecForSettings = (): Codec<Settings> =>
buildCodecForObject<Settings>()
.property("advanceOrderMode", codecForBoolean())
.build("Settings");
const SETTINGS_KEY = buildStorageKey("merchant-settings", codecForSettings());
export function useSettings(): [
Readonly<Settings>,
<T extends keyof Settings>(key: T, value: Settings[T]) => void,
] {
const { value, update } = useLocalStorage(SETTINGS_KEY);
const parsed: Settings = value ?? defaultSettings;
function updateField<T extends keyof Settings>(k: T, v: Settings[T]) {
update({ ...parsed, [k]: v });
}
return [parsed, updateField];
}

View File

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

View File

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

View File

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

View File

@ -43,7 +43,6 @@ import { Duration, MerchantBackend, WithId } from "../../../../declaration.js";
import { OrderCreateSchema as schema } from "../../../../schemas/index.js";
import { rate } from "../../../../utils/amount.js";
import { undefinedIfEmpty } from "../../../../utils/table.js";
import { useSettings } from "../../../../hooks/useSettings.js";
interface Props {
onCreate: (d: MerchantBackend.Orders.PostOrderRequest) => void;
@ -139,7 +138,7 @@ export function CreatePage({
const [value, valueHandler] = useState(with_defaults(instanceConfig));
const config = useConfigContext();
const zero = Amounts.zeroOfCurrency(config.currency);
const [settings] = useSettings()
const inventoryList = Object.values(value.inventoryProducts || {});
const productList = Object.values(value.products || {});
@ -375,7 +374,6 @@ export function CreatePage({
inventory={instanceInventory}
/>
{settings.advanceOrderMode &&
<NonInventoryProductFrom
productToEdit={editingProduct}
onAddProduct={(p) => {
@ -383,7 +381,6 @@ export function CreatePage({
return addNewProduct(p);
}}
/>
}
{allProducts.length > 0 && (
<ProductList
@ -448,7 +445,6 @@ export function CreatePage({
tooltip={i18n.str`Title of the order to be shown to the customer`}
/>
{settings.advanceOrderMode &&
<InputGroup
name="shipping"
label={i18n.str`Shipping and Fulfillment`}
@ -474,9 +470,7 @@ export function CreatePage({
tooltip={i18n.str`URL to which the user will be redirected after successful payment.`}
/>
</InputGroup>
}
{settings.advanceOrderMode &&
<InputGroup
name="payments"
label={i18n.str`Taler payment options`}
@ -534,9 +528,7 @@ export function CreatePage({
}
/>
</InputGroup>
}
{settings.advanceOrderMode &&
<InputGroup
name="extra"
label={i18n.str`Additional information`}
@ -549,7 +541,6 @@ export function CreatePage({
tooltip={i18n.str`You must enter a value in JavaScript Object Notation (JSON).`}
/>
</InputGroup>
}
</FormProvider>
<div class="buttons is-right mt-5">

View File

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

View File

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

View File

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

View File

@ -139,7 +139,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
>
<InputWithAddon<Entity>
name="template_id"
help={`${backend.url}/instances/templates/${state.template_id ?? ""}`}
addonBefore={`${backend.url}/instances/templates/`}
label={i18n.str`Identifier`}
tooltip={i18n.str`Name of the template in URLs.`}
/>

View File

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

View File

@ -33,7 +33,6 @@ import { InputDuration } from "../../../../components/form/InputDuration.js";
import { InputNumber } from "../../../../components/form/InputNumber.js";
import { useBackendContext } from "../../../../context/backend.js";
import { MerchantBackend } from "../../../../declaration.js";
import { InputSelector } from "../../../../components/form/InputSelector.js";
type Entity = MerchantBackend.Webhooks.WebhookAddDetails;
@ -51,9 +50,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
const errors: FormErrors<Entity> = {
webhook_id: !state.webhook_id ? 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,
event_type: !state.event_type ? i18n.str`required` : undefined,
http_method: !state.http_method
? i18n.str`required`
: !validMethod.includes(state.http_method)
@ -87,30 +84,16 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
label={i18n.str`ID`}
tooltip={i18n.str`Webhook ID to use`}
/>
<InputSelector
<Input<Entity>
name="event_type"
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`}
/>
<InputSelector
<Input<Entity>
name="http_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`}
/>
<Input<Entity>
name="url"
label={i18n.str`URL`}

View File

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

View File

@ -1,77 +0,0 @@
import { VNode, h } from "preact";
import { LangSelector } from "../../components/menu/LangSelector.js";
import { useLang, useTranslationContext } from "@gnu-taler/web-util/browser";
import { InputToggle } from "../../components/form/InputToggle.js";
import { Settings, useSettings } from "../../hooks/useSettings.js";
import { FormErrors, FormProvider } from "../../components/form/FormProvider.js";
import { useState } from "preact/hooks";
function getBrowserLang(): string | undefined {
if (typeof window === "undefined") return undefined;
if (window.navigator.languages) return window.navigator.languages[0];
if (window.navigator.language) return window.navigator.language;
return undefined;
}
export function Settings(): VNode {
const { i18n } = useTranslationContext()
const borwserLang = getBrowserLang()
const { update } = useLang()
const [value, updateValue] = useSettings()
const errors: FormErrors<Settings> = {
}
function valueHandler(s: (d: Partial<Settings>) => Partial<Settings>): void {
const next = s(value)
updateValue("advanceOrderMode", next.advanceOrderMode ?? false)
}
return <div>
<section class="section is-main-section">
<div class="columns">
<div class="column" />
<div class="column is-four-fifths">
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label" style={{ width: 200 }}>
<i18n.Translate>Language</i18n.Translate>
<span class="icon has-tooltip-right" data-tooltip={"Force language setting instance of taking the browser"}>
<i class="mdi mdi-information" />
</span>
</label>
</div>
<div class="field has-addons">
<LangSelector />
&nbsp;
{borwserLang !== undefined && <button
data-tooltip={i18n.str`generate random secret key`}
class="button is-info mr-3"
onClick={(e) => {
update(borwserLang.substring(0, 2))
}}
>
<i18n.Translate>Set default</i18n.Translate>
</button>}
</div>
</div>
<FormProvider<Settings>
name="settings"
errors={errors}
object={value}
valueHandler={valueHandler}
>
<InputToggle<Settings>
label={i18n.str`Advance order creation`}
tooltip={i18n.str`Shows more options in the order creation form`}
name="advanceOrderMode"
/>
</FormProvider>
</div>
<div class="column" />
</div>
</section>
</div>
}

View File

@ -52,8 +52,6 @@ $tooltip-color: red;
@import "../../node_modules/@creativebulma/bulma-tooltip/dist/bulma-tooltip.min.css";
@import "../../node_modules/bulma-timeline/dist/css/bulma-timeline.min.css";
@import "toggle";
.notification {
background-color: transparent;
}
@ -94,7 +92,6 @@ div {
position: relative;
pointer-events: none;
opacity: 0.5;
&:after {
// @include loader;
position: absolute;
@ -128,7 +125,6 @@ input[type="checkbox"]:indeterminate+.check {
tr:hover .right-sticky {
background-color: hsl(0, 0%, 80%);
}
.table.is-striped tbody tr:nth-child(even):hover .right-sticky {
background-color: hsl(0, 0%, 95%);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -23,7 +23,6 @@ import {
j2s,
NotificationType,
TransactionMajorState,
TransactionMinorState,
WalletNotification,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
@ -47,14 +46,12 @@ export async function runPeerToPeerPullTest(t: GlobalTestState) {
const w1 = await createWalletDaemonWithClient(t, {
name: "w1",
persistent: true,
handleNotification(wn) {
allW1Notifications.push(wn);
},
});
const w2 = await createWalletDaemonWithClient(t, {
name: "w2",
persistent: true,
handleNotification(wn) {
allW2Notifications.push(wn);
},
@ -92,15 +89,6 @@ export async function runPeerToPeerPullTest(t: GlobalTestState) {
},
);
const peerPullCreditReadyCond = wallet1.waitForNotificationCond(
(x) => x.type === NotificationType.TransactionStateTransition &&
x.transactionId === resp.transactionId &&
x.newTxState.major === TransactionMajorState.Pending &&
x.newTxState.minor === TransactionMinorState.Ready,
);
await peerPullCreditReadyCond;
const checkResp = await wallet2.client.call(
WalletApiOperation.PreparePeerPullDebit,
{
@ -110,6 +98,8 @@ export async function runPeerToPeerPullTest(t: GlobalTestState) {
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(
(x) => x.type === NotificationType.TransactionStateTransition &&
x.transactionId === resp.transactionId &&

View File

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

View File

@ -338,17 +338,8 @@ export async function runTests(spec: TestRunSpec) {
currentChild.stdout?.pipe(harnessLogStream);
currentChild.stderr?.pipe(harnessLogStream);
// Default timeout when the test doesn't override it.
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;
const defaultTimeout = 60000;
const testTimeoutMs = testCase.timeoutMs ?? defaultTimeout;
if (spec.noTimeout) {
console.log(`running ${testName}, no timeout`);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1788,89 +1788,6 @@ export interface ExchangeRefreshRevealRequest {
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 {
// Optional base URL of the exchange for looking up wire transfers
// associated with this transaction. If not given,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -41,15 +41,6 @@ import {
Wallet,
WalletApiOperation,
} 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");
@ -178,53 +169,6 @@ class NativeWalletMessageHandler {
}
}
/**
* Handle an Anastasis request from the native app.
*/
async function handleAnastasisRequest(
operation: string,
id: string,
args: any,
): Promise<CoreApiResponse> {
const wrapSuccessResponse = (result: unknown): CoreApiResponseSuccess => {
return {
type: "response",
id,
operation,
result,
};
};
let req = args ?? {};
switch (operation) {
case "anastasisReduce":
// TODO: do some input validation here
let reduceRes = await reduceAction(req.state, req.action, req.args ?? {});
// For now, this will return "success" even if the wrapped Anastasis
// response is a ReducerStateError.
return wrapSuccessResponse(reduceRes);
case "anastasisStartBackup":
return wrapSuccessResponse(await getBackupStartState());
case "anastasisStartRecovery":
return wrapSuccessResponse(await getRecoveryStartState());
case "anastasisDiscoverPolicies":
let discoverRes = await discoverPolicies(req.state, req.cursor);
let aggregatedPolicies = mergeDiscoveryAggregate(
discoverRes.policies ?? [],
req.state.discoveryState?.aggregatedPolicies ?? [],
);
return wrapSuccessResponse({
...req.state,
discoveryState: {
state: "finished",
aggregatedPolicies,
cursor: discoverRes.cursor,
},
});
}
}
export function installNativeWalletListener(): void {
setGlobalLogLevelFromString("trace");
const handler = new NativeWalletMessageHandler();
@ -246,11 +190,7 @@ export function installNativeWalletListener(): void {
let respMsg: CoreApiResponse;
try {
if (msg.operation.startsWith("anastasis")) {
respMsg = await handleAnastasisRequest(operation, id, msg.args ?? {});
} else {
respMsg = await handler.handleMessage(operation, id, msg.args ?? {});
}
} catch (e) {
respMsg = {
type: "error",
@ -325,36 +265,7 @@ export async function testWithLocal() {
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
globalThis.testWithGv = testWithGv;
// @ts-ignore
globalThis.testWithLocal = testWithLocal;
// @ts-ignore
globalThis.testArgon2id = testArgon2id;
// @ts-ignore
globalThis.testReduceAction = reduceAction;
// @ts-ignore
globalThis.testDiscoverPolicies = discoverPolicies;

View File

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

View File

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

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