aboutsummaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorChristian Blättler <blatc2@bfh.ch>2024-06-13 11:35:52 +0200
committerChristian Blättler <blatc2@bfh.ch>2024-06-13 11:35:52 +0200
commiteb964dfae0a12f9a90eb066d610f627538f8997c (patch)
tree26a6cd74c9a29edce05b2dcd51cf497374bf8e30 /packages
parent9d0fc80a905e02a0a0b63dd547daac6e7b17fb52 (diff)
parentf9d4ff5b43e48a07ac81d7e7ef800ddb12f5f90a (diff)
downloadwallet-core-eb964dfae0a12f9a90eb066d610f627538f8997c.tar.xz
Merge branch 'master' into feature/tokens
Diffstat (limited to 'packages')
-rw-r--r--packages/aml-backoffice-ui/package.json2
-rw-r--r--packages/anastasis-cli/package.json2
-rw-r--r--packages/anastasis-core/package.json2
-rw-r--r--packages/anastasis-core/src/index.ts33
-rw-r--r--packages/anastasis-webui/package.json2
-rw-r--r--packages/auditor-backoffice-ui/package.json2
-rw-r--r--packages/bank-ui/package.json2
-rw-r--r--packages/bank-ui/src/hooks/preferences.ts11
-rw-r--r--packages/bank-ui/src/pages/OperationState/state.ts24
-rw-r--r--packages/bank-ui/src/pages/PaytoWireTransferForm.tsx57
-rw-r--r--packages/bank-ui/src/pages/QrCodeSection.tsx8
-rw-r--r--packages/bank-ui/src/pages/WalletWithdrawForm.tsx31
-rw-r--r--packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx23
-rw-r--r--packages/bank-ui/src/pages/account/ShowAccountDetails.tsx41
-rw-r--r--packages/bank-ui/src/settings.json2
-rw-r--r--packages/bank-ui/src/settings.ts16
-rw-r--r--packages/challenger-ui/package.json2
-rw-r--r--packages/idb-bridge/package.json4
-rw-r--r--packages/merchant-backend-ui/package.json2
-rw-r--r--packages/merchant-backoffice-ui/package.json2
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx79
-rw-r--r--packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx16
-rw-r--r--packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx4
-rw-r--r--packages/merchant-backoffice-ui/src/components/modal/index.tsx89
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx30
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx126
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx11
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx36
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx2
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx2
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx1
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx2
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx1
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx2
-rw-r--r--packages/pogen/package.json2
-rw-r--r--packages/taler-harness/debian/changelog24
-rw-r--r--packages/taler-harness/package.json2
-rw-r--r--packages/taler-harness/src/harness/harness.ts4
-rw-r--r--packages/taler-harness/src/harness/helpers.ts5
-rw-r--r--packages/taler-harness/src/integrationtests/test-currency-scope.ts71
-rw-r--r--packages/taler-harness/src/integrationtests/test-multiexchange.ts69
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-template.ts4
-rw-r--r--packages/taler-harness/src/integrationtests/test-refund-auto.ts166
-rw-r--r--packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts51
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-denom-expire.ts98
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-exchange-update.ts43
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-insufficient-balance.ts83
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts21
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-conversion.ts50
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts25
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-flex.ts70
-rw-r--r--packages/taler-harness/src/integrationtests/testrunner.ts4
-rw-r--r--packages/taler-util/package.json2
-rw-r--r--packages/taler-util/src/CancellationToken.ts2
-rw-r--r--packages/taler-util/src/bank-api-client.ts2
-rw-r--r--packages/taler-util/src/errors.ts5
-rw-r--r--packages/taler-util/src/http-client/bank-integration.ts8
-rw-r--r--packages/taler-util/src/http-client/bank-revenue.ts4
-rw-r--r--packages/taler-util/src/http-client/types.ts141
-rw-r--r--packages/taler-util/src/http-impl.qtart.ts6
-rw-r--r--packages/taler-util/src/invariants.ts2
-rw-r--r--packages/taler-util/src/notifications.ts2
-rw-r--r--packages/taler-util/src/payto.ts20
-rw-r--r--packages/taler-util/src/qtart.ts5
-rw-r--r--packages/taler-util/src/taler-error-codes.ts148
-rw-r--r--packages/taler-util/src/taler-types.ts4
-rw-r--r--packages/taler-util/src/transactions-types.ts7
-rw-r--r--packages/taler-util/src/wallet-types.ts54
-rw-r--r--packages/taler-wallet-cli/debian/changelog24
-rw-r--r--packages/taler-wallet-cli/package.json2
-rw-r--r--packages/taler-wallet-cli/src/index.ts10
-rw-r--r--packages/taler-wallet-core/package.json2
-rw-r--r--packages/taler-wallet-core/src/backup/index.ts16
-rw-r--r--packages/taler-wallet-core/src/balance.ts20
-rw-r--r--packages/taler-wallet-core/src/coinSelection.ts5
-rw-r--r--packages/taler-wallet-core/src/common.ts92
-rw-r--r--packages/taler-wallet-core/src/db.ts126
-rw-r--r--packages/taler-wallet-core/src/dbless.ts2
-rw-r--r--packages/taler-wallet-core/src/deposits.ts26
-rw-r--r--packages/taler-wallet-core/src/exchanges.ts348
-rw-r--r--packages/taler-wallet-core/src/instructedAmountConversion.ts22
-rw-r--r--packages/taler-wallet-core/src/pay-merchant.ts110
-rw-r--r--packages/taler-wallet-core/src/pay-peer-common.ts6
-rw-r--r--packages/taler-wallet-core/src/pay-peer-pull-credit.ts49
-rw-r--r--packages/taler-wallet-core/src/pay-peer-pull-debit.ts2
-rw-r--r--packages/taler-wallet-core/src/pay-peer-push-credit.ts35
-rw-r--r--packages/taler-wallet-core/src/pay-peer-push-debit.ts35
-rw-r--r--packages/taler-wallet-core/src/recoup.ts4
-rw-r--r--packages/taler-wallet-core/src/refresh.ts143
-rw-r--r--packages/taler-wallet-core/src/shepherd.ts70
-rw-r--r--packages/taler-wallet-core/src/transactions.ts117
-rw-r--r--packages/taler-wallet-core/src/versions.ts9
-rw-r--r--packages/taler-wallet-core/src/wallet-api-types.ts13
-rw-r--r--packages/taler-wallet-core/src/wallet.ts84
-rw-r--r--packages/taler-wallet-core/src/withdraw.ts590
-rw-r--r--packages/taler-wallet-embedded/package.json2
-rw-r--r--packages/taler-wallet-embedded/src/wallet-qjs-tests.ts118
-rw-r--r--packages/taler-wallet-embedded/src/wallet-qjs.ts209
-rw-r--r--packages/taler-wallet-webextension/manifest-common.json4
-rw-r--r--packages/taler-wallet-webextension/package.json2
-rw-r--r--packages/taler-wallet-webextension/src/components/HistoryItem.tsx4
-rw-r--r--packages/taler-wallet-webextension/src/components/WalletActivity.tsx6
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx21
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/views.tsx32
-rw-r--r--packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts105
-rw-r--r--packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx24
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferPickup/views.tsx22
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/index.ts22
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/state.ts203
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx178
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/test.ts41
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx28
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useIsOnline.ts20
-rw-r--r--packages/taler-wallet-webextension/src/platform/chrome.ts30
-rw-r--r--packages/taler-wallet-webextension/src/platform/dev.ts20
-rw-r--r--packages/taler-wallet-webextension/src/popup/BalancePage.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts2
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts68
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx15
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts57
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx13
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx15
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx1
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Transaction.tsx12
-rw-r--r--packages/taler-wallet-webextension/src/wxApi.ts2
-rw-r--r--packages/taler-wallet-webextension/src/wxBackend.ts150
-rw-r--r--packages/web-util/package.json2
-rw-r--r--packages/web-util/src/components/CopyButton.tsx2
-rw-r--r--packages/web-util/src/index.build.ts6
129 files changed, 3242 insertions, 2031 deletions
diff --git a/packages/aml-backoffice-ui/package.json b/packages/aml-backoffice-ui/package.json
index 749565946..9c33862f7 100644
--- a/packages/aml-backoffice-ui/package.json
+++ b/packages/aml-backoffice-ui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/aml-backoffice-ui",
- "version": "0.10.7",
+ "version": "0.11.4",
"author": "sebasjm",
"license": "AGPL-3.0-OR-LATER",
"description": "Back-office SPA for GNU Taler Exchange.",
diff --git a/packages/anastasis-cli/package.json b/packages/anastasis-cli/package.json
index 5a9d6abea..40bdb927e 100644
--- a/packages/anastasis-cli/package.json
+++ b/packages/anastasis-cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/anastasis-cli",
- "version": "0.10.7",
+ "version": "0.11.4",
"description": "",
"engines": {
"node": ">=0.18.0"
diff --git a/packages/anastasis-core/package.json b/packages/anastasis-core/package.json
index 576acc988..c987f0ceb 100644
--- a/packages/anastasis-core/package.json
+++ b/packages/anastasis-core/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/anastasis-core",
- "version": "0.10.7",
+ "version": "0.11.4",
"description": "",
"main": "./lib/index.js",
"module": "./lib/index.js",
diff --git a/packages/anastasis-core/src/index.ts b/packages/anastasis-core/src/index.ts
index 05fa4a49f..a48db5c25 100644
--- a/packages/anastasis-core/src/index.ts
+++ b/packages/anastasis-core/src/index.ts
@@ -43,7 +43,7 @@ import {
URL,
j2s,
} from "@gnu-taler/taler-util";
-import { HttpResponse } from "@gnu-taler/taler-util/http";
+import { HttpResponse, createPlatformHttpLib } from "@gnu-taler/taler-util/http";
import { anastasisData } from "./anastasis-data.js";
import {
codecForChallengeInstructionMessage,
@@ -139,6 +139,11 @@ export * from "./challenge-feedback-types.js";
const logger = new Logger("anastasis-core:index.ts");
+const http = createPlatformHttpLib({
+ enableThrottling: true,
+ requireTls: false,
+});
+
const ANASTASIS_HTTP_HEADER_POLICY_META_DATA = "Anastasis-Policy-Meta-Data";
function getContinents(): ContinentInfo[] {
@@ -279,9 +284,9 @@ async function getProviderInfo(
providerBaseUrl: string,
): Promise<AuthenticationProviderStatus> {
// FIXME: Use a reasonable timeout here.
- let resp: Response;
+ let resp: HttpResponse;
try {
- resp = await fetch(new URL("config", providerBaseUrl).href);
+ resp = await http.fetch(new URL("config", providerBaseUrl).href);
} catch (e) {
console.warn(
"Encountered an HTTP error whilst trying to get the provider's config: ",
@@ -293,7 +298,7 @@ async function getProviderInfo(
hint: "request to anastasis provider failed",
};
}
- if (!resp.ok) {
+ if (resp.status < 200 || resp.status > 299) {
console.warn("Got bad response code whilst getting provider config", resp);
return {
status: "error",
@@ -556,7 +561,7 @@ async function uploadSecret(
// FIXME: Get this from the params
reqUrl.searchParams.set("timeout_ms", "500");
}
- const resp = await fetch(reqUrl.href, {
+ const resp = await http.fetch(reqUrl.href, {
method: "POST",
headers: {
"content-type": "application/json",
@@ -646,7 +651,7 @@ async function uploadSecret(
reqUrl.searchParams.set("timeout_ms", "500");
}
logger.info(`uploading policy to ${prov.provider_url}`);
- const resp = await fetch(reqUrl.href, {
+ const resp = await http.fetch(reqUrl.href, {
method: "POST",
headers: {
"Anastasis-Policy-Signature": encodeCrock(sig),
@@ -757,14 +762,14 @@ async function downloadPolicyFromProvider(
const acctKeypair = accountKeypairDerive(userId);
const reqUrl = new URL(`policy/${acctKeypair.pub}`, providerUrl);
reqUrl.searchParams.set("version", `${version}`);
- const resp = await fetch(reqUrl.href);
+ const resp = await http.fetch(reqUrl.href);
if (resp.status !== 200) {
logger.info(
`Could not download policy from provider ${providerUrl}, status ${resp.status}`,
);
return undefined;
}
- const body = await resp.arrayBuffer();
+ const body = await resp.bytes();
const bodyDecrypted = await decryptRecoveryDocument(
userId,
encodeCrock(body),
@@ -981,10 +986,10 @@ async function requestTruth(
const hresp = await getResponseHash(truth, solveRequest);
- let resp: Response;
+ let resp: HttpResponse;
try {
- resp = await fetch(url.href, {
+ resp = await http.fetch(url.href, {
method: "POST",
headers: {
Accept: "application/json",
@@ -1022,7 +1027,7 @@ async function requestTruth(
truth.provider_salt,
);
- const respBody = new Uint8Array(await resp.arrayBuffer());
+ const respBody = new Uint8Array(await resp.bytes());
const keyShare = await decryptKeyShare(
encodeCrock(respBody),
userId,
@@ -1138,10 +1143,10 @@ async function selectChallenge(
}
}
- let resp: Response;
+ let resp: HttpResponse;
try {
- resp = await fetch(url.href, {
+ resp = await http.fetch(url.href, {
method: "POST",
headers: {
Accept: "application/json",
@@ -1859,7 +1864,7 @@ export async function discoverPolicies(
);
const acctKeypair = accountKeypairDerive(userId);
const reqUrl = new URL(`policy/${acctKeypair.pub}/meta`, providerUrl);
- const resp = await fetch(reqUrl.href);
+ const resp = await http.fetch(reqUrl.href);
if (resp.status !== 200) {
logger.warn(`Could not fetch policy metadate from ${reqUrl.href}`);
continue;
diff --git a/packages/anastasis-webui/package.json b/packages/anastasis-webui/package.json
index 108b1476e..9f56489d1 100644
--- a/packages/anastasis-webui/package.json
+++ b/packages/anastasis-webui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/anastasis-webui",
- "version": "0.10.7",
+ "version": "0.11.4",
"license": "MIT",
"type": "module",
"scripts": {
diff --git a/packages/auditor-backoffice-ui/package.json b/packages/auditor-backoffice-ui/package.json
index 776c179b4..ce420417c 100644
--- a/packages/auditor-backoffice-ui/package.json
+++ b/packages/auditor-backoffice-ui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/auditor-backoffice-ui",
- "version": "0.10.7",
+ "version": "0.11.4",
"license": "AGPL-3.0-or-later",
"type": "module",
"scripts": {
diff --git a/packages/bank-ui/package.json b/packages/bank-ui/package.json
index f06905a93..db89e58be 100644
--- a/packages/bank-ui/package.json
+++ b/packages/bank-ui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/bank-ui",
- "version": "0.10.7",
+ "version": "0.11.4",
"license": "AGPL-3.0-OR-LATER",
"type": "module",
"scripts": {
diff --git a/packages/bank-ui/src/hooks/preferences.ts b/packages/bank-ui/src/hooks/preferences.ts
index bb3dcb153..4cb5e6a95 100644
--- a/packages/bank-ui/src/hooks/preferences.ts
+++ b/packages/bank-ui/src/hooks/preferences.ts
@@ -31,8 +31,6 @@ interface Preferences {
showWithdrawalSuccess: boolean;
showDemoDescription: boolean;
showInstallWallet: boolean;
- maxWithdrawalAmount: number;
- fastWithdrawal: boolean;
showDebugInfo: boolean;
}
@@ -41,17 +39,13 @@ export const codecForPreferences = (): Codec<Preferences> =>
.property("showWithdrawalSuccess", codecForBoolean())
.property("showDemoDescription", codecForBoolean())
.property("showInstallWallet", codecForBoolean())
- .property("fastWithdrawal", codecForBoolean())
.property("showDebugInfo", codecForBoolean())
- .property("maxWithdrawalAmount", codecForNumber())
.build("Settings");
const defaultPreferences: Preferences = {
showWithdrawalSuccess: true,
showDemoDescription: true,
showInstallWallet: true,
- maxWithdrawalAmount: 25,
- fastWithdrawal: false,
showDebugInfo: false,
};
@@ -82,7 +76,6 @@ export function usePreferences(): [
export function getAllBooleanPreferences(): Array<keyof Preferences> {
return [
- "fastWithdrawal",
"showDebugInfo",
"showDemoDescription",
"showInstallWallet",
@@ -95,16 +88,12 @@ export function getLabelForPreferences(
i18n: ReturnType<typeof useTranslationContext>["i18n"],
): TranslatedString {
switch (k) {
- case "maxWithdrawalAmount":
- return i18n.str`Max withdrawal amount`;
case "showWithdrawalSuccess":
return i18n.str`Show withdrawal confirmation`;
case "showDemoDescription":
return i18n.str`Show demo description`;
case "showInstallWallet":
return i18n.str`Show install wallet first`;
- case "fastWithdrawal":
- return i18n.str`Use fast withdrawal form`;
case "showDebugInfo":
return i18n.str`Show debug info`;
}
diff --git a/packages/bank-ui/src/pages/OperationState/state.ts b/packages/bank-ui/src/pages/OperationState/state.ts
index 19c097d18..5544c4e23 100644
--- a/packages/bank-ui/src/pages/OperationState/state.ts
+++ b/packages/bank-ui/src/pages/OperationState/state.ts
@@ -18,6 +18,7 @@ import {
Amounts,
HttpStatusCode,
TalerCoreBankErrorsByMethod,
+ TalerCorebankApi,
TalerError,
assertUnreachable,
parsePaytoUri,
@@ -33,6 +34,7 @@ import { useSessionState } from "../../hooks/session.js";
import { useBankState } from "../../hooks/bank-state.js";
import { usePreferences } from "../../hooks/preferences.js";
import { Props, State } from "./index.js";
+import { useSettingsContext } from "../../context/settings.js";
export function useComponentState({
currency,
@@ -41,7 +43,8 @@ export function useComponentState({
routeHere,
onAuthorizationRequired,
}: Props): utils.RecursiveState<State> {
- const [settings] = usePreferences();
+ const [preference] = usePreferences();
+ const settings = useSettingsContext();
const [bankState, updateBankState] = useBankState();
const { state: credentials } = useSessionState();
const creds = credentials.status !== "loggedIn" ? undefined : credentials;
@@ -52,15 +55,22 @@ export function useComponentState({
const [failure, setFailure] = useState<
TalerCoreBankErrorsByMethod<"createWithdrawal"> | undefined
>();
- const amount = settings.maxWithdrawalAmount;
+ const amount = settings.defaultSuggestedAmount;
async function doSilentStart() {
// FIXME: if amount is not enough use balance
const parsedAmount = Amounts.parseOrThrow(`${currency}:${amount}`);
if (!creds) return;
- const resp = await bank.createWithdrawal(creds, {
- amount: Amounts.stringify(parsedAmount),
- });
+ const params: TalerCorebankApi.BankAccountCreateWithdrawalRequest =
+ settings.fastWithdrawalForm
+ ? {
+ suggested_amount: Amounts.stringify(parsedAmount),
+ }
+ : {
+ amount: Amounts.stringify(parsedAmount),
+ };
+
+ const resp = await bank.createWithdrawal(creds, params);
if (resp.type === "fail") {
setFailure(resp);
return;
@@ -73,7 +83,7 @@ export function useComponentState({
if (withdrawalOperationId === undefined) {
doSilentStart();
}
- }, [settings.fastWithdrawal, amount]);
+ }, [settings.fastWithdrawalForm, amount]);
if (failure) {
return {
@@ -174,7 +184,7 @@ export function useComponentState({
}
if (data.status === "confirmed") {
- if (!settings.showWithdrawalSuccess) {
+ if (!preference.showWithdrawalSuccess) {
updateBankState("currentWithdrawalOperationId", undefined);
// onClose()
}
diff --git a/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx
index 3bf891504..0fb8c0ac1 100644
--- a/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx
+++ b/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -79,6 +79,7 @@ export function PaytoWireTransferForm({
routeHere,
onAuthorizationRequired,
limit,
+ balance,
}: Props): VNode {
const [inputType, setInputType] = useState<"form" | "payto" | "qr">("form");
const isRawPayto = inputType !== "form";
@@ -111,6 +112,16 @@ export function PaytoWireTransferForm({
? ("x-taler-bank" as const)
: ("iban" as const);
+ const wireFee =
+ config.wire_transfer_fees === undefined
+ ? Amounts.zeroOfCurrency(config.currency)
+ : Amounts.parseOrThrow(config.wire_transfer_fees);
+
+ const limitWithFee =
+ Amounts.cmp(limit, wireFee) === 1
+ ? Amounts.sub(limit, wireFee).amount
+ : Amounts.zeroOfAmount(limit);
+
const errorsWire = undefinedIfEmpty({
account: !account
? i18n.str`Required`
@@ -124,7 +135,7 @@ export function PaytoWireTransferForm({
? i18n.str`Required`
: !parsedAmount
? i18n.str`Not valid`
- : validateAmount(parsedAmount, limit, i18n),
+ : validateAmount(parsedAmount, limitWithFee, i18n),
});
const parsed = !rawPaytoInput ? undefined : parsePaytoUri(rawPaytoInput);
@@ -134,7 +145,7 @@ export function PaytoWireTransferForm({
? i18n.str`Required`
: !parsed
? i18n.str`Does not follow the pattern`
- : validateRawPayto(parsed, limit, url.host, i18n, paytoType),
+ : validateRawPayto(parsed, limitWithFee, url.host, i18n, paytoType),
});
async function doSend() {
@@ -479,9 +490,9 @@ export function PaytoWireTransferForm({
e.preventDefault();
}}
>
- <div class="p-4 sm:p-8">
+ <div class="m-4">
{!isRawPayto ? (
- <div class="grid max-w-xs grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <div class="grid max-w-xs grid-cols-1 gap-x-6 gap-y-8 ">
{(() => {
switch (paytoType) {
case "x-taler-bank": {
@@ -622,7 +633,45 @@ export function PaytoWireTransferForm({
</div>
</div>
)}
+ {Amounts.cmp(limitWithFee, balance) > 0 ? (
+ <p class="mt-2 text-sm text-gray-900">
+ <i18n.Translate>
+ You can transfer{" "}
+ <RenderAmount
+ value={limitWithFee}
+ spec={config.currency_specification}
+ />
+ </i18n.Translate>
+ </p>
+ ) : undefined}
</div>
+ {Amounts.isZero(wireFee) ? undefined : (
+ <div class="px-4 my-4">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <div class="sm:col-span-6">
+ <dl class="mt-4 space-y-4">
+ <Fragment>
+ <div class="flex items-center justify-between ">
+ <dt class="flex items-center text-sm text-gray-600">
+ <span>
+ <i18n.Translate>Cost</i18n.Translate>
+ </span>
+ </dt>
+ <dd class="text-sm text-gray-900">
+ <RenderAmount
+ value={wireFee}
+ negative
+ withColor
+ spec={config.currency_specification}
+ />
+ </dd>
+ </div>
+ </Fragment>
+ </dl>
+ </div>
+ </div>
+ </div>
+ )}
<div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
{routeCancel ? (
<a
diff --git a/packages/bank-ui/src/pages/QrCodeSection.tsx b/packages/bank-ui/src/pages/QrCodeSection.tsx
index 359d4c18f..2a21295c7 100644
--- a/packages/bank-ui/src/pages/QrCodeSection.tsx
+++ b/packages/bank-ui/src/pages/QrCodeSection.tsx
@@ -86,10 +86,10 @@ export function QrCodeSection({
<div class="mt-4 mb-4 text-sm text-gray-500">
<p>
<i18n.Translate>
- You will see the details of the operation in your wallet
- including the fees (if applies). If you still don't have one you
- can install it following instructions in
- </i18n.Translate>{" "}
+ Your wallet will display the details of the transaction
+ including the fees (if applicable). If you do not yet have a
+ wallet, please follow the instructions on
+ </i18n.Translate>
<a
class="font-semibold text-gray-500 hover:text-gray-400"
name="wallet page"
diff --git a/packages/bank-ui/src/pages/WalletWithdrawForm.tsx b/packages/bank-ui/src/pages/WalletWithdrawForm.tsx
index a9c652643..7cf2c7881 100644
--- a/packages/bank-ui/src/pages/WalletWithdrawForm.tsx
+++ b/packages/bank-ui/src/pages/WalletWithdrawForm.tsx
@@ -19,6 +19,7 @@ import {
AmountJson,
Amounts,
HttpStatusCode,
+ TalerCorebankApi,
TranslatedString,
assertUnreachable,
parseWithdrawUri,
@@ -45,6 +46,7 @@ import {
RenderAmount,
doAutoFocus,
} from "./PaytoWireTransferForm.js";
+import { useSettingsContext } from "../context/settings.js";
const RefAmount = forwardRef(InputAmount);
@@ -64,7 +66,7 @@ function OldWithdrawalForm({
routeCancel: RouteDefinition;
}): VNode {
const { i18n } = useTranslationContext();
- const [settings] = usePreferences();
+ const settings = useSettingsContext();
// const walletInegrationApi = useTalerWalletIntegrationAPI()
// const { navigateTo } = useNavigationContext();
@@ -79,7 +81,7 @@ function OldWithdrawalForm({
const creds = credentials.status !== "loggedIn" ? undefined : credentials;
const [amountStr, setAmountStr] = useState<string | undefined>(
- `${settings.maxWithdrawalAmount}`,
+ `${settings.defaultSuggestedAmount ?? 1}`,
);
const [notification, notify, handleError] = useLocalNotification();
@@ -141,9 +143,15 @@ function OldWithdrawalForm({
async function doStart() {
if (!parsedAmount || !creds) return;
await handleError(async () => {
- const resp = await api.createWithdrawal(creds, {
- amount: Amounts.stringify(parsedAmount),
- });
+ const params: TalerCorebankApi.BankAccountCreateWithdrawalRequest =
+ settings.fastWithdrawalForm
+ ? {
+ suggested_amount: Amounts.stringify(parsedAmount),
+ }
+ : {
+ amount: Amounts.stringify(parsedAmount),
+ };
+ const resp = await api.createWithdrawal(creds, params);
if (resp.type === "ok") {
const uri = parseWithdrawUri(resp.body.taler_withdraw_uri);
if (!uri) {
@@ -234,9 +242,9 @@ function OldWithdrawalForm({
</i18n.Translate>
</p>
{Amounts.cmp(limit, balance) > 0 ? (
- <p class="mt-2 text-sm text-gray-500">
+ <p class="mt-2 text-sm text-gray-900">
<i18n.Translate>
- Your account allows you to withdraw{" "}
+ You can withdraw{" "}
<RenderAmount
value={limit}
spec={config.currency_specification}
@@ -340,7 +348,8 @@ export function WalletWithdrawForm({
routeCancel: RouteDefinition;
}): VNode {
const { i18n } = useTranslationContext();
- const [settings, updateSettings] = usePreferences();
+ const [pref, updatePref] = usePreferences();
+ const settings = useSettingsContext();
return (
<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-6 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
@@ -357,11 +366,11 @@ export function WalletWithdrawForm({
</div>
<div class="col-span-2">
- {settings.showInstallWallet && (
+ {pref.showInstallWallet && (
<Attention
title={i18n.str`You need a Taler wallet`}
onClose={() => {
- updateSettings("showInstallWallet", false);
+ updatePref("showInstallWallet", false);
}}
>
<i18n.Translate>
@@ -379,7 +388,7 @@ export function WalletWithdrawForm({
</Attention>
)}
- {!settings.fastWithdrawal ? (
+ {!settings.fastWithdrawalForm ? (
<OldWithdrawalForm
focus={focus}
routeOperationDetails={routeOperationDetails}
diff --git a/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
index 853dd7bae..b270c447a 100644
--- a/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
+++ b/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
@@ -17,6 +17,7 @@
import {
AbsoluteTime,
AmountJson,
+ Amounts,
HttpStatusCode,
PaytoUri,
PaytoUriIBAN,
@@ -79,6 +80,11 @@ export function WithdrawalConfirmationQuestion({
lib: { bank: api },
} = useBankCoreApiContext();
+ const wireFee =
+ config.wire_transfer_fees === undefined
+ ? Amounts.zeroOfCurrency(config.currency)
+ : Amounts.parseOrThrow(config.wire_transfer_fees);
+
async function doTransfer() {
await handleError(async () => {
if (!creds) return;
@@ -357,6 +363,23 @@ export function WithdrawalConfirmationQuestion({
/>
</dd>
</div>
+ {Amounts.isZero(wireFee) ? undefined : (
+ <Fragment>
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>Cost</i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ <RenderAmount
+ value={wireFee}
+ negative
+ withColor
+ spec={config.currency_specification}
+ />
+ </dd>
+ </div>
+ </Fragment>
+ )}
</dl>
</div>
</div>
diff --git a/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx b/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx
index 6db0e5512..0e2144d77 100644
--- a/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx
+++ b/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx
@@ -15,6 +15,7 @@
*/
import {
AbsoluteTime,
+ AccountLetter,
HttpStatusCode,
TalerCorebankApi,
TalerError,
@@ -200,28 +201,17 @@ export function ShowAccountDetails({
}
const url = bank.getRevenueAPI(account);
- url.username = account;
const baseURL = url.href;
-
+ const revenueURL = new URL(baseURL)
+ revenueURL.username = account;
+ revenueURL.password = creds?.token ?? ""
const ac = parsePaytoUri(result.body.payto_uri);
const payto = !ac?.isKnown ? undefined : ac;
- let accountLetter: string | undefined = undefined;
- if (payto) {
- switch (payto.targetType) {
- case "iban": {
- accountLetter = `account-info-url=${url.href}\naccount-type=${payto.targetType}\niban=${payto.iban}\nreceiver-name=${result.body.name}\n`;
- break;
- }
- case "x-taler-bank": {
- accountLetter = `account-info-url=${url.href}\naccount-type=${payto.targetType}\naccount=${payto.account}\nhost=${payto.host}\nreceiver-name=${result.body.name}\n`;
- break;
- }
- case "bitcoin": {
- accountLetter = `account-info-url=${url.href}\naccount-type=${payto.targetType}\naddress=${payto.address}\nreceiver-name=${result.body.name}\n`;
- break;
- }
+ const accountLetter : AccountLetter | undefined = !payto
+ ? undefined
+ : {
+ accountURI: result.body.payto_uri, infoURL: revenueURL.href
}
- }
return (
<Fragment>
@@ -327,7 +317,7 @@ export function ShowAccountDetails({
name="account-type"
id="account-type"
disabled={true}
- value={account}
+ value={payto.targetType}
autocomplete="off"
/>
</div>
@@ -372,16 +362,16 @@ export function ShowAccountDetails({
<div class="sm:col-span-5">
<label
class="block text-sm font-medium leading-6 text-gray-900"
- for="iban"
+ for="account-name"
>
- {i18n.str`IBAN`}
+ {i18n.str`Account name`}
</label>
<div class="mt-2">
<input
type="text"
class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- name="iban"
- id="iban"
+ name="account-name"
+ id="account-name"
disabled={true}
value={payto.account}
autocomplete="off"
@@ -389,7 +379,7 @@ export function ShowAccountDetails({
</div>
<p class="mt-2 text-sm text-gray-500">
<i18n.Translate>
- International Bank Account Number.
+ Bank account identifier for wire transfers.
</i18n.Translate>
</p>
</div>
@@ -486,7 +476,7 @@ export function ShowAccountDetails({
<i18n.Translate>Cancel</i18n.Translate>
</a>
<CopyButton
- getContent={() => accountLetter ?? ""}
+ getContent={() => !accountLetter ? "" : JSON.stringify(accountLetter)}
class="flex text-center disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
>
<i18n.Translate>Copy</i18n.Translate>
@@ -498,3 +488,4 @@ export function ShowAccountDetails({
</Fragment>
);
}
+
diff --git a/packages/bank-ui/src/settings.json b/packages/bank-ui/src/settings.json
index df5fe75ce..f14168e77 100644
--- a/packages/bank-ui/src/settings.json
+++ b/packages/bank-ui/src/settings.json
@@ -2,6 +2,8 @@
"backendBaseURL": "http://bank.taler.test:1180/",
"simplePasswordForRandomAccounts": true,
"allowRandomAccountCreation": true,
+ "fastWithdrawalForm": true,
+ "defaultSuggestedAmount": 11,
"bankName": "Taler DEVELOPMENT Bank",
"topNavSites": {
"Exchange": "http://Exchnage.taler.test:1180/",
diff --git a/packages/bank-ui/src/settings.ts b/packages/bank-ui/src/settings.ts
index c085c7cd8..6d8f7b850 100644
--- a/packages/bank-ui/src/settings.ts
+++ b/packages/bank-ui/src/settings.ts
@@ -20,6 +20,7 @@ import {
canonicalizeBaseUrl,
codecForBoolean,
codecForMap,
+ codecForNumber,
codecForString,
codecOptional,
} from "@gnu-taler/taler-util";
@@ -45,6 +46,17 @@ export interface UiSettings {
// - value: link target, where the user is going to be redirected
// default: empty list
topNavSites?: Record<string, string>;
+ // Use the withdrawal form which redirect the user to the wallet
+ // without asking the amount to the user.
+ // - true: on withdrawal creation the spa will use suggested_amount instead
+ // of fixed amount
+ // - false: on withdrawal creation the spa will use fixed amount
+ // default: false
+ fastWithdrawalForm?: boolean;
+ // When the withdrawal form use the suggested amount the bank
+ // will send a default value that the user can change.
+ // default: 10
+ defaultSuggestedAmount?: number;
}
/**
@@ -56,12 +68,16 @@ const defaultSettings: UiSettings = {
simplePasswordForRandomAccounts: false,
allowRandomAccountCreation: false,
topNavSites: {},
+ fastWithdrawalForm: false,
+ defaultSuggestedAmount: 10,
};
const codecForUISettings = (): Codec<UiSettings> =>
buildCodecForObject<UiSettings>()
.property("backendBaseURL", codecOptional(codecForString()))
.property("allowRandomAccountCreation", codecOptional(codecForBoolean()))
+ .property("fastWithdrawalForm", codecOptional(codecForBoolean()))
+ .property("defaultSuggestedAmount", codecOptional(codecForNumber()))
.property(
"simplePasswordForRandomAccounts",
codecOptional(codecForBoolean()),
diff --git a/packages/challenger-ui/package.json b/packages/challenger-ui/package.json
index 8234e2385..7cc73771b 100644
--- a/packages/challenger-ui/package.json
+++ b/packages/challenger-ui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/challenger-ui",
- "version": "0.10.7",
+ "version": "0.11.4",
"author": "sebasjm",
"license": "AGPL-3.0-OR-LATER",
"description": "UI for GNU Challenger.",
diff --git a/packages/idb-bridge/package.json b/packages/idb-bridge/package.json
index 376265c0f..ce3123619 100644
--- a/packages/idb-bridge/package.json
+++ b/packages/idb-bridge/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/idb-bridge",
- "version": "0.10.7",
+ "version": "0.11.4",
"description": "IndexedDB implementation that uses SQLite3 as storage",
"main": "./dist/idb-bridge.js",
"module": "./lib/index.js",
@@ -38,6 +38,6 @@
"failFast": true
},
"optionalDependencies": {
- "better-sqlite3": "9.4.0"
+ "better-sqlite3": "10.0.0"
}
}
diff --git a/packages/merchant-backend-ui/package.json b/packages/merchant-backend-ui/package.json
index bd16317f5..bc8627312 100644
--- a/packages/merchant-backend-ui/package.json
+++ b/packages/merchant-backend-ui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/merchant-backend-ui",
- "version": "0.10.7",
+ "version": "0.11.4",
"license": "AGPL-3.0-or-later",
"scripts": {
"compile": "tsc && ./build.mjs",
diff --git a/packages/merchant-backoffice-ui/package.json b/packages/merchant-backoffice-ui/package.json
index e80604777..8aabdce87 100644
--- a/packages/merchant-backoffice-ui/package.json
+++ b/packages/merchant-backoffice-ui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/merchant-backoffice-ui",
- "version": "0.10.7",
+ "version": "0.11.4",
"license": "AGPL-3.0-or-later",
"type": "module",
"scripts": {
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
index a0c15c77c..4ac798afe 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
@@ -18,13 +18,10 @@
*
* @author Sebastian Javier Marchano (sebasjm)
*/
-import {
- parsePaytoUri,
- PaytoUriGeneric,
- stringifyPaytoUri,
-} from "@gnu-taler/taler-util";
+import { parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
+import { useEffect, useState } from "preact/hooks";
import { COUNTRY_TABLE } from "../../utils/constants.js";
import { undefinedIfEmpty } from "../../utils/table.js";
import { FormErrors, FormProvider } from "./FormProvider.js";
@@ -32,7 +29,6 @@ import { Input } from "./Input.js";
import { InputGroup } from "./InputGroup.js";
import { InputSelector } from "./InputSelector.js";
import { InputProps, useField } from "./useField.js";
-import { useEffect, useState } from "preact/hooks";
export interface Props<T> extends InputProps<T> {
isValid?: (e: any) => boolean;
@@ -108,13 +104,13 @@ function validateEthereum_path1(
* bank.com/path
* bank.com/path/subpath/
*/
-const DOMAIN_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9](?:\.[a-zA-Z]{2,})+(\/[a-zA-Z0-9-.]+)*\/?$/
+const DOMAIN_REGEX =
+ /^[a-zA-Z0-9][a-zA-Z0-9-_]{1,61}[a-zA-Z0-9-_](?:\.[a-zA-Z0-9-_]{2,})+(:[0-9]+)?(\/[a-zA-Z0-9-.]+)*\/?$/;
function validateTalerBank_path1(
addr: string,
i18n: ReturnType<typeof useTranslationContext>["i18n"],
): string | undefined {
- console.log(addr, DOMAIN_REGEX.test(addr))
try {
const valid = DOMAIN_REGEX.test(addr);
if (valid) return undefined;
@@ -206,6 +202,7 @@ export function InputPaytoForm<T>({
const { value: initialValueStr, onChange } = useField<T>(name);
const initialPayto = parsePaytoUri(initialValueStr ?? "");
+
const paths = !initialPayto ? [] : initialPayto.targetPath.split("/");
const initialPath1 = paths.length >= 1 ? paths[0] : undefined;
const initialPath2 = paths.length >= 2 ? paths[1] : undefined;
@@ -219,6 +216,22 @@ export function InputPaytoForm<T>({
path2: initialPath2,
};
const [value, setValue] = useState<Partial<Entity>>(initial);
+ useEffect(() => {
+ const nv = parsePaytoUri(initialValueStr ?? "");
+ const paths = !initialPayto ? [] : initialPayto.targetPath.split("/");
+ if (nv !== undefined && nv.isKnown) {
+ if (nv.targetType === "iban" && paths.length >= 2) {
+ //FIXME: workaround EBIC not supported
+ paths[0] = paths[1]
+ }
+ setValue({
+ target: nv.targetType,
+ params: nv.params,
+ path1: paths.length >= 1 ? paths[0] : undefined,
+ path2: paths.length >= 2 ? paths[1] : undefined,
+ });
+ }
+ }, [initialValueStr]);
const { i18n } = useTranslationContext();
@@ -252,7 +265,8 @@ export function InputPaytoForm<T>({
(k) => (errors as any)[k] !== undefined,
);
- const path1WithSlash = value.path1 && !value.path1.endsWith("/") ? value.path1 + "/" : value.path1
+ const path1WithSlash =
+ value.path1 && !value.path1.endsWith("/") ? value.path1 + "/" : value.path1;
const str =
hasErrors || !value.target
? undefined
@@ -268,37 +282,6 @@ export function InputPaytoForm<T>({
onChange(str as any);
}, [str]);
- // const submit = useCallback((): void => {
- // // const accounts: TalerMerchantApi.AccountAddDetails[] = paytos;
- // // const alreadyExists =
- // // accounts.findIndex((x) => x.payto_uri === paytoURL) !== -1;
- // // if (!alreadyExists) {
- // const newValue: TalerMerchantApi.AccountAddDetails = {
- // payto_uri: paytoURL,
- // };
- // if (value.auth) {
- // if (value.auth.url) {
- // newValue.credit_facade_url = value.auth.url;
- // }
- // if (value.auth.type === "none") {
- // newValue.credit_facade_credentials = {
- // type: "none",
- // };
- // }
- // if (value.auth.type === "basic") {
- // newValue.credit_facade_credentials = {
- // type: "basic",
- // username: value.auth.username ?? "",
- // password: value.auth.password ?? "",
- // };
- // }
- // }
- // onChange(newValue as any);
- // // }
- // // valueHandler(defaultTarget);
- // }, [value]);
-
- //FIXME: translating plural singular
return (
<InputGroup name="payto" label={label} fixed tooltip={tooltip}>
<FormProvider<Entity>
@@ -413,11 +396,17 @@ export function InputPaytoForm<T>({
return v;
}}
tooltip={i18n.str`Bank host.`}
- help={<Fragment>
- <div><i18n.Translate>Without scheme and may include subpath:</i18n.Translate></div>
- <div>bank.com/</div>
- <div>bank.com/path/subpath/</div>
- </Fragment>}
+ help={
+ <Fragment>
+ <div>
+ <i18n.Translate>
+ Without scheme and may include subpath:
+ </i18n.Translate>
+ </div>
+ <div>bank.com/</div>
+ <div>bank.com/path/subpath/</div>
+ </Fragment>
+ }
/>
<Input<Entity>
name="path2"
diff --git a/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx b/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
index 864d09f48..efcca302f 100644
--- a/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
+++ b/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
@@ -60,22 +60,6 @@ export function DefaultInstanceFormFields({
tooltip={i18n.str`Legal name of the business represented by this instance.`}
/>
- <TextField name="asdasd" label="">
- <i18n.Translate>
- Choose individual if you don't have or are not required to have legal business permission.
- </i18n.Translate>
- </TextField>
-
- <InputSelector<Entity>
- name="user_type"
- label={i18n.str`Selling as`}
- tooltip={i18n.str`Different type of account can have different rules and requirements.`}
- values={["business", "individual"]}
- toStr={(d: string) => {
- return d.toUpperCase();
- }}
- />
-
<Input<Entity>
name="email"
label={i18n.str`Email`}
diff --git a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
index dbe21e0e9..aeb49e81d 100644
--- a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
+++ b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
@@ -111,7 +111,7 @@ export function Sidebar({ mobile }: Props): VNode {
<li>
<a href={"/templates"} class="has-icon">
<span class="icon">
- <i class="mdi mdi-newspaper" />
+ <i class="mdi mdi-qrcode" />
</span>
<span class="menu-item-label">
<i18n.Translate>Templates</i18n.Translate>
@@ -166,7 +166,7 @@ export function Sidebar({ mobile }: Props): VNode {
<li>
<a href={"/webhooks"} class="has-icon">
<span class="icon">
- <i class="mdi mdi-newspaper" />
+ <i class="mdi mdi-webhook" />
</span>
<span class="menu-item-label">
<i18n.Translate>Webhooks</i18n.Translate>
diff --git a/packages/merchant-backoffice-ui/src/components/modal/index.tsx b/packages/merchant-backoffice-ui/src/components/modal/index.tsx
index 1335d0f77..43062d13e 100644
--- a/packages/merchant-backoffice-ui/src/components/modal/index.tsx
+++ b/packages/merchant-backoffice-ui/src/components/modal/index.tsx
@@ -24,9 +24,14 @@ import { ComponentChildren, Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { DEFAULT_REQUEST_TIMEOUT } from "../../utils/constants.js";
import { Spinner } from "../exception/loading.js";
-import { FormProvider } from "../form/FormProvider.js";
+import { FormErrors, FormProvider } from "../form/FormProvider.js";
import { Input } from "../form/Input.js";
import { useSessionContext } from "../../context/session.js";
+import {
+ AccountLetter,
+ codecForAccountLetter,
+ PaytoString,
+} from "@gnu-taler/taler-util";
interface Props {
active?: boolean;
@@ -201,6 +206,88 @@ export function ClearConfirmModal({
);
}
+interface ImportingAccountModalProps {
+ onCancel: () => void;
+ onConfirm: (account: AccountLetter) => void;
+}
+
+export function ImportingAccountModal({
+ onCancel,
+ onConfirm,
+}: ImportingAccountModalProps): VNode {
+ const { i18n } = useTranslationContext();
+ const [letter, setLetter] = useState<string>();
+ let parsed = undefined;
+ try {
+ parsed = JSON.parse(letter ?? "");
+ } catch (e) {
+ parsed = undefined;
+ }
+ let account: AccountLetter | undefined = undefined;
+ let parsingError: string | undefined = undefined;
+ try {
+ account =
+ parsed !== undefined ? codecForAccountLetter().decode(parsed) : undefined;
+ } catch (e) {
+ account = undefined;
+ if (e instanceof Error) {
+ parsingError = e.message;
+ }
+ }
+ const errors: FormErrors<{ letter: string }> = {
+ letter: !letter
+ ? i18n.str`required`
+ : parsed === undefined
+ ? i18n.str`letter should be a JSON string`
+ : account === undefined
+ ? i18n.str`JSON string is invalid`
+ : undefined,
+ };
+ return (
+ <ConfirmModal
+ label={i18n.str`Import`}
+ description={i18n.str`Importing an account from the bank`}
+ active
+ onCancel={onCancel}
+ disabled={account === undefined}
+ onConfirm={() => onConfirm(account!)}
+ >
+ <p>
+ <i18n.Translate>
+ You can export your account settings from the Libeufin Bank's account
+ profile. Paste the content in the next field.
+ </i18n.Translate>
+ </p>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <i18n.Translate>Account information</i18n.Translate>
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input
+ class="input"
+ value={letter ?? ""}
+ onChange={(e) => {
+ setLetter(e.currentTarget.value);
+ }}
+ />
+ </p>
+ {letter !== undefined && errors.letter && (
+ <p class="help is-danger">{errors.letter}</p>
+ )}
+ {parsingError !== undefined && (
+ <p class="help is-danger">{parsingError}</p>
+ )}
+ </div>
+ </div>
+ </div>
+ </ConfirmModal>
+ );
+}
+
interface DeleteModalProps {
element: { id: string; name: string };
onCancel: () => void;
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx
index d05375b6c..d0e7a83cd 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx
@@ -31,6 +31,7 @@ import {
import { Input } from "../../../../components/form/Input.js";
import { InputPaytoForm } from "../../../../components/form/InputPaytoForm.js";
import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { ImportingAccountModal } from "../../../../components/modal/index.js";
import { undefinedIfEmpty } from "../../../../utils/table.js";
import { safeConvertURL } from "../update/UpdatePage.js";
@@ -46,6 +47,7 @@ const accountAuthType = ["none", "basic"];
export function CreatePage({ onCreate, onBack }: Props): VNode {
const { i18n } = useTranslationContext();
+ const [importing, setImporting] = useState(false);
const [state, setState] = useState<Partial<Entity>>({});
const facadeURL = safeConvertURL(state.credit_facade_url);
const errors: FormErrors<Entity> = {
@@ -115,9 +117,25 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
credit_facade_url,
});
};
-
return (
<div>
+ {importing && <ImportingAccountModal onCancel={()=> {setImporting(false)}} onConfirm={(ac) => {
+ state.payto_uri = ac.accountURI
+ const u = new URL(ac.infoURL)
+ u.password = ""
+ if (u.username || u.password) {
+ state.credit_facade_credentials = {
+ type: "basic",
+ password: u.password,
+ username: u.username,
+ }
+ state.repeatPassword = u.password
+ }
+ u.password = ""
+ u.username = ""
+ state.credit_facade_url = u.href;
+ setImporting(false)
+ }} />}
<section class="section is-main-section">
<div class="columns">
<div class="column" />
@@ -171,6 +189,16 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
</FormProvider>
<div class="buttons is-right mt-5">
+ <button
+ class="button is-info"
+ data-tooltip={i18n.str`Need to complete marked fields`}
+ onClick={() => {
+ setImporting(true)
+ }}
+ >
+ <i18n.Translate>Import from bank</i18n.Translate>
+ </button>
+
{onBack && (
<button class="button" onClick={onBack}>
<i18n.Translate>Cancel</i18n.Translate>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx
index 9bab33f6f..aa1481a2e 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx
@@ -24,6 +24,7 @@ import {
HttpStatusCode,
OperationFail,
OperationOk,
+ PaytoString,
TalerError,
TalerMerchantApi,
TalerRevenueHttpClient,
@@ -67,51 +68,55 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode {
const resp = await testRevenueAPI(
revenueAPI,
request.credit_facade_credentials,
+ request.payto_uri,
);
+ if (resp instanceof TalerError) {
+ setNotif({
+ message: i18n.str`Could not add bank account`,
+ type: "ERROR",
+ description: i18n.str`The request to check the revenue API failed.`,
+ details: JSON.stringify(resp.errorDetail, undefined, 2),
+ });
+ return;
+ }
if (resp.type === "fail") {
switch (resp.case) {
- case TestRevenueErrorType.NO_CONFIG: {
- setNotif({
- message: i18n.str`Could not create account`,
- type: "ERROR",
- description: i18n.str`The endpoint doesn't seems to be a Taler Revenue API`,
- });
- return;
- }
- case TestRevenueErrorType.CLIENT_BAD_REQUEST: {
+ case HttpStatusCode.BadRequest: {
setNotif({
- message: i18n.str`Could not create account`,
+ message: i18n.str`Could not add bank account`,
type: "ERROR",
description: i18n.str`Server replied with "bad request".`,
});
return;
+
}
- case TestRevenueErrorType.UNAUTHORIZED: {
+ case HttpStatusCode.Unauthorized: {
setNotif({
- message: i18n.str`Could not create account`,
+ message: i18n.str`Could not add bank account`,
type: "ERROR",
description: i18n.str`Unauthorized, try with another credentials.`,
});
return;
+
}
- case TestRevenueErrorType.NOT_FOUND: {
+ case HttpStatusCode.NotFound: {
setNotif({
- message: i18n.str`Could not create account`,
+ message: i18n.str`Could not add bank account`,
type: "ERROR",
- description: i18n.str`Check facade URL, server replied with "not found".`,
+ description: i18n.str`The endpoint doesn't seems to be a Taler Revenue API`,
});
return;
}
- case TestRevenueErrorType.GENERIC_ERROR: {
+ case TestRevenueErrorType.ANOTHER_ACCOUNT: {
setNotif({
- message: i18n.str`Could not create account`,
+ message: i18n.str`Could not add bank account`,
type: "ERROR",
- description: resp.detail.hint,
+ description: i18n.str`The account info URL returned information from an account which is not the same in the account form: ${resp.detail.hint}`,
});
return;
}
default: {
- assertUnreachable(resp.case);
+ assertUnreachable(resp);
}
}
}
@@ -136,17 +141,18 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode {
}
export enum TestRevenueErrorType {
- NO_CONFIG,
- CLIENT_BAD_REQUEST,
- UNAUTHORIZED,
- NOT_FOUND,
- GENERIC_ERROR,
+ ANOTHER_ACCOUNT,
}
export async function testRevenueAPI(
revenueAPI: URL,
creds: FacadeCredentials | undefined,
-): Promise<OperationOk<void> | OperationFail<TestRevenueErrorType>> {
+ account: PaytoString,
+): Promise<OperationOk<void> | OperationFail<HttpStatusCode.NotFound>
+| OperationFail<HttpStatusCode.Unauthorized>
+| OperationFail<HttpStatusCode.BadRequest>
+| OperationFail<TestRevenueErrorType.ANOTHER_ACCOUNT>
+| TalerError> {
const api = new TalerRevenueHttpClient(
revenueAPI.href,
new BrowserFetchHttpLib(),
@@ -167,69 +173,33 @@ export async function testRevenueAPI(
const config = await api.getConfig(auth);
if (config.type === "fail") {
- switch (config.case) {
- case HttpStatusCode.Unauthorized: {
- return {
- type: "fail",
- case: TestRevenueErrorType.UNAUTHORIZED,
- detail: {
- code: 1,
- },
- };
- }
- case HttpStatusCode.NotFound: {
- return {
- type: "fail",
- case: TestRevenueErrorType.NO_CONFIG,
- detail: {
- code: 1,
- },
- };
- }
- }
+ return config;
}
const history = await api.getHistory(auth);
if (history.type === "fail") {
- switch (history.case) {
- case HttpStatusCode.BadRequest: {
- return {
- type: "fail",
- case: TestRevenueErrorType.CLIENT_BAD_REQUEST,
- detail: {
- code: 1,
- },
- };
- }
- case HttpStatusCode.Unauthorized: {
- return {
- type: "fail",
- case: TestRevenueErrorType.UNAUTHORIZED,
- detail: {
- code: 1,
- },
- };
- }
- case HttpStatusCode.NotFound: {
- return {
- type: "fail",
- case: TestRevenueErrorType.NOT_FOUND,
- detail: {
- code: 1,
- },
- };
- }
- }
+ return history;
}
- } catch (err) {
- if (err instanceof TalerError) {
+ if (history.body.credit_account !== account) {
return {
type: "fail",
- case: TestRevenueErrorType.GENERIC_ERROR,
- detail: err.errorDetail,
+ case: TestRevenueErrorType.ANOTHER_ACCOUNT,
+ detail: {
+ code: 1,
+ hint: history.body.credit_account
+ },
};
}
+ } catch (err) {
+ if (err instanceof TalerError) {
+ return err;
+ // return {
+ // type: "fail",
+ // case: TestRevenueErrorType.GENERIC_ERROR,
+ // detail: err.errorDetail,
+ // };
+ }
}
return opFixedSuccess(undefined);
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx
index efe484402..a9cb2805b 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx
@@ -48,7 +48,7 @@ export function CardTable({
<header class="card-header">
<p class="card-header-title">
<span class="icon">
- <i class="mdi mdi-newspaper" />
+ <i class="mdi mdi-bank" />
</span>
<i18n.Translate>Bank accounts</i18n.Translate>
</p>
@@ -240,9 +240,6 @@ function Table({
<th>
<i18n.Translate>IBAN</i18n.Translate>
</th>
- <th>
- <i18n.Translate>BIC</i18n.Translate>
- </th>
<th />
</tr>
</thead>
@@ -263,12 +260,6 @@ function Table({
>
{ac.iban}
</td>
- <td
- onClick={(): void => onSelect(acc)}
- style={{ cursor: "pointer" }}
- >
- {ac.bic ?? ""}
- </td>
<td class="is-actions-cell right-sticky">
<div class="buttons is-right">
<button
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx
index 70942fd55..9116aaa62 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx
@@ -88,51 +88,55 @@ export default function UpdateValidator({
const resp = await testRevenueAPI(
revenueAPI,
request.credit_facade_credentials,
+ result.body.payto_uri,
);
+ if (resp instanceof TalerError) {
+ setNotif({
+ message: i18n.str`Could not create account`,
+ type: "ERROR",
+ description: i18n.str`The request to check the revenue API failed.`,
+ details: JSON.stringify(resp.errorDetail, undefined, 2),
+ });
+ return;
+ }
if (resp.type === "fail") {
switch (resp.case) {
- case TestRevenueErrorType.NO_CONFIG: {
- setNotif({
- message: i18n.str`Could not create account`,
- type: "ERROR",
- description: i18n.str`The endpoint doesn't seems to be a Taler Revenue API`,
- });
- return;
- }
- case TestRevenueErrorType.CLIENT_BAD_REQUEST: {
+ case HttpStatusCode.BadRequest: {
setNotif({
message: i18n.str`Could not create account`,
type: "ERROR",
description: i18n.str`Server replied with "bad request".`,
});
return;
+
}
- case TestRevenueErrorType.UNAUTHORIZED: {
+ case HttpStatusCode.Unauthorized: {
setNotif({
message: i18n.str`Could not create account`,
type: "ERROR",
description: i18n.str`Unauthorized, try with another credentials.`,
});
return;
+
}
- case TestRevenueErrorType.NOT_FOUND: {
+ case HttpStatusCode.NotFound: {
setNotif({
message: i18n.str`Could not create account`,
type: "ERROR",
- description: i18n.str`Check facade URL, server replied with "not found".`,
+ description: i18n.str`The endpoint doesn't seems to be a Taler Revenue API`,
});
return;
}
- case TestRevenueErrorType.GENERIC_ERROR: {
+ case TestRevenueErrorType.ANOTHER_ACCOUNT: {
setNotif({
- message: i18n.str`Could not create account`,
+ message: i18n.str`Could not add bank account`,
type: "ERROR",
- description: resp.detail.hint,
+ description: i18n.str`The account info URL returned information from an account which is not the same in the account form: ${resp.detail.hint}`,
});
return;
}
default: {
- assertUnreachable(resp.case)
+ assertUnreachable(resp);
}
}
}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx
index d5522c2d4..a16817bab 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx
@@ -101,7 +101,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
/>
<Input<Entity>
name="otp_device_description"
- label={i18n.str`Descripiton`}
+ label={i18n.str`Description`}
tooltip={i18n.str`Useful to identify the device physically`}
/>
<InputSelector<Entity>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx
index afe3c98e2..e4206ff7d 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx
@@ -52,7 +52,7 @@ export function CardTable({
<header class="card-header">
<p class="card-header-title">
<span class="icon">
- <i class="mdi mdi-newspaper" />
+ <i class="mdi mdi-lock" />
</span>
<i18n.Translate>OTP Devices</i18n.Translate>
</p>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx
index 50262be17..336a336ed 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx
@@ -145,7 +145,6 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
template_id: state.id!,
template_description: state.description!,
template_contract,
- required_currency: contract_amount !== undefined ? undefined : config.currency,
editable_defaults: {
amount: !state.amount_editable ? undefined : (state.amount ?? zero),
summary: !state.summary_editable ? undefined : (state.summary ?? ""),
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx
index 082e622e3..4c55bae2a 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx
@@ -56,7 +56,7 @@ export function CardTable({
<header class="card-header">
<p class="card-header-title">
<span class="icon">
- <i class="mdi mdi-newspaper" />
+ <i class="mdi mdi-qrcode" />
</span>
<i18n.Translate>Templates</i18n.Translate>
</p>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx
index 32c5637aa..d284fda67 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx
@@ -161,7 +161,6 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
return onUpdate({
template_description: state.description!,
template_contract,
- required_currency: contract_amount !== undefined ? undefined : config.currency,
editable_defaults: {
amount: !state.amount_editable ? undefined : (state.amount ?? zero),
summary: !state.summary_editable ? undefined : (state.summary ?? ""),
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx
index 919285e78..877bd30e5 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx
@@ -52,7 +52,7 @@ export function CardTable({
<header class="card-header">
<p class="card-header-title">
<span class="icon">
- <i class="mdi mdi-newspaper" />
+ <i class="mdi mdi-webhook" />
</span>
<i18n.Translate>Webhooks</i18n.Translate>
</p>
diff --git a/packages/pogen/package.json b/packages/pogen/package.json
index 24edc348b..81d66125f 100644
--- a/packages/pogen/package.json
+++ b/packages/pogen/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/pogen",
- "version": "0.10.7",
+ "version": "0.11.4",
"bin": {
"pogen": "bin/pogen"
},
diff --git a/packages/taler-harness/debian/changelog b/packages/taler-harness/debian/changelog
index 269c6b99d..a891cc7ba 100644
--- a/packages/taler-harness/debian/changelog
+++ b/packages/taler-harness/debian/changelog
@@ -1,3 +1,27 @@
+taler-harness (0.11.4) unstable; urgency=low
+
+ * Release 0.11.4
+
+ -- Florian Dold <dold@taler.net> Mon, 10 Jun 2024 19:57:55 +0200
+
+taler-harness (0.11.3) unstable; urgency=low
+
+ * Release 0.11.3
+
+ -- Florian Dold <dold@taler.net> Fri, 07 Jun 2024 19:12:44 +0200
+
+taler-harness (0.11.2) unstable; urgency=low
+
+ * Release 0.11.2
+
+ -- Florian Dold <dold@taler.net> Wed, 05 Jun 2024 20:17:56 +0200
+
+taler-harness (0.11.1) unstable; urgency=low
+
+ * Release 0.11.1
+
+ -- Florian Dold <dold@taler.net> Mon, 27 May 2024 14:46:35 -0600
+
taler-harness (0.10.7) unstable; urgency=low
* Release 0.10.7
diff --git a/packages/taler-harness/package.json b/packages/taler-harness/package.json
index 38d640f51..bca870c8b 100644
--- a/packages/taler-harness/package.json
+++ b/packages/taler-harness/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-harness",
- "version": "0.10.7",
+ "version": "0.11.4",
"description": "",
"engines": {
"node": ">=0.12.0"
diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts
index 136ec3d15..4fc462ddf 100644
--- a/packages/taler-harness/src/harness/harness.ts
+++ b/packages/taler-harness/src/harness/harness.ts
@@ -274,6 +274,7 @@ export class GlobalTestState {
procs: ProcessWrapper[];
servers: http.Server[];
inShutdown: boolean = false;
+ stepSet: Set<string> = new Set();
constructor(params: GlobalTestParams) {
this.testDir = params.testDir;
this.procs = [];
@@ -423,6 +424,9 @@ export class GlobalTestState {
// Now we just log, later we may report the steps that were done
// to easily see where the test hangs.
console.info(`STEP: ${stepName}`);
+ if (this.stepSet.has(stepName)) {
+ throw Error(`duplicate step (${stepName})`);
+ }
}
}
diff --git a/packages/taler-harness/src/harness/helpers.ts b/packages/taler-harness/src/harness/helpers.ts
index 4e3ce66b9..d194b0d36 100644
--- a/packages/taler-harness/src/harness/helpers.ts
+++ b/packages/taler-harness/src/harness/helpers.ts
@@ -116,6 +116,8 @@ export interface EnvOptions {
mixedAgeRestriction?: boolean;
+ skipWireFeeCreation?: boolean;
+
additionalExchangeConfig?(e: ExchangeService): void;
additionalMerchantConfig?(m: MerchantService): void;
additionalBankConfig?(b: BankService): void;
@@ -466,11 +468,12 @@ export async function createSimpleTestkudosEnvironmentV3(
bank.corebankApiBaseUrl,
).href;
- const exchangeBankAccount = {
+ const exchangeBankAccount: HarnessExchangeBankAccount = {
wireGatewayApiBaseUrl,
accountName: exchangeBankUsername,
accountPassword: exchangeBankPassword,
accountPaytoUri: exchangePaytoUri,
+ skipWireFeeCreation: opts.skipWireFeeCreation === true,
};
await exchange.addBankAccount("1", exchangeBankAccount);
diff --git a/packages/taler-harness/src/integrationtests/test-currency-scope.ts b/packages/taler-harness/src/integrationtests/test-currency-scope.ts
index 69e45f678..34d18d87d 100644
--- a/packages/taler-harness/src/integrationtests/test-currency-scope.ts
+++ b/packages/taler-harness/src/integrationtests/test-currency-scope.ts
@@ -17,13 +17,14 @@
/**
* Imports.
*/
-import { Duration, j2s } from "@gnu-taler/taler-util";
+import { Duration, TalerCorebankApiClient, j2s } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { defaultCoinConfig } from "../harness/denomStructures.js";
import {
+ BankService,
ExchangeService,
- FakebankService,
GlobalTestState,
+ HarnessExchangeBankAccount,
MerchantService,
generateRandomPayto,
setupDb,
@@ -31,6 +32,7 @@ import {
import {
createWalletDaemonWithClient,
withdrawViaBankV2,
+ withdrawViaBankV3,
} from "../harness/helpers.js";
/**
@@ -44,7 +46,7 @@ export async function runCurrencyScopeTest(t: GlobalTestState) {
nameSuffix: "exchange2",
});
- const bank = await FakebankService.create(t, {
+ const bank = await BankService.create(t, {
allowRegistrations: true,
currency: "TESTKUDOS",
database: dbDefault.connStr,
@@ -72,17 +74,25 @@ export async function runCurrencyScopeTest(t: GlobalTestState) {
database: dbDefault.connStr,
});
- const exchangeOneBankAccount = await bank.createExchangeAccount(
- "myexchange",
- "x",
- );
- await exchangeOne.addBankAccount("1", exchangeOneBankAccount);
-
- const exchangeTwoBankAccount = await bank.createExchangeAccount(
- "myexchange2",
- "x",
- );
- await exchangeTwo.addBankAccount("1", exchangeTwoBankAccount);
+ let exchangeOneBankAccount: HarnessExchangeBankAccount = {
+ wireGatewayApiBaseUrl: new URL(
+ "accounts/myexchange/taler-wire-gateway/",
+ bank.corebankApiBaseUrl,
+ ).href,
+ accountName: "myexchange",
+ accountPassword: "x",
+ accountPaytoUri: generateRandomPayto("myexchange"),
+ };
+
+ let exchangeTwoBankAccount: HarnessExchangeBankAccount = {
+ wireGatewayApiBaseUrl: new URL(
+ "accounts/myexchange2/taler-wire-gateway/",
+ bank.corebankApiBaseUrl,
+ ).href,
+ accountName: "myexchange2",
+ accountPassword: "x",
+ accountPaytoUri: generateRandomPayto("myexchange2"),
+ };
bank.setSuggestedExchange(
exchangeOne,
@@ -93,6 +103,31 @@ export async function runCurrencyScopeTest(t: GlobalTestState) {
await bank.pingUntilAvailable();
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, {
+ auth: {
+ username: "admin",
+ password: "adminpw",
+ },
+ });
+
+ await bankClient.registerAccountExtended({
+ name: exchangeOneBankAccount.accountName,
+ username: exchangeOneBankAccount.accountName,
+ password: exchangeOneBankAccount.accountPassword,
+ is_taler_exchange: true,
+ payto_uri: exchangeOneBankAccount.accountPaytoUri,
+ });
+ await exchangeOne.addBankAccount("1", exchangeOneBankAccount);
+
+ await bankClient.registerAccountExtended({
+ name: exchangeTwoBankAccount.accountName,
+ username: exchangeTwoBankAccount.accountName,
+ password: exchangeTwoBankAccount.accountPassword,
+ is_taler_exchange: true,
+ payto_uri: exchangeTwoBankAccount.accountPaytoUri,
+ });
+ await exchangeTwo.addBankAccount("1", exchangeTwoBankAccount);
+
// Set up the first exchange
exchangeOne.addOfferedCoins(defaultCoinConfig);
@@ -139,16 +174,16 @@ export async function runCurrencyScopeTest(t: GlobalTestState) {
// Withdraw digital cash into the wallet.
- const w1 = await withdrawViaBankV2(t, {
+ const w1 = await withdrawViaBankV3(t, {
walletClient,
- bank,
+ bankClient,
exchange: exchangeOne,
amount: "TESTKUDOS:6",
});
- const w2 = await withdrawViaBankV2(t, {
+ const w2 = await withdrawViaBankV3(t, {
walletClient,
- bank,
+ bankClient,
exchange: exchangeTwo,
amount: "TESTKUDOS:6",
});
diff --git a/packages/taler-harness/src/integrationtests/test-multiexchange.ts b/packages/taler-harness/src/integrationtests/test-multiexchange.ts
index b5cf0770f..26e843073 100644
--- a/packages/taler-harness/src/integrationtests/test-multiexchange.ts
+++ b/packages/taler-harness/src/integrationtests/test-multiexchange.ts
@@ -17,13 +17,14 @@
/**
* Imports.
*/
-import { Duration, TalerMerchantApi } from "@gnu-taler/taler-util";
+import { Duration, TalerCorebankApiClient, TalerMerchantApi } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { defaultCoinConfig } from "../harness/denomStructures.js";
import {
+ BankService,
ExchangeService,
- FakebankService,
GlobalTestState,
+ HarnessExchangeBankAccount,
MerchantService,
generateRandomPayto,
setupDb,
@@ -32,6 +33,7 @@ import {
createWalletDaemonWithClient,
makeTestPaymentV2,
withdrawViaBankV2,
+ withdrawViaBankV3,
} from "../harness/helpers.js";
/**
@@ -45,7 +47,7 @@ export async function runMultiExchangeTest(t: GlobalTestState) {
nameSuffix: "exchange2",
});
- const bank = await FakebankService.create(t, {
+ const bank = await BankService.create(t, {
allowRegistrations: true,
currency: "TESTKUDOS",
database: dbDefault.connStr,
@@ -73,17 +75,25 @@ export async function runMultiExchangeTest(t: GlobalTestState) {
database: dbDefault.connStr,
});
- const exchangeOneBankAccount = await bank.createExchangeAccount(
- "myexchange",
- "x",
- );
- await exchangeOne.addBankAccount("1", exchangeOneBankAccount);
+ let exchangeOneBankAccount: HarnessExchangeBankAccount = {
+ wireGatewayApiBaseUrl: new URL(
+ "accounts/myexchange/taler-wire-gateway/",
+ bank.corebankApiBaseUrl,
+ ).href,
+ accountName: "myexchange",
+ accountPassword: "x",
+ accountPaytoUri: generateRandomPayto("myexchange"),
+ };
- const exchangeTwoBankAccount = await bank.createExchangeAccount(
- "myexchange2",
- "x",
- );
- await exchangeTwo.addBankAccount("1", exchangeTwoBankAccount);
+ let exchangeTwoBankAccount: HarnessExchangeBankAccount = {
+ wireGatewayApiBaseUrl: new URL(
+ "accounts/myexchange2/taler-wire-gateway/",
+ bank.corebankApiBaseUrl,
+ ).href,
+ accountName: "myexchange2",
+ accountPassword: "x",
+ accountPaytoUri: generateRandomPayto("myexchange2"),
+ };
bank.setSuggestedExchange(
exchangeOne,
@@ -94,6 +104,31 @@ export async function runMultiExchangeTest(t: GlobalTestState) {
await bank.pingUntilAvailable();
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, {
+ auth: {
+ username: "admin",
+ password: "adminpw",
+ },
+ });
+
+ await bankClient.registerAccountExtended({
+ name: exchangeOneBankAccount.accountName,
+ username: exchangeOneBankAccount.accountName,
+ password: exchangeOneBankAccount.accountPassword,
+ is_taler_exchange: true,
+ payto_uri: exchangeOneBankAccount.accountPaytoUri,
+ });
+ await exchangeOne.addBankAccount("1", exchangeOneBankAccount);
+
+ await bankClient.registerAccountExtended({
+ name: exchangeTwoBankAccount.accountName,
+ username: exchangeTwoBankAccount.accountName,
+ password: exchangeTwoBankAccount.accountPassword,
+ is_taler_exchange: true,
+ payto_uri: exchangeTwoBankAccount.accountPaytoUri,
+ });
+ await exchangeTwo.addBankAccount("1", exchangeTwoBankAccount);
+
// Set up the first exchange
exchangeOne.addOfferedCoins(defaultCoinConfig);
@@ -141,16 +176,16 @@ export async function runMultiExchangeTest(t: GlobalTestState) {
// Withdraw digital cash into the wallet.
- await withdrawViaBankV2(t, {
+ await withdrawViaBankV3(t, {
walletClient,
- bank,
+ bankClient,
exchange: exchangeOne,
amount: "TESTKUDOS:6",
});
- await withdrawViaBankV2(t, {
+ await withdrawViaBankV3(t, {
walletClient,
- bank,
+ bankClient,
exchange: exchangeTwo,
amount: "TESTKUDOS:6",
});
diff --git a/packages/taler-harness/src/integrationtests/test-payment-template.ts b/packages/taler-harness/src/integrationtests/test-payment-template.ts
index fc4cd1198..af92d43c5 100644
--- a/packages/taler-harness/src/integrationtests/test-payment-template.ts
+++ b/packages/taler-harness/src/integrationtests/test-payment-template.ts
@@ -93,7 +93,9 @@ export async function runPaymentTemplateTest(t: GlobalTestState) {
WalletApiOperation.PreparePayForTemplate,
{
talerPayTemplateUri,
- templateParams: {},
+ templateParams: {
+ amount: "TESTKUDOS:1",
+ },
},
);
diff --git a/packages/taler-harness/src/integrationtests/test-refund-auto.ts b/packages/taler-harness/src/integrationtests/test-refund-auto.ts
index 5fcfa066a..6e02071af 100644
--- a/packages/taler-harness/src/integrationtests/test-refund-auto.ts
+++ b/packages/taler-harness/src/integrationtests/test-refund-auto.ts
@@ -17,7 +17,12 @@
/**
* Imports.
*/
-import { Duration, MerchantApiClient } from "@gnu-taler/taler-util";
+import {
+ Duration,
+ MerchantApiClient,
+ TransactionMajorState,
+ TransactionMinorState,
+} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { GlobalTestState } from "../harness/harness.js";
import {
@@ -47,67 +52,134 @@ export async function runRefundAutoTest(t: GlobalTestState) {
await wres.withdrawalFinishedCond;
- // Set up order.
- const orderResp = await merchantClient.createOrder({
- order: {
- summary: "Buy me!",
- amount: "TESTKUDOS:5",
- fulfillment_url: "taler://fulfillment-success/thx",
- auto_refund: {
- d_us: 3000 * 1000,
+ // Test case where the auto-refund happens
+ {
+ // Set up order.
+ const orderResp = await merchantClient.createOrder({
+ order: {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ auto_refund: {
+ d_us: 3000 * 1000,
+ },
},
- },
- refund_delay: Duration.toTalerProtocolDuration(
- Duration.fromSpec({ minutes: 5 }),
- ),
- });
+ refund_delay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 5 }),
+ ),
+ });
- let orderStatus = await merchantClient.queryPrivateOrderStatus({
- orderId: orderResp.order_id,
- });
+ let orderStatus = await merchantClient.queryPrivateOrderStatus({
+ orderId: orderResp.order_id,
+ });
- t.assertTrue(orderStatus.order_status === "unpaid");
+ t.assertTrue(orderStatus.order_status === "unpaid");
- // Make wallet pay for the order
+ // Make wallet pay for the order
- const r1 = await walletClient.call(WalletApiOperation.PreparePayForUri, {
- talerPayUri: orderStatus.taler_pay_uri,
- });
+ const r1 = await walletClient.call(WalletApiOperation.PreparePayForUri, {
+ talerPayUri: orderStatus.taler_pay_uri,
+ });
- await walletClient.call(WalletApiOperation.ConfirmPay, {
- transactionId: r1.transactionId,
- });
+ await walletClient.call(WalletApiOperation.ConfirmPay, {
+ transactionId: r1.transactionId,
+ });
- // Check if payment was successful.
+ // Check if payment was successful.
- orderStatus = await merchantClient.queryPrivateOrderStatus({
- orderId: orderResp.order_id,
- });
+ orderStatus = await merchantClient.queryPrivateOrderStatus({
+ orderId: orderResp.order_id,
+ });
- t.assertTrue(orderStatus.order_status === "paid");
+ t.assertTrue(orderStatus.order_status === "paid");
- const ref = await merchantClient.giveRefund({
- amount: "TESTKUDOS:5",
- instance: "default",
- justification: "foo",
- orderId: orderResp.order_id,
- });
+ const ref = await merchantClient.giveRefund({
+ amount: "TESTKUDOS:5",
+ instance: "default",
+ justification: "foo",
+ orderId: orderResp.order_id,
+ });
+
+ console.log(ref);
+
+ // The wallet should now automatically pick up the refund.
+ await walletClient.call(
+ WalletApiOperation.TestingWaitTransactionsFinal,
+ {},
+ );
+
+ const transactions = await walletClient.call(
+ WalletApiOperation.GetTransactions,
+ {
+ sort: "stable-ascending",
+ },
+ );
+ console.log(JSON.stringify(transactions, undefined, 2));
+
+ const transactionTypes = transactions.transactions.map((x) => x.type);
+ t.assertDeepEqual(transactionTypes, ["withdrawal", "payment", "refund"]);
+ }
+
+ // Now test the case where the auto-refund just expires
+
+ {
+ // Set up order.
+ const orderResp = await merchantClient.createOrder({
+ order: {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ auto_refund: {
+ d_us: 3000 * 1000,
+ },
+ },
+ refund_delay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 5 }),
+ ),
+ });
+
+ let orderStatus = await merchantClient.queryPrivateOrderStatus({
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
- console.log(ref);
+ // Make wallet pay for the order
- // The wallet should now automatically pick up the refund.
- await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+ const r1 = await walletClient.call(WalletApiOperation.PreparePayForUri, {
+ talerPayUri: orderStatus.taler_pay_uri,
+ });
- const transactions = await walletClient.call(
- WalletApiOperation.GetTransactions,
- {},
- );
- console.log(JSON.stringify(transactions, undefined, 2));
+ await walletClient.call(WalletApiOperation.ConfirmPay, {
+ transactionId: r1.transactionId,
+ });
- const transactionTypes = transactions.transactions.map((x) => x.type);
- t.assertDeepEqual(transactionTypes, ["withdrawal", "payment", "refund"]);
+ // Check if payment was successful.
- await t.shutdown();
+ orderStatus = await merchantClient.queryPrivateOrderStatus({
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "paid");
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: r1.transactionId,
+ txState: {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.AutoRefund,
+ },
+ });
+ // Only time-travel the wallet
+ await walletClient.call(WalletApiOperation.TestingSetTimetravel, {
+ offsetMs: 5000,
+ });
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: r1.transactionId,
+ txState: {
+ major: TransactionMajorState.Done,
+ },
+ });
+ }
}
runRefundAutoTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts b/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts
index e6c84b75d..046bd5aed 100644
--- a/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts
+++ b/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts
@@ -24,11 +24,12 @@ import {
NotificationType,
PreparePayResultType,
TalerCorebankApiClient,
+ j2s,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { makeNoFeeCoinConfig } from "../harness/denomStructures.js";
import {
- BankService,
+ BankService,
ExchangeService,
GlobalTestState,
MerchantService,
@@ -78,7 +79,10 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) {
await exchange.addBankAccount("1", {
accountName: exchangeBankUsername,
accountPassword: exchangeBankPassword,
- wireGatewayApiBaseUrl: new URL("accounts/exchange/taler-wire-gateway/", bank.baseUrl).href,
+ wireGatewayApiBaseUrl: new URL(
+ "accounts/exchange/taler-wire-gateway/",
+ bank.baseUrl,
+ ).href,
accountPaytoUri: exchangePaytoUri,
});
@@ -129,29 +133,42 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) {
const { walletClient } = await createWalletDaemonWithClient(t, {
name: "w1",
+ persistent: true,
});
const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
- // Withdraw digital cash into the wallet.
+ t.logStep("exchangeUpdated1Cond");
+ // Withdraw digital cash into the wallet.
+ t.logStep("Withdraw digital cash into the wallet.");
const wres = await withdrawViaBankV3(t, {
walletClient,
bankClient,
exchange,
amount: "TESTKUDOS:15",
});
+ t.logStep("wait");
await wres.withdrawalFinishedCond;
-
const exchangeUpdated1Cond = walletClient.waitForNotificationCond(
(x) =>
- x.type === NotificationType.ExchangeStateTransition &&
- x.exchangeBaseUrl === exchange.baseUrl,
+ {
+ t.logStep(`EXCHANGE UPDATE, ${j2s(x)}`)
+ return x.type === NotificationType.ExchangeStateTransition &&
+ x.exchangeBaseUrl === exchange.baseUrl
+ }
);
+ t.logStep("waiting tx");
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+ {
+ const balance = await walletClient.call(WalletApiOperation.GetBalances, {});
+ t.assertAmountEquals(balance.balances[0].available, "TESTKUDOS:15");
+ }
+
// Travel into the future, the deposit expiration is two years
// into the future.
- console.log("applying first time travel");
+ t.logStep("applying first time travel");
await applyTimeTravelV2(
Duration.toMilliseconds(Duration.fromSpec({ days: 400 })),
{
@@ -162,9 +179,16 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) {
);
// The time travel should cause exchanges to update.
+ t.logStep("The time travel should cause exchanges to update");
await exchangeUpdated1Cond;
+ t.logStep("exchange updated, waiting for tx");
await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+ {
+ const balance = await walletClient.call(WalletApiOperation.GetBalances, {});
+ t.assertAmountEquals(balance.balances[0].available, "TESTKUDOS:15");
+ }
+ t.logStep("withdrawing second time");
const wres2 = await withdrawViaBankV3(t, {
walletClient,
bankClient,
@@ -173,8 +197,14 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) {
});
await wres2.withdrawalFinishedCond;
+ t.logStep("witdrawn, waiting tx");
await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+ {
+ const balance = await walletClient.call(WalletApiOperation.GetBalances, {});
+ t.assertAmountEquals(balance.balances[0].available, "TESTKUDOS:35");
+ }
+
const exchangeUpdated2Cond = walletClient.waitForNotificationCond(
(x) =>
x.type === NotificationType.ExchangeStateTransition &&
@@ -183,7 +213,7 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) {
// Travel into the future, the deposit expiration is two years
// into the future.
- console.log("applying second time travel");
+ t.logStep("applying second time travel");
await applyTimeTravelV2(
Duration.toMilliseconds(Duration.fromSpec({ years: 2, months: 6 })),
{
@@ -194,8 +224,13 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) {
);
// The time travel should cause exchanges to update.
+ t.logStep("The time travel should cause exchanges to update.");
await exchangeUpdated2Cond;
await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+ {
+ const balance = await walletClient.call(WalletApiOperation.GetBalances, {});
+ t.assertAmountEquals(balance.balances[0].available, "TESTKUDOS:35");
+ }
// At this point, the original coins should've been refreshed.
// It would be too late to refresh them now, as we're past
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-denom-expire.ts b/packages/taler-harness/src/integrationtests/test-wallet-denom-expire.ts
index b9d028efd..a2573eda1 100644
--- a/packages/taler-harness/src/integrationtests/test-wallet-denom-expire.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-denom-expire.ts
@@ -17,21 +17,16 @@
/**
* Imports.
*/
-import { Duration, Logger, NotificationType, TalerCorebankApiClient, j2s } from "@gnu-taler/taler-util";
+import { Duration, Logger, NotificationType, j2s } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { makeNoFeeCoinConfig } from "../harness/denomStructures.js";
import {
- BankService,
- ExchangeService,
- FakebankService,
GlobalTestState,
- MerchantService,
- generateRandomPayto,
setupDb,
} from "../harness/harness.js";
import {
applyTimeTravelV2,
- createWalletDaemonWithClient,
+ createSimpleTestkudosEnvironmentV3,
withdrawViaBankV3,
} from "../harness/helpers.js";
@@ -45,89 +40,14 @@ export async function runWalletDenomExpireTest(t: GlobalTestState) {
const db = await setupDb(t);
- const bank = await BankService.create(t, {
- allowRegistrations: true,
- currency: "TESTKUDOS",
- database: db.connStr,
- httpPort: 8082,
- });
-
- const exchange = ExchangeService.create(t, {
- name: "testexchange-1",
- currency: "TESTKUDOS",
- httpPort: 8081,
- database: db.connStr,
- });
-
- const merchant = await MerchantService.create(t, {
- name: "testmerchant-1",
- currency: "TESTKUDOS",
- httpPort: 8083,
- database: db.connStr,
- });
-
- let receiverName = "Exchange";
- let exchangeBankUsername = "exchange";
- let exchangeBankPassword = "mypw";
- let exchangePaytoUri = generateRandomPayto(exchangeBankUsername);
-
- await exchange.addBankAccount("1", {
- accountName: exchangeBankUsername,
- accountPassword: exchangeBankPassword,
- wireGatewayApiBaseUrl: new URL("accounts/exchange/taler-wire-gateway/", bank.baseUrl).href,
- accountPaytoUri: exchangePaytoUri,
- });
-
- bank.setSuggestedExchange(exchange, exchangePaytoUri);
-
- await bank.start();
-
- await bank.pingUntilAvailable();
-
- const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, {
- auth: {
- username: "admin",
- password: "adminpw",
- },
- });
-
- await bankClient.registerAccountExtended({
- name: receiverName,
- password: exchangeBankPassword,
- username: exchangeBankUsername,
- is_taler_exchange: true,
- payto_uri: exchangePaytoUri,
- });
-
- exchange.addCoinConfigList(makeNoFeeCoinConfig("TESTKUDOS"));
-
- await exchange.start();
- await exchange.pingUntilAvailable();
-
- merchant.addExchange(exchange);
-
- await merchant.start();
- await merchant.pingUntilAvailable();
-
- console.log("merchant started, configuring instances");
-
- await merchant.addInstanceWithWireAccount({
- id: "default",
- name: "Default Instance",
- paytoUris: [generateRandomPayto("merchant-default")],
- });
+ const coinConfig = makeNoFeeCoinConfig("TESTKUDOS");
- await merchant.addInstanceWithWireAccount({
- id: "minst1",
- name: "minst1",
- paytoUris: [generateRandomPayto("minst1")],
- });
-
- console.log("setup done!");
-
- const { walletClient } = await createWalletDaemonWithClient(t, {
- name: "default",
- });
+ const {
+ walletClient,
+ bankClient,
+ exchange,
+ merchant,
+ } = await createSimpleTestkudosEnvironmentV3(t, coinConfig, {});
// Withdraw digital cash into the wallet.
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-exchange-update.ts b/packages/taler-harness/src/integrationtests/test-wallet-exchange-update.ts
index b36e6ef61..3a1b467c3 100644
--- a/packages/taler-harness/src/integrationtests/test-wallet-exchange-update.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-exchange-update.ts
@@ -21,6 +21,7 @@ import {
AmountString,
ExchangeUpdateStatus,
NotificationType,
+ TalerCorebankApiClient,
j2s,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
@@ -30,11 +31,14 @@ import {
ExchangeService,
FakebankService,
GlobalTestState,
+ HarnessExchangeBankAccount,
+ generateRandomPayto,
setupDb,
} from "../harness/harness.js";
import {
createWalletDaemonWithClient,
withdrawViaBankV2,
+ withdrawViaBankV3,
} from "../harness/helpers.js";
/**
@@ -51,7 +55,7 @@ export async function runWalletExchangeUpdateTest(
nameSuffix: "two",
});
- const bank = await FakebankService.create(t, {
+ const bank = await BankService.create(t, {
allowRegistrations: true,
currency: "TESTKUDOS",
database: db.connStr,
@@ -75,10 +79,27 @@ export async function runWalletExchangeUpdateTest(
database: db2.connStr,
});
- const exchangeBankAccount = await bank.createExchangeAccount(
- "myexchange",
- "x",
- );
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, {
+ auth: {
+ username: "admin",
+ password: "adminpw",
+ },
+ });
+
+ // const exchangeBankAccount = await bank.createExchangeAccount(
+ // "myexchange",
+ // "x",
+ // );
+
+ let exchangeBankAccount: HarnessExchangeBankAccount = {
+ wireGatewayApiBaseUrl: new URL(
+ "accounts/myexchange/taler-wire-gateway/",
+ bank.corebankApiBaseUrl,
+ ).href,
+ accountName: "myexchange",
+ accountPassword: "x",
+ accountPaytoUri: generateRandomPayto("myexchange"),
+ };
await exchangeOne.addBankAccount("1", exchangeBankAccount);
await exchangeTwo.addBankAccount("1", exchangeBankAccount);
@@ -88,6 +109,14 @@ export async function runWalletExchangeUpdateTest(
await bank.start();
+ bankClient.registerAccountExtended({
+ name: exchangeBankAccount.accountName,
+ username: exchangeBankAccount.accountName,
+ password: exchangeBankAccount.accountPassword,
+ is_taler_exchange: true,
+ payto_uri: exchangeBankAccount.accountPaytoUri,
+ });
+
exchangeOne.addCoinConfigList(defaultCoinConfig.map((x) => x("TESTKUDOS")));
exchangeTwo.addCoinConfigList(defaultCoinConfig.map((x) => x("TESTKUDOS")));
@@ -108,9 +137,9 @@ export async function runWalletExchangeUpdateTest(
t.assertDeepEqual(exchangesListResult.exchanges.length, 0);
- const wres = await withdrawViaBankV2(t, {
+ const wres = await withdrawViaBankV3(t, {
walletClient,
- bank,
+ bankClient,
exchange: exchangeOne,
amount: "TESTKUDOS:10",
});
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-insufficient-balance.ts b/packages/taler-harness/src/integrationtests/test-wallet-insufficient-balance.ts
index ac1244446..4062e186d 100644
--- a/packages/taler-harness/src/integrationtests/test-wallet-insufficient-balance.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-insufficient-balance.ts
@@ -22,71 +22,32 @@ import {
Duration,
PaymentInsufficientBalanceDetails,
TalerErrorCode,
- WalletNotification,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
import {
- ExchangeService,
- FakebankService,
GlobalTestState,
- MerchantService,
- WalletClient,
- WalletService,
generateRandomPayto,
setupDb,
} from "../harness/harness.js";
-import { withdrawViaBankV2 } from "../harness/helpers.js";
+import { createSimpleTestkudosEnvironmentV3, withdrawViaBankV3 } from "../harness/helpers.js";
export async function runWalletInsufficientBalanceTest(t: GlobalTestState) {
// Set up test environment
const db = await setupDb(t);
- const bank = await FakebankService.create(t, {
- allowRegistrations: true,
- currency: "TESTKUDOS",
- database: db.connStr,
- httpPort: 8082,
- });
-
- const exchange = ExchangeService.create(t, {
- name: "testexchange-1",
- currency: "TESTKUDOS",
- httpPort: 8081,
- database: db.connStr,
- });
-
- const merchant = await MerchantService.create(t, {
- name: "testmerchant-1",
- currency: "TESTKUDOS",
- httpPort: 8083,
- database: db.connStr,
- });
-
- const exchangeBankAccount = await bank.createExchangeAccount(
- "myexchange",
- "x",
- );
- exchangeBankAccount.skipWireFeeCreation = true;
- exchange.addBankAccount("1", exchangeBankAccount);
-
- bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri);
-
- await bank.start();
-
- await bank.pingUntilAvailable();
-
const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS"));
- exchange.addCoinConfigList(coinConfig);
-
- await exchange.start();
- await exchange.pingUntilAvailable();
- merchant.addExchange(exchange);
-
- await merchant.start();
- await merchant.pingUntilAvailable();
+ let {
+ bankClient,
+ exchange,
+ merchant,
+ walletService,
+ walletClient,
+ } = await createSimpleTestkudosEnvironmentV3(t, coinConfig, {
+ skipWireFeeCreation: true,
+ });
await merchant.addInstanceWithWireAccount({
id: "default",
@@ -106,24 +67,6 @@ export async function runWalletInsufficientBalanceTest(t: GlobalTestState) {
),
});
- const walletService = new WalletService(t, {
- name: "wallet",
- useInMemoryDb: true,
- });
- await walletService.start();
- await walletService.pingUntilAvailable();
-
- const allNotifications: WalletNotification[] = [];
-
- const walletClient = new WalletClient({
- name: "wallet",
- unixPath: walletService.socketPath,
- onNotification(n) {
- console.log("got notification", n);
- allNotifications.push(n);
- },
- });
- await walletClient.connect();
await walletClient.client.call(WalletApiOperation.InitWallet, {
config: {
testing: {
@@ -132,9 +75,9 @@ export async function runWalletInsufficientBalanceTest(t: GlobalTestState) {
},
});
- const wres = await withdrawViaBankV2(t, {
+ const wres = await withdrawViaBankV3(t, {
amount: "TESTKUDOS:10",
- bank,
+ bankClient,
exchange,
walletClient,
});
@@ -146,10 +89,12 @@ export async function runWalletInsufficientBalanceTest(t: GlobalTestState) {
depositPaytoUri: "payto://x-taler-bank/localhost/foobar",
});
});
+
t.assertDeepEqual(
exc.errorDetail.code,
TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE,
);
+
const insufficientBalanceDetails: PaymentInsufficientBalanceDetails =
exc.errorDetail.insufficientBalanceDetails;
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts
index a13095883..3ec2a3bcd 100644
--- a/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts
@@ -46,7 +46,7 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
"TESTKUDOS:10",
);
- // Hand it to the wallet
+ t.logStep("Hand it to the wallet")
const r1 = await walletClient.client.call(
WalletApiOperation.GetWithdrawalDetailsForUri,
@@ -55,7 +55,7 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
},
);
- // Withdraw
+ t.logStep("Withdraw")
const r2 = await walletClient.client.call(
WalletApiOperation.AcceptBankIntegratedWithdrawal,
@@ -65,6 +65,7 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
},
);
+ t.logStep("wait confirmed")
const withdrawalBankConfirmedCond = walletClient.waitForNotificationCond(
(x) => {
return (
@@ -76,6 +77,7 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
},
);
+ t.logStep("wait finished")
const withdrawalFinishedCond = walletClient.waitForNotificationCond((x) => {
return (
x.type === NotificationType.TransactionStateTransition &&
@@ -84,6 +86,7 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
);
});
+ t.logStep("wait withdraw coins")
const withdrawalReserveReadyCond = walletClient.waitForNotificationCond(
(x) => {
return (
@@ -95,7 +98,7 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
},
);
- // Do it twice to check idempotency
+ t.logStep("Do it twice to check idempotency")
const r3 = await walletClient.client.call(
WalletApiOperation.AcceptBankIntegratedWithdrawal,
{
@@ -104,9 +107,10 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
},
);
+ t.logStep("stop wirewatch")
await exchange.stopWirewatch();
- // Check status before withdrawal is confirmed by bank.
+ t.logStep("Check status before withdrawal is confirmed by bank.")
{
const txn = await walletClient.client.call(
WalletApiOperation.GetTransactions,
@@ -122,7 +126,7 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
t.assertTrue(tx0.withdrawalDetails.reserveIsReady === false);
}
- // Confirm it
+ t.logStep("Confirm it")
await bankClient.confirmWithdrawalOperation(user.username, {
withdrawalOperationId: wop.withdrawal_id,
@@ -132,6 +136,7 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
// Check status after withdrawal is confirmed by bank,
// but before funds are wired to the exchange.
+ t.logStep("Check status after withdrawal")
{
const txn = await walletClient.client.call(
WalletApiOperation.GetTransactions,
@@ -147,11 +152,13 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
t.assertTrue(tx0.withdrawalDetails.reserveIsReady === false);
}
+ t.logStep("start wirewatch")
await exchange.startWirewatch();
+ t.logStep("wait reserve")
await withdrawalReserveReadyCond;
- // Check status after funds were wired.
+ t.logStep("Check status after funds were wired.")
{
const txn = await walletClient.client.call(
WalletApiOperation.GetTransactions,
@@ -169,7 +176,7 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
await withdrawalFinishedCond;
- // Check balance
+ t.logStep("Check balance")
const balResp = await walletClient.client.call(
WalletApiOperation.GetBalances,
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-conversion.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-conversion.ts
index 615feafa7..c55e1faf0 100644
--- a/packages/taler-harness/src/integrationtests/test-withdrawal-conversion.ts
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-conversion.ts
@@ -33,9 +33,11 @@ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import * as http from "node:http";
import { defaultCoinConfig } from "../harness/denomStructures.js";
import {
+ BankService,
ExchangeService,
FakebankService,
GlobalTestState,
+ HarnessExchangeBankAccount,
MerchantService,
generateRandomPayto,
setupDb,
@@ -135,7 +137,7 @@ export async function runWithdrawalConversionTest(t: GlobalTestState) {
const db = await setupDb(t);
- const bank = await FakebankService.create(t, {
+ const bank = await BankService.create(t, {
allowRegistrations: true,
currency: "TESTKUDOS",
database: db.connStr,
@@ -156,17 +158,40 @@ export async function runWithdrawalConversionTest(t: GlobalTestState) {
database: db.connStr,
});
- const exchangeBankAccount = await bank.createExchangeAccount(
- "myexchange",
- "x",
- );
- exchangeBankAccount.conversionUrl = "http://localhost:8071/";
+ let exchangeBankAccount: HarnessExchangeBankAccount = {
+ wireGatewayApiBaseUrl: new URL(
+ "accounts/myexchange/taler-wire-gateway/",
+ bank.corebankApiBaseUrl,
+ ).href,
+ accountName: "myexchange",
+ accountPassword: "x",
+ accountPaytoUri: generateRandomPayto("myexchange"),
+ conversionUrl: "http://localhost:8071/",
+ };
+
await exchange.addBankAccount("1", exchangeBankAccount);
await bank.start();
await bank.pingUntilAvailable();
+ const bankClientAuth = {
+ username: "admin",
+ password: "adminpw",
+ };
+
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, {
+ auth: bankClientAuth,
+ });
+
+ await bankClient.registerAccountExtended({
+ name: exchangeBankAccount.accountName,
+ username: exchangeBankAccount.accountName,
+ password: exchangeBankAccount.accountPassword,
+ is_taler_exchange: true,
+ payto_uri: exchangeBankAccount.accountPaytoUri,
+ });
+
exchange.addOfferedCoins(defaultCoinConfig);
await exchange.start();
@@ -194,7 +219,7 @@ export async function runWithdrawalConversionTest(t: GlobalTestState) {
),
});
- const { walletClient, walletService } = await createWalletDaemonWithClient(
+ const { walletClient } = await createWalletDaemonWithClient(
t,
{ name: "wallet" },
);
@@ -203,11 +228,7 @@ export async function runWithdrawalConversionTest(t: GlobalTestState) {
// Create a withdrawal operation
- const bankAccessApiClient = new TalerCorebankApiClient(
- bank.corebankApiBaseUrl,
- );
-
- const user = await bankAccessApiClient.createRandomBankUser();
+ const user = await bankClient.createRandomBankUser();
await walletClient.call(WalletApiOperation.AddExchange, {
exchangeBaseUrl: exchange.baseUrl,
@@ -277,10 +298,7 @@ export async function runWithdrawalConversionTest(t: GlobalTestState) {
const wireGatewayApiClient = new WireGatewayApiClient(
exchangeBankAccount.wireGatewayApiBaseUrl,
{
- auth: {
- username: exchangeBankAccount.accountName,
- password: exchangeBankAccount.accountPassword,
- },
+ auth: bankClientAuth,
},
);
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts
index 1c65de7d9..0657d2da7 100644
--- a/packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts
@@ -90,7 +90,10 @@ export async function runWithdrawalFeesTest(t: GlobalTestState) {
await exchange.addBankAccount("1", {
accountName: exchangeBankUsername,
accountPassword: exchangeBankPassword,
- wireGatewayApiBaseUrl: new URL("accounts/exchange/taler-wire-gateway/", bank.baseUrl).href,
+ wireGatewayApiBaseUrl: new URL(
+ "accounts/exchange/taler-wire-gateway/",
+ bank.baseUrl,
+ ).href,
accountPaytoUri: exchangePaytoUri,
});
@@ -133,12 +136,9 @@ export async function runWithdrawalFeesTest(t: GlobalTestState) {
const user = await bankClient.createRandomBankUser();
bankClient.setAuth(user);
- const wop = await bankClient.createWithdrawalOperation(
- user.username,
- amount,
- );
+ const wop = await bankClient.createWithdrawalOperation(user.username, amount);
- // Hand it to the wallet
+ t.logStep("Hand it to the wallet")
const details = await wallet.client.call(
WalletApiOperation.GetWithdrawalDetailsForUri,
@@ -149,10 +149,13 @@ export async function runWithdrawalFeesTest(t: GlobalTestState) {
console.log(j2s(details));
+ const myAmount = details.amount;
+ t.assertTrue(!!myAmount);
+
const amountDetails = await wallet.client.call(
WalletApiOperation.GetWithdrawalDetailsForAmount,
{
- amount: details.amount,
+ amount: myAmount,
exchangeBaseUrl: details.possibleExchanges[0].exchangeBaseUrl,
},
);
@@ -162,23 +165,25 @@ export async function runWithdrawalFeesTest(t: GlobalTestState) {
t.assertAmountEquals(amountDetails.amountEffective, "TESTKUDOS:5");
t.assertAmountEquals(amountDetails.amountRaw, "TESTKUDOS:7.5");
+ t.logStep("Complete all pending operations")
+
await wallet.runPending();
- // Withdraw (AKA select)
+ t.logStep("Withdraw (AKA select)")
await wallet.client.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, {
exchangeBaseUrl: exchange.baseUrl,
talerWithdrawUri: wop.taler_withdraw_uri,
});
- // Confirm it
+ t.logStep("Confirm it")
await bankClient.confirmWithdrawalOperation(user.username, {
withdrawalOperationId: wop.withdrawal_id,
});
await wallet.runUntilDone();
- // Check balance
+ t.logStep("Check balance")
const balResp = await wallet.client.call(WalletApiOperation.GetBalances, {});
console.log(j2s(balResp));
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-flex.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-flex.ts
new file mode 100644
index 000000000..ffc7249b8
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-flex.ts
@@ -0,0 +1,70 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import { j2s } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import { createSimpleTestkudosEnvironmentV3 } from "../harness/helpers.js";
+
+/**
+ * Run test for bank-integrated withdrawal with flexible amount,
+ * i.e. the amount is chosen by the wallet.
+ */
+export async function runWithdrawalFlexTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bankClient, exchange } =
+ await createSimpleTestkudosEnvironmentV3(t);
+
+ // Create a withdrawal operation
+ const user = await bankClient.createRandomBankUser();
+ bankClient.setAuth(user);
+ const wop = await bankClient.createWithdrawalOperation(
+ user.username,
+ undefined,
+ );
+
+ const r1 = await walletClient.call(
+ WalletApiOperation.GetWithdrawalDetailsForUri,
+ {
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ },
+ );
+
+ console.log(j2s(r1));
+
+ // Withdraw
+
+ const r2 = await walletClient.call(
+ WalletApiOperation.AcceptBankIntegratedWithdrawal,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ amount: "TESTKUDOS:10",
+ },
+ );
+
+ await bankClient.confirmWithdrawalOperation(user.username, {
+ withdrawalOperationId: wop.withdrawal_id,
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+}
+
+runWithdrawalFlexTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts
index eb2ae7fa6..4588310b1 100644
--- a/packages/taler-harness/src/integrationtests/testrunner.ts
+++ b/packages/taler-harness/src/integrationtests/testrunner.ts
@@ -113,14 +113,15 @@ import { runWalletRefreshTest } from "./test-wallet-refresh.js";
import { runWalletWirefeesTest } from "./test-wallet-wirefees.js";
import { runWallettestingTest } from "./test-wallettesting.js";
import { runWithdrawalAbortBankTest } from "./test-withdrawal-abort-bank.js";
+import { runWithdrawalAmountTest } from "./test-withdrawal-amount.js";
import { runWithdrawalBankIntegratedTest } from "./test-withdrawal-bank-integrated.js";
import { runWithdrawalConversionTest } from "./test-withdrawal-conversion.js";
import { runWithdrawalFakebankTest } from "./test-withdrawal-fakebank.js";
import { runWithdrawalFeesTest } from "./test-withdrawal-fees.js";
+import { runWithdrawalFlexTest } from "./test-withdrawal-flex.js";
import { runWithdrawalHandoverTest } from "./test-withdrawal-handover.js";
import { runWithdrawalHugeTest } from "./test-withdrawal-huge.js";
import { runWithdrawalManualTest } from "./test-withdrawal-manual.js";
-import { runWithdrawalAmountTest } from "./test-withdrawal-amount.js";
/**
* Test runner.
@@ -232,6 +233,7 @@ const allTests: TestMainFunction[] = [
runPeerPushLargeTest,
runWithdrawalHandoverTest,
runWithdrawalAmountTest,
+ runWithdrawalFlexTest,
];
export interface TestRunSpec {
diff --git a/packages/taler-util/package.json b/packages/taler-util/package.json
index 74b2d6155..87e6a7cfa 100644
--- a/packages/taler-util/package.json
+++ b/packages/taler-util/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-util",
- "version": "0.10.7",
+ "version": "0.11.4",
"description": "Generic helper functionality for GNU Taler",
"type": "module",
"types": "./lib/index.node.d.ts",
diff --git a/packages/taler-util/src/CancellationToken.ts b/packages/taler-util/src/CancellationToken.ts
index 3aa576d77..5f38f0c7b 100644
--- a/packages/taler-util/src/CancellationToken.ts
+++ b/packages/taler-util/src/CancellationToken.ts
@@ -172,7 +172,7 @@ class CancellationToken {
} = CancellationToken.create();
let timer: NodeJS.Timeout | null;
- timer = setTimeout(() => originalCancel(CancellationToken.timeout), ms);
+ timer = setTimeout(() => originalCancel(`CancellationToken.timeout ${ms}`), ms);
const disposeTimer = () => {
if (timer == null) return;
clearTimeout(timer);
diff --git a/packages/taler-util/src/bank-api-client.ts b/packages/taler-util/src/bank-api-client.ts
index e9f442af6..e1409087f 100644
--- a/packages/taler-util/src/bank-api-client.ts
+++ b/packages/taler-util/src/bank-api-client.ts
@@ -385,7 +385,7 @@ export class TalerCorebankApiClient {
async createWithdrawalOperation(
user: string,
- amount: string,
+ amount: string | undefined,
): Promise<WithdrawalOperationInfo> {
const url = new URL(`accounts/${user}/withdrawals`, this.baseUrl);
const resp = await this.httpLib.fetch(url.href, {
diff --git a/packages/taler-util/src/errors.ts b/packages/taler-util/src/errors.ts
index 9378d25e8..d68177e4e 100644
--- a/packages/taler-util/src/errors.ts
+++ b/packages/taler-util/src/errors.ts
@@ -166,6 +166,11 @@ export interface DetailsMap {
[TalerErrorCode.WALLET_DB_UNAVAILABLE]: {
innerError: TalerErrorDetail | undefined;
};
+ [TalerErrorCode.WALLET_EXCHANGE_TOS_NOT_ACCEPTED]: {
+ exchangeBaseUrl: string;
+ tosStatus: string;
+ currentEtag: string | undefined;
+ };
}
type ErrBody<Y> = Y extends keyof DetailsMap ? DetailsMap[Y] : empty;
diff --git a/packages/taler-util/src/http-client/bank-integration.ts b/packages/taler-util/src/http-client/bank-integration.ts
index 75e6a627a..23740328b 100644
--- a/packages/taler-util/src/http-client/bank-integration.ts
+++ b/packages/taler-util/src/http-client/bank-integration.ts
@@ -50,7 +50,9 @@ export type TalerBankIntegrationErrorsByMethod<
* The API is used by the wallets.
*/
export class TalerBankIntegrationHttpClient {
- public readonly PROTOCOL_VERSION = "2:0:2";
+ public static readonly PROTOCOL_VERSION = "2:0:1";
+ public readonly PROTOCOL_VERSION =
+ TalerBankIntegrationHttpClient.PROTOCOL_VERSION;
httpLib: HttpRequestLibrary;
@@ -147,6 +149,10 @@ export class TalerBankIntegrationHttpClient {
return opKnownTalerFailure(details.code, details);
case TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE:
return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_AMOUNT_DIFFERS:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_AMOUNT_REQUIRED:
+ return opKnownTalerFailure(details.code, details);
default:
return opUnknownFailure(resp, details);
}
diff --git a/packages/taler-util/src/http-client/bank-revenue.ts b/packages/taler-util/src/http-client/bank-revenue.ts
index 34afe7d86..8331856a9 100644
--- a/packages/taler-util/src/http-client/bank-revenue.ts
+++ b/packages/taler-util/src/http-client/bank-revenue.ts
@@ -25,6 +25,7 @@ import { LibtoolVersion } from "../libtool-version.js";
import {
FailCasesByMethod,
ResultByMethod,
+ opFixedSuccess,
opKnownHttpFailure,
opSuccessFromHttp,
opUnknownFailure,
@@ -117,6 +118,9 @@ export class TalerRevenueHttpClient {
switch (resp.status) {
case HttpStatusCode.Ok:
return opSuccessFromHttp(resp, codecForRevenueIncomingHistory());
+ // FIXME: missing in docs
+ case HttpStatusCode.NoContent:
+ return opFixedSuccess({incoming_transactions: [], credit_account: "" });
case HttpStatusCode.BadRequest:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Unauthorized:
diff --git a/packages/taler-util/src/http-client/types.ts b/packages/taler-util/src/http-client/types.ts
index edddf7d94..3e6d857cb 100644
--- a/packages/taler-util/src/http-client/types.ts
+++ b/packages/taler-util/src/http-client/types.ts
@@ -360,6 +360,7 @@ export const codecForCoreBankConfig = (): Codec<TalerCorebankApi.Config> =>
),
)
.property("wire_type", codecOptionalDefault(codecForString(), "iban"))
+ .property("wire_transfer_fees", codecOptional(codecForAmountString()))
.build("TalerCorebankApi.Config");
//FIXME: implement this codec
@@ -902,7 +903,6 @@ export const codecForTemplateDetails =
.property("template_description", codecForString())
.property("otp_id", codecOptional(codecForString()))
.property("template_contract", codecForTemplateContractDetails())
- .property("required_currency", codecOptional(codecForString()))
.property(
"editable_defaults",
codecOptional(codecForTemplateContractDetailsDefaults()),
@@ -931,7 +931,6 @@ export const codecForWalletTemplateDetails =
(): Codec<TalerMerchantApi.WalletTemplateDetails> =>
buildCodecForObject<TalerMerchantApi.WalletTemplateDetails>()
.property("template_contract", codecForTemplateContractDetails())
- .property("required_currency", codecOptional(codecForString()))
.property(
"editable_defaults",
codecOptional(codecForTemplateContractDetailsDefaults()),
@@ -1311,9 +1310,12 @@ export const codecForBankWithdrawalOperationStatus =
codecForConstString("confirmed"),
),
)
- .property("amount", codecForAmountString())
+ .property("amount", codecOptional(codecForAmountString()))
+ .property("currency", codecOptional(codecForCurrencyName()))
+ .property("suggested_amount", codecOptional(codecForAmountString()))
+ .property("card_fees", codecOptional(codecForAmountString()))
.property("sender_wire", codecOptional(codecForPaytoString()))
- .property("suggested_exchange", codecOptional(codecForString()))
+ .property("suggested_exchange", codecOptional(codecForURL()))
.property("confirm_transfer_url", codecOptional(codecForURL()))
.property("wire_types", codecForList(codecForString()))
.property("selected_reserve_pub", codecOptional(codecForString()))
@@ -2028,20 +2030,53 @@ export namespace TalerBankIntegrationApi {
// confirmed: the transfer has been confirmed and registered by the bank
status: WithdrawalOperationStatus;
- // Amount that will be withdrawn with this operation
- // (raw amount without fee considerations).
- amount: AmountString;
+ // Currency used for the withdrawal.
+ // MUST be present when amount is absent.
+ // @since v2, may become mandatory in the future.
+ currency?: string;
- // Bank account of the customer that is withdrawing, as a
- // payto URI.
+ // Amount that will be withdrawn with this operation
+ // (raw amount without fee considerations). Only
+ // given once the amount is fixed and cannot be changed.
+ // Optional since **vC2EC**.
+ amount?: AmountString | undefined;
+
+ // Suggestion for the amount to be withdrawn with this
+ // operation. Given if a suggestion was made but the
+ // user may still change the amount.
+ // Optional since **vC2EC**.
+ suggested_amount?: AmountString | undefined;
+
+ // Maximum amount that the wallet can choose to withdraw.
+ // Only applicable when the amount is not fixed.
+ // @since **vC2EC**.
+ max_amount?: AmountString | undefined;
+
+ // The non-Taler card fees the customer will have
+ // to pay to the bank / payment service provider
+ // they are using to make the withdrawal.
+ // @since **vC2EC**
+ card_fees?: AmountString | undefined;
+
+ // Bank account of the customer that is debiting, as an
+ // RFC 8905 payto URI.
sender_wire?: PaytoString;
- // Suggestion for an exchange given by the bank.
+ // Base URL of the suggested exchange. The bank may have
+ // neither a suggestion nor a requirement for the exchange.
+ // This value is typically set in the bank's configuration.
suggested_exchange?: string;
+ // Base URL of an exchange that must be used. Optional,
+ // not given *unless* a particular exchange is mandatory.
+ // This value is typically set in the bank's configuration.
+ // @since **vC2EC**
+ required_exchange?: string;
+
// URL that the user needs to navigate to in order to
// complete some final confirmation (e.g. 2FA).
- // It may contain withdrawal operation id
+ // Only applicable when status is selected or pending.
+ // It may contain the withdrawal operation id.
confirm_transfer_url?: string;
// Wire transfer types supported by the bank.
@@ -2051,17 +2086,24 @@ export namespace TalerBankIntegrationApi {
// only non-null if status is selected or confirmed.
selected_reserve_pub?: string;
- // Exchange account selected by the wallet
+ // Exchange account selected by the wallet;
// only non-null if status is selected or confirmed.
+ // @since **v1**
selected_exchange_account?: string;
}
export interface BankWithdrawalOperationPostRequest {
- // Reserve public key.
+ // Reserve public key that should become the wire transfer
+ // subject to fund the withdrawal.
reserve_pub: string;
// Payto address of the exchange selected for the withdrawal.
selected_exchange: PaytoString;
+
+ // Selected amount to be transferred. Optional if the
+ // backend already knows the amount.
+ // @since **vC2EC**
+ amount?: AmountString | undefined;
}
export interface BankWithdrawalOperationPostResponse {
@@ -2075,7 +2117,7 @@ export namespace TalerBankIntegrationApi {
// URL that the user needs to navigate to in order to
// complete some final confirmation (e.g. 2FA).
//
- // Only applicable when status is selected.
+ // Only applicable when status is selected or pending.
// It may contain withdrawal operation id
confirm_transfer_url?: string;
}
@@ -2150,12 +2192,31 @@ export namespace TalerCorebankApi {
// Default to 'iban' is missing
// @since v4, may become mandatory in the future.
wire_type: string;
+
+ // Wire transfer execution fees.
+ // @since v4, will become mandatory in the next version.
+ wire_transfer_fees?: AmountString;
}
export interface BankAccountCreateWithdrawalRequest {
- // Amount to withdraw.
- amount: AmountString;
+ // Amount to withdraw. If given, the wallet
+ // cannot change the amount.
+ // Optional since **vC2EC**.
+ amount?: AmountString;
+
+ // Suggested amount to withdraw. The wallet can
+ // still change the suggestion.
+ // @since **vC2EC**
+ suggested_amount?: AmountString;
+
+ // The non-Taler card fees the customer will have
+ // to pay to the account owner, bank and/or
+ // payment service provider
+ // they are using to make this withdrawal.
+ // @since **vC2EC**
+ card_fees?: AmountString;
}
+
export interface BankAccountCreateWithdrawalResponse {
// ID of the withdrawal, can be used to view/modify the withdrawal operation.
withdrawal_id: string;
@@ -2498,10 +2559,6 @@ export namespace TalerCorebankApi {
export interface CashoutInfo {
cashout_id: number;
- /**
- * @deprecated since 4, use new 2fa
- */
- status?: "pending" | "aborted" | "confirmed";
}
export interface GlobalCashouts {
// Every string represents a cash-out operation ID.
@@ -4693,17 +4750,6 @@ export namespace TalerMerchantApi {
// user-editable defaults for this template.
// Since protocol **v13**.
editable_defaults?: TemplateContractDetailsDefaults;
-
- // Required currency for payments. Useful if no
- // amount is specified in the template_contract
- // but the user should be required to pay in a
- // particular currency anyway. Merchant backends
- // may reject requests if the template_contract
- // or editable_defaults do
- // specify an amount in a different currency.
- // This parameter is optional.
- // Since protocol **v13**.
- required_currency?: string;
}
export interface TemplateContractDetails {
// Human-readable summary for the template.
@@ -4755,17 +4801,6 @@ export namespace TalerMerchantApi {
// user-editable defaults for this template.
// Since protocol **v13**.
editable_defaults?: TemplateContractDetailsDefaults;
-
- // Required currency for payments. Useful if no
- // amount is specified in the template_contract
- // but the user should be required to pay in a
- // particular currency anyway. Merchant backends
- // may reject requests if the template_contract
- // or editable_defaults do
- // specify an amount in a different currency.
- // This parameter is optional.
- // Since protocol **v13**.
- required_currency?: string;
}
export interface TemplateSummaryResponse {
@@ -4791,17 +4826,6 @@ export namespace TalerMerchantApi {
// user-editable defaults for this template.
// Since protocol **v13**.
editable_defaults?: TemplateContractDetailsDefaults;
-
- // Required currency for payments. Useful if no
- // amount is specified in the template_contract
- // but the user should be required to pay in a
- // particular currency anyway. Merchant backends
- // may reject requests if the template_contract
- // or editable_defaults do
- // specify an amount in a different currency.
- // This parameter is optional.
- // Since protocol **v13**.
- required_currency?: string;
}
export interface TemplateDetails {
@@ -4820,17 +4844,6 @@ export namespace TalerMerchantApi {
// user-editable defaults for this template.
// Since protocol **v13**.
editable_defaults?: TemplateContractDetailsDefaults;
-
- // Required currency for payments. Useful if no
- // amount is specified in the template_contract
- // but the user should be required to pay in a
- // particular currency anyway. Merchant backends
- // may reject requests if the template_contract
- // or editable_defaults do
- // specify an amount in a different currency.
- // This parameter is optional.
- // Since protocol **v13**.
- required_currency?: string;
}
export interface UsingTemplateDetails {
// Summary of the template
diff --git a/packages/taler-util/src/http-impl.qtart.ts b/packages/taler-util/src/http-impl.qtart.ts
index b4e4ebbe7..f60c82fc3 100644
--- a/packages/taler-util/src/http-impl.qtart.ts
+++ b/packages/taler-util/src/http-impl.qtart.ts
@@ -118,7 +118,10 @@ export class HttpLibImpl implements HttpRequestLibrary {
// Just like WHATWG fetch(), the qjs http client doesn't
// really support cancellation, so cancellation here just
// means that the result is ignored!
- const fetchProm = qjsOs.fetchHttp(url, {
+ const {
+ promise: fetchProm,
+ cancelFn
+ } = qjsOs.fetchHttp(url, {
method,
data,
headers: headersList,
@@ -135,6 +138,7 @@ export class HttpLibImpl implements HttpRequestLibrary {
if (opt?.cancellationToken) {
cancelCancelledHandler = opt.cancellationToken.onCancelled(() => {
+ cancelFn();
cancelPromCap.reject(new RequestCancelledError());
});
}
diff --git a/packages/taler-util/src/invariants.ts b/packages/taler-util/src/invariants.ts
index c6e9b8113..113d697c3 100644
--- a/packages/taler-util/src/invariants.ts
+++ b/packages/taler-util/src/invariants.ts
@@ -33,7 +33,7 @@ export class InvariantViolatedError extends Error {
*
* A violation of this invariant means that the database is inconsistent.
*/
-export function checkDbInvariant(b: boolean, m?: string): asserts b {
+export function checkDbInvariant(b: boolean, m: string): asserts b {
if (!b) {
if (m) {
throw Error(`BUG: database invariant failed (${m})`);
diff --git a/packages/taler-util/src/notifications.ts b/packages/taler-util/src/notifications.ts
index d4dfe7589..a8a8c3299 100644
--- a/packages/taler-util/src/notifications.ts
+++ b/packages/taler-util/src/notifications.ts
@@ -128,7 +128,7 @@ export enum ObservabilityEventType {
TaskStart = "task-start",
TaskStop = "task-stop",
TaskReset = "task-reset",
- ShepherdTaskResult = "sheperd-task-result",
+ ShepherdTaskResult = "shepherd-task-result",
DeclareTaskDependency = "declare-task-dependency",
CryptoStart = "crypto-start",
CryptoFinishSuccess = "crypto-finish-success",
diff --git a/packages/taler-util/src/payto.ts b/packages/taler-util/src/payto.ts
index a471d0b87..39c25cffd 100644
--- a/packages/taler-util/src/payto.ts
+++ b/packages/taler-util/src/payto.ts
@@ -15,7 +15,7 @@
*/
import { generateFakeSegwitAddress } from "./bitcoin.js";
-import { Codec, Context, DecodingError, renderContext } from "./codec.js";
+import { Codec, Context, DecodingError, buildCodecForObject, codecForStringURL, renderContext } from "./codec.js";
import { URLSearchParams } from "./url.js";
export type PaytoUri =
@@ -291,3 +291,21 @@ export function talerPaytoFromExchangeReserve(
return `payto://${proto}/${url.host}${url.pathname}${reservePub}`;
}
+
+/**
+ * The account letter is all the information
+ * the merchant backend requires from the
+ * bank account to check transfer.
+ *
+ */
+export type AccountLetter = {
+ accountURI: PaytoString;
+ infoURL: string;
+};
+
+export const codecForAccountLetter =
+ (): Codec<AccountLetter> =>
+ buildCodecForObject<AccountLetter>()
+ .property("infoURL", codecForStringURL(true))
+ .property("accountURI", codecForPaytoString())
+ .build("AccountLetter");
diff --git a/packages/taler-util/src/qtart.ts b/packages/taler-util/src/qtart.ts
index e298a157c..6a5984973 100644
--- a/packages/taler-util/src/qtart.ts
+++ b/packages/taler-util/src/qtart.ts
@@ -17,7 +17,10 @@ export interface QjsHttpOptions {
}
export interface QjsOsLib {
- fetchHttp(url: string, options?: QjsHttpOptions): Promise<QjsHttpResp>;
+ fetchHttp(url: string, options?: QjsHttpOptions): {
+ promise: Promise<QjsHttpResp>,
+ cancelFn: () => number,
+ };
postMessageToHost(s: string): void;
setMessageFromHostHandler(h: (s: string) => void): void;
rename(oldPath: string, newPath: string): number;
diff --git a/packages/taler-util/src/taler-error-codes.ts b/packages/taler-util/src/taler-error-codes.ts
index 9985e74b3..f77357407 100644
--- a/packages/taler-util/src/taler-error-codes.ts
+++ b/packages/taler-util/src/taler-error-codes.ts
@@ -354,7 +354,7 @@ export enum TalerErrorCode {
/**
* The backend could not locate a required template to generate an HTML reply. The system administrator should check if the resource files are installed in the correct location and are readable to the service.
- * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_ACCEPTABLE (406).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_FAILED_TO_LOAD_TEMPLATE = 74,
@@ -1945,7 +1945,7 @@ export enum TalerErrorCode {
/**
- * The payto-URI hash did not match. Hence the request was denied.
+ * The KYC authorization signature was invalid. Hence the request was denied.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
* (A value of 0 indicates that the error is generated client-side).
*/
@@ -2017,6 +2017,22 @@ export enum TalerErrorCode {
/**
+ * The exchange is unaware of the given requirement row.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_CHECK_REQUEST_UNKNOWN = 1939,
+
+
+ /**
+ * The exchange has no account public key to check the KYC authorization signature against. Hence the request was denied. The user should do a wire transfer to the exchange with the KYC authorization key in the subject.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_CHECK_AUTHORIZATION_KEY_UNKNOWN = 1940,
+
+
+ /**
* The exchange does not know a contract under the given contract public key.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
* (A value of 0 indicates that the error is generated client-side).
@@ -2105,6 +2121,14 @@ export enum TalerErrorCode {
/**
+ * The product category is not known to the backend.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_CATEGORY_UNKNOWN = 2003,
+
+
+ /**
* The proposal is not known to the backend.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
* (A value of 0 indicates that the error is generated client-side).
@@ -2561,6 +2585,14 @@ export enum TalerErrorCode {
/**
+ * Invalid token because it was already used, is expired or not yet valid.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_TOKEN_INVALID = 2183,
+
+
+ /**
* The contract hash does not match the given order ID.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
@@ -2921,6 +2953,14 @@ export enum TalerErrorCode {
/**
+ * A token family referenced in this order is either expired or not valid yet.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_NOT_VALID = 2534,
+
+
+ /**
* The exchange says it does not know this transfer.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
* (A value of 0 indicates that the error is generated client-side).
@@ -3057,6 +3097,14 @@ export enum TalerErrorCode {
/**
+ * A category with the same name exists already.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_CATEGORIES_CONFLICT_CATEGORY_EXISTS = 2651,
+
+
+ /**
* The update would have reduced the total amount of product lost, which is not allowed.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
* (A value of 0 indicates that the error is generated client-side).
@@ -3233,6 +3281,22 @@ export enum TalerErrorCode {
/**
+ * The auditor refused the connection due to a lack of authorization.
+ * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ AUDITOR_GENERIC_UNAUTHORIZED = 3001,
+
+
+ /**
+ * This method is not allowed here.
+ * Returned with an HTTP status code of #MHD_HTTP_METHOD_NOT_ALLOWED (405).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ AUDITOR_GENERIC_METHOD_NOT_ALLOWED = 3002,
+
+
+ /**
* The signature from the exchange on the deposit confirmation is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
* (A value of 0 indicates that the error is generated client-side).
@@ -3633,6 +3697,22 @@ export enum TalerErrorCode {
/**
+ * Specified amount will not work for this withdrawal.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_AMOUNT_DIFFERS = 5148,
+
+
+ /**
+ * The backend requires an amount to be specified.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_AMOUNT_REQUIRED = 5149,
+
+
+ /**
* The sync service failed find the account in its database.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
* (A value of 0 indicates that the error is generated client-side).
@@ -4049,6 +4129,14 @@ export enum TalerErrorCode {
/**
+ * A wallet-core request failed because the user needs to first accept the exchange's terms of service.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_EXCHANGE_TOS_NOT_ACCEPTED = 7037,
+
+
+ /**
* We encountered a timeout with our payment backend.
* Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504).
* (A value of 0 indicates that the error is generated client-side).
@@ -4609,6 +4697,62 @@ export enum TalerErrorCode {
/**
+ * The Donau is not aware of the donation unit requested for the operation.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DONAU_GENERIC_DONATION_UNIT_UNKNOWN = 8611,
+
+
+ /**
+ * The Donau failed to talk to the process responsible for its private donation unit keys or the helpers had no donation units (properly) configured.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DONAU_DONATION_UNIT_HELPER_UNAVAILABLE = 8612,
+
+
+ /**
+ * The Donau failed to talk to the process responsible for its private signing keys.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DONAU_SIGNKEY_HELPER_UNAVAILABLE = 8613,
+
+
+ /**
+ * The response from the online signing key helper process was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DONAU_SIGNKEY_HELPER_BUG = 8614,
+
+
+ /**
+ * The number of segments included in the URI does not match the number of segments expected by the endpoint.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DONAU_GENERIC_WRONG_NUMBER_OF_SEGMENTS = 8615,
+
+
+ /**
+ * The signature of the donation receipt is not valid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DONAU_DONATION_RECEIPT_SIGNATURE_INVALID = 8616,
+
+
+ /**
+ * The client re-used a unique donor identifier nonce, which is not allowed.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DONAU_DONOR_IDENTIFIER_NONCE_REUSE = 8617,
+
+
+ /**
* A generic error happened in the LibEuFin nexus. See the enclose details JSON for more information.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
diff --git a/packages/taler-util/src/taler-types.ts b/packages/taler-util/src/taler-types.ts
index e2536b74a..66f98ea9a 100644
--- a/packages/taler-util/src/taler-types.ts
+++ b/packages/taler-util/src/taler-types.ts
@@ -978,7 +978,7 @@ export class WithdrawOperationStatusResponse {
aborted: boolean;
- amount: string;
+ amount: string | undefined;
sender_wire?: string;
@@ -1557,7 +1557,7 @@ export const codecForWithdrawOperationStatusResponse =
.property("selection_done", codecForBoolean())
.property("transfer_done", codecForBoolean())
.property("aborted", codecForBoolean())
- .property("amount", codecForString())
+ .property("amount", codecOptional(codecForString()))
.property("sender_wire", codecOptional(codecForString()))
.property("suggested_exchange", codecOptional(codecForString()))
.property("confirm_transfer_url", codecOptional(codecForString()))
diff --git a/packages/taler-util/src/transactions-types.ts b/packages/taler-util/src/transactions-types.ts
index cee3de9fa..a6ac5aec6 100644
--- a/packages/taler-util/src/transactions-types.ts
+++ b/packages/taler-util/src/transactions-types.ts
@@ -105,8 +105,11 @@ export enum TransactionMajorState {
Done = "done",
Aborting = "aborting",
Aborted = "aborted",
- Suspended = "suspended",
Dialog = "dialog",
+ Finalizing = "finalizing",
+ // Plain suspended is always a suspended pending state.
+ Suspended = "suspended",
+ SuspendedFinalizing = "suspended-finalizing",
SuspendedAborting = "suspended-aborting",
Failed = "failed",
Expired = "expired",
@@ -324,7 +327,7 @@ export interface TransactionWithdrawal extends TransactionCommon {
/**
* Exchange of the withdrawal.
*/
- exchangeBaseUrl: string;
+ exchangeBaseUrl: string | undefined;
/**
* Amount that got subtracted from the reserve balance.
diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts
index d472af187..d23780145 100644
--- a/packages/taler-util/src/wallet-types.ts
+++ b/packages/taler-util/src/wallet-types.ts
@@ -54,7 +54,7 @@ import {
canonicalizeBaseUrl,
} from "./index.js";
import { VersionMatchResult } from "./libtool-version.js";
-import { PaytoUri } from "./payto.js";
+import { PaytoString, PaytoUri, codecForPaytoString } from "./payto.js";
import { AgeCommitmentProof } from "./taler-crypto.js";
import { TalerErrorCode } from "./taler-error-codes.js";
import {
@@ -229,11 +229,13 @@ interface GetPlanForWalletInitiatedOperation {
export interface ConvertAmountRequest {
amount: AmountString;
type: TransactionAmountMode;
+ depositPaytoUri: PaytoString;
}
export const codecForConvertAmountRequest =
buildCodecForObject<ConvertAmountRequest>()
.property("amount", codecForAmountString())
+ .property("depositPaytoUri", codecForPaytoString())
.property(
"type",
codecForEither(
@@ -663,11 +665,11 @@ export interface CoinDumpJson {
withdrawal_reserve_pub: string | undefined;
coin_status: CoinStatus;
spend_allocation:
- | {
- id: string;
- amount: AmountString;
- }
- | undefined;
+ | {
+ id: string;
+ amount: AmountString;
+ }
+ | undefined;
/**
* Information about the age restriction
*/
@@ -801,7 +803,7 @@ export const codecForPreparePayResultPaymentPossible =
)
.build("PreparePayResultPaymentPossible");
-export interface BalanceDetails { }
+export interface BalanceDetails {}
/**
* Detailed reason for why the wallet's balance is insufficient.
@@ -984,9 +986,14 @@ export interface PreparePayResultAlreadyConfirmed {
export interface BankWithdrawDetails {
status: WithdrawalOperationStatus;
- amount: AmountJson;
+ currency: string;
+ amount: AmountJson | undefined;
+ editableAmount: boolean;
+ maxAmount: AmountJson | undefined;
+ wireFee: AmountJson | undefined;
senderWire?: string;
- suggestedExchange?: string;
+ exchange?: string;
+ editableExchange: boolean;
confirmTransferUrl?: string;
wireTypes: string[];
operationId: string;
@@ -1331,6 +1338,7 @@ export enum ExchangeTosStatus {
Pending = "pending",
Proposed = "proposed",
Accepted = "accepted",
+ MissingTos = "missing-tos",
}
export enum ExchangeEntryStatus {
@@ -1846,18 +1854,16 @@ export interface GetWithdrawalDetailsForAmountRequest {
export interface PrepareBankIntegratedWithdrawalRequest {
talerWithdrawUri: string;
- selectedExchange?: string;
}
export const codecForPrepareBankIntegratedWithdrawalRequest =
(): Codec<PrepareBankIntegratedWithdrawalRequest> =>
buildCodecForObject<PrepareBankIntegratedWithdrawalRequest>()
.property("talerWithdrawUri", codecForString())
- .property("selectedExchange", codecOptional(codecForString()))
.build("PrepareBankIntegratedWithdrawalRequest");
export interface PrepareBankIntegratedWithdrawalResponse {
- transactionId?: string;
+ transactionId: TransactionIdStr;
info: WithdrawUriInfoResponse;
}
@@ -1883,6 +1889,13 @@ export interface AcceptBankIntegratedWithdrawalRequest {
talerWithdrawUri: string;
exchangeBaseUrl: string;
forcedDenomSel?: ForcedDenomSel;
+ /**
+ * Amount to withdraw.
+ * If the bank's withdrawal operation uses a fixed amount,
+ * this field must either be left undefined or its value must match
+ * the amount from the withdrawal operation.
+ */
+ amount?: AmountString;
restrictAge?: number;
}
@@ -1892,6 +1905,7 @@ export const codecForAcceptBankIntegratedWithdrawalRequest =
.property("exchangeBaseUrl", codecForCanonBaseUrl())
.property("talerWithdrawUri", codecForString())
.property("forcedDenomSel", codecForAny())
+ .property("amount", codecOptional(codecForAmountString()))
.property("restrictAge", codecOptional(codecForNumber()))
.build("AcceptBankIntegratedWithdrawalRequest");
@@ -2047,7 +2061,7 @@ export interface CheckPayTemplateRequest {
export type CheckPayTemplateReponse = {
templateDetails: TalerMerchantApi.WalletTemplateDetails;
supportedCurrencies: string[];
-}
+};
export const codecForCheckPayTemplateRequest =
(): Codec<CheckPayTemplateRequest> =>
@@ -2352,8 +2366,13 @@ export interface WithdrawUriInfoResponse {
operationId: string;
status: WithdrawalOperationStatus;
confirmTransferUrl?: string;
- amount: AmountString;
+ currency: string;
+ amount: AmountString | undefined;
+ editableAmount: boolean;
+ maxAmount: AmountString | undefined;
+ wireFee: AmountString | undefined;
defaultExchangeBaseUrl?: string;
+ editableExchange: boolean;
possibleExchanges: ExchangeListItem[];
}
@@ -2371,7 +2390,12 @@ export const codecForWithdrawUriInfoResponse =
codecForConstString("confirmed"),
),
)
- .property("amount", codecForAmountString())
+ .property("amount", codecOptional(codecForAmountString()))
+ .property("maxAmount", codecOptional(codecForAmountString()))
+ .property("wireFee", codecOptional(codecForAmountString()))
+ .property("currency", codecForString())
+ .property("editableAmount", codecForBoolean())
+ .property("editableExchange", codecForBoolean())
.property("defaultExchangeBaseUrl", codecOptional(codecForCanonBaseUrl()))
.property("possibleExchanges", codecForList(codecForExchangeListItem()))
.build("WithdrawUriInfoResponse");
diff --git a/packages/taler-wallet-cli/debian/changelog b/packages/taler-wallet-cli/debian/changelog
index e136caa61..5fa99e801 100644
--- a/packages/taler-wallet-cli/debian/changelog
+++ b/packages/taler-wallet-cli/debian/changelog
@@ -1,3 +1,27 @@
+taler-wallet-cli (0.11.4) unstable; urgency=low
+
+ * Release 0.11.4
+
+ -- Florian Dold <dold@taler.net> Mon, 10 Jun 2024 19:57:55 +0200
+
+taler-wallet-cli (0.11.3) unstable; urgency=low
+
+ * Release 0.11.3
+
+ -- Florian Dold <dold@taler.net> Fri, 07 Jun 2024 19:12:44 +0200
+
+taler-wallet-cli (0.11.2) unstable; urgency=low
+
+ * Release 0.11.2
+
+ -- Florian Dold <dold@taler.net> Wed, 05 Jun 2024 20:17:56 +0200
+
+taler-wallet-cli (0.11.1) unstable; urgency=low
+
+ * Release 0.11.1
+
+ -- Florian Dold <dold@taler.net> Mon, 27 May 2024 14:46:35 -0600
+
taler-wallet-cli (0.10.7) unstable; urgency=low
* Release 0.10.7
diff --git a/packages/taler-wallet-cli/package.json b/packages/taler-wallet-cli/package.json
index 922556749..ecc8252e6 100644
--- a/packages/taler-wallet-cli/package.json
+++ b/packages/taler-wallet-cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-wallet-cli",
- "version": "0.10.7",
+ "version": "0.11.4",
"description": "",
"engines": {
"node": ">=0.18.0"
diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts
index a1b008f5e..5bde7db01 100644
--- a/packages/taler-wallet-cli/src/index.ts
+++ b/packages/taler-wallet-cli/src/index.ts
@@ -1231,6 +1231,16 @@ advancedCli
});
advancedCli
+ .subcommand("resetAllRetries", "reset-all-retries", {
+ help: "Reset all retry counters.",
+ })
+ .action(async (args) => {
+ await withWallet(args, { lazyTaskLoop: true }, async (wallet) => {
+ await wallet.client.call(WalletApiOperation.TestingResetAllRetries, {});
+ });
+ });
+
+advancedCli
.subcommand("tasks", "tasks", {
help: "Show active wallet-core tasks.",
})
diff --git a/packages/taler-wallet-core/package.json b/packages/taler-wallet-core/package.json
index 46b3cef4e..c710861d3 100644
--- a/packages/taler-wallet-core/package.json
+++ b/packages/taler-wallet-core/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-wallet-core",
- "version": "0.10.7",
+ "version": "0.11.4",
"description": "",
"engines": {
"node": ">=0.18.0"
diff --git a/packages/taler-wallet-core/src/backup/index.ts b/packages/taler-wallet-core/src/backup/index.ts
index 15904b470..09d5ae75d 100644
--- a/packages/taler-wallet-core/src/backup/index.ts
+++ b/packages/taler-wallet-core/src/backup/index.ts
@@ -805,9 +805,10 @@ async function backupRecoveryTheirs(
let backupStateEntry: ConfigRecord | undefined = await tx.config.get(
ConfigRecordKey.WalletBackupState,
);
- checkDbInvariant(!!backupStateEntry);
+ checkDbInvariant(!!backupStateEntry, `no backup entry`);
checkDbInvariant(
backupStateEntry.key === ConfigRecordKey.WalletBackupState,
+ `backup entry inconsistent`,
);
backupStateEntry.value.lastBackupNonce = undefined;
backupStateEntry.value.lastBackupTimestamp = undefined;
@@ -913,7 +914,10 @@ export async function provideBackupState(
},
);
if (bs) {
- checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState);
+ checkDbInvariant(
+ bs.key === ConfigRecordKey.WalletBackupState,
+ `backup entry inconsistent`,
+ );
return bs.value;
}
// We need to generate the key outside of the transaction
@@ -941,6 +945,7 @@ export async function provideBackupState(
}
checkDbInvariant(
backupStateEntry.key === ConfigRecordKey.WalletBackupState,
+ `backup entry inconsistent`,
);
return backupStateEntry.value;
});
@@ -952,7 +957,10 @@ export async function getWalletBackupState(
): Promise<WalletBackupConfState> {
const bs = await tx.config.get(ConfigRecordKey.WalletBackupState);
checkDbInvariant(!!bs, "wallet backup state should be in DB");
- checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState);
+ checkDbInvariant(
+ bs.key === ConfigRecordKey.WalletBackupState,
+ `backup entry inconsistent`,
+ );
return bs.value;
}
@@ -962,7 +970,7 @@ export async function setWalletDeviceId(
): Promise<void> {
await provideBackupState(wex);
await wex.db.runReadWriteTx({ storeNames: ["config"] }, async (tx) => {
- let backupStateEntry: ConfigRecord | undefined = await tx.config.get(
+ const backupStateEntry: ConfigRecord | undefined = await tx.config.get(
ConfigRecordKey.WalletBackupState,
);
if (
diff --git a/packages/taler-wallet-core/src/balance.ts b/packages/taler-wallet-core/src/balance.ts
index 76e604324..381028906 100644
--- a/packages/taler-wallet-core/src/balance.ts
+++ b/packages/taler-wallet-core/src/balance.ts
@@ -69,8 +69,8 @@ import { ExchangeRestrictionSpec, findMatchingWire } from "./coinSelection.js";
import {
DepositOperationStatus,
ExchangeEntryDbRecordStatus,
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
PeerPushDebitStatus,
RefreshGroupRecord,
RefreshOperationStatus,
@@ -304,8 +304,8 @@ export async function getBalancesInsideTransaction(
const balanceStore: BalancesStore = new BalancesStore(wex, tx);
const keyRangeActive = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
await tx.exchanges.iter().forEachAsync(async (ex) => {
@@ -379,6 +379,10 @@ export async function getBalancesInsideTransaction(
wg.denomsSel !== undefined,
"wg in kyc state should have been initialized",
);
+ checkDbInvariant(
+ wg.exchangeBaseUrl !== undefined,
+ "wg in kyc state should have been initialized",
+ );
const currency = Amounts.currencyOf(wg.denomsSel.totalCoinValue);
await balanceStore.setFlagIncomingKyc(currency, wg.exchangeBaseUrl);
break;
@@ -389,6 +393,10 @@ export async function getBalancesInsideTransaction(
wg.denomsSel !== undefined,
"wg in aml state should have been initialized",
);
+ checkDbInvariant(
+ wg.exchangeBaseUrl !== undefined,
+ "wg in kyc state should have been initialized",
+ );
const currency = Amounts.currencyOf(wg.denomsSel.totalCoinValue);
await balanceStore.setFlagIncomingAml(currency, wg.exchangeBaseUrl);
break;
@@ -408,6 +416,10 @@ export async function getBalancesInsideTransaction(
wg.denomsSel !== undefined,
"wg in confirmed state should have been initialized",
);
+ checkDbInvariant(
+ wg.exchangeBaseUrl !== undefined,
+ "wg in kyc state should have been initialized",
+ );
const currency = Amounts.currencyOf(wg.denomsSel.totalCoinValue);
await balanceStore.setFlagIncomingConfirmation(
currency,
diff --git a/packages/taler-wallet-core/src/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts
index a60e41ecd..db6384c93 100644
--- a/packages/taler-wallet-core/src/coinSelection.ts
+++ b/packages/taler-wallet-core/src/coinSelection.ts
@@ -691,7 +691,7 @@ export function checkAccountRestriction(
switch (myRestriction.type) {
case "deny":
return { ok: false };
- case "regex":
+ case "regex": {
const regex = new RegExp(myRestriction.payto_regex);
if (!regex.test(paytoUri)) {
return {
@@ -700,6 +700,7 @@ export function checkAccountRestriction(
hintI18n: myRestriction.human_hint_i18n,
};
}
+ }
}
}
return {
@@ -909,7 +910,7 @@ async function selectPayCandidates(
coinAvail.exchangeBaseUrl,
coinAvail.denomPubHash,
]);
- checkDbInvariant(!!denom);
+ checkDbInvariant(!!denom, `denomination of a coin is missing hash: ${coinAvail.denomPubHash}`);
if (denom.isRevoked) {
logger.trace("denom is revoked");
continue;
diff --git a/packages/taler-wallet-core/src/common.ts b/packages/taler-wallet-core/src/common.ts
index edaba5ba4..13c875575 100644
--- a/packages/taler-wallet-core/src/common.ts
+++ b/packages/taler-wallet-core/src/common.ts
@@ -31,6 +31,8 @@ import {
ExchangeUpdateStatus,
Logger,
RefreshReason,
+ TalerError,
+ TalerErrorCode,
TalerErrorDetail,
TalerPreciseTimestamp,
TalerProtocolTimestamp,
@@ -57,11 +59,11 @@ import {
PurchaseRecord,
RecoupGroupRecord,
RefreshGroupRecord,
- RewardRecord,
WalletDbReadWriteTransaction,
WithdrawalGroupRecord,
timestampPreciseToDb,
} from "./db.js";
+import { ReadyExchangeSummary } from "./exchanges.js";
import { createRefreshGroup } from "./refresh.js";
import { WalletExecutionContext, getDenomInfo } from "./wallet.js";
@@ -121,7 +123,10 @@ export async function makeCoinAvailable(
coinRecord.exchangeBaseUrl,
coinRecord.denomPubHash,
]);
- checkDbInvariant(!!denom);
+ checkDbInvariant(
+ !!denom,
+ `denomination of a coin is missing hash: ${coinRecord.denomPubHash}`,
+ );
const ageRestriction = coinRecord.maxAge;
let car = await tx.coinAvailability.get([
coinRecord.exchangeBaseUrl,
@@ -175,13 +180,19 @@ export async function spendCoins(
coin.exchangeBaseUrl,
coin.denomPubHash,
);
- checkDbInvariant(!!denom);
+ checkDbInvariant(
+ !!denom,
+ `denomination of a coin is missing hash: ${coin.denomPubHash}`,
+ );
const coinAvailability = await tx.coinAvailability.get([
coin.exchangeBaseUrl,
coin.denomPubHash,
coin.maxAge,
]);
- checkDbInvariant(!!coinAvailability);
+ checkDbInvariant(
+ !!coinAvailability,
+ `age denom info is missing for ${coin.maxAge}`,
+ );
const contrib = csi.contributions[i];
if (coin.status !== CoinStatus.Fresh) {
const alloc = coin.spendAllocation;
@@ -213,7 +224,6 @@ export async function spendCoins(
amount: Amounts.stringify(remaining.amount),
coinPub: coin.coinPub,
});
- checkDbInvariant(!!coinAvailability);
if (coinAvailability.freshCoinCount === 0) {
throw Error(
`invalid coin count ${coinAvailability.freshCoinCount} in DB`,
@@ -258,6 +268,9 @@ export enum TombstoneTag {
export function getExchangeTosStatusFromRecord(
exchange: ExchangeEntryRecord,
): ExchangeTosStatus {
+ if (exchange.tosCurrentEtag == null) {
+ return ExchangeTosStatus.MissingTos;
+ }
if (!exchange.tosAcceptedEtag) {
return ExchangeTosStatus.Proposed;
}
@@ -558,6 +571,28 @@ export function getAutoRefreshExecuteThreshold(d: {
}
/**
+ * Type and schema definitions for pending tasks in the wallet.
+ *
+ * These are only used internally, and are not part of the stable public
+ * interface to the wallet.
+ */
+
+export enum PendingTaskType {
+ ExchangeUpdate = "exchange-update",
+ Purchase = "purchase",
+ Refresh = "refresh",
+ Recoup = "recoup",
+ RewardPickup = "reward-pickup",
+ Withdraw = "withdraw",
+ Deposit = "deposit",
+ Backup = "backup",
+ PeerPushDebit = "peer-push-debit",
+ PeerPullCredit = "peer-pull-credit",
+ PeerPushCredit = "peer-push-credit",
+ PeerPullDebit = "peer-pull-debit",
+}
+
+/**
* Parsed representation of task identifiers.
*/
export type ParsedTaskIdentifier =
@@ -660,9 +695,6 @@ export namespace TaskIdentifiers {
exchBaseUrl,
)}` as TaskIdStr;
}
- export function forTipPickup(tipRecord: RewardRecord): TaskIdStr {
- return `${PendingTaskType.RewardPickup}:${tipRecord.walletRewardId}` as TaskIdStr;
- }
export function forRefresh(
refreshGroupRecord: RefreshGroupRecord,
): TaskIdStr {
@@ -747,28 +779,6 @@ export interface TransactionContext {
deleteTransaction(): Promise<void>;
}
-/**
- * Type and schema definitions for pending tasks in the wallet.
- *
- * These are only used internally, and are not part of the stable public
- * interface to the wallet.
- */
-
-export enum PendingTaskType {
- ExchangeUpdate = "exchange-update",
- Purchase = "purchase",
- Refresh = "refresh",
- Recoup = "recoup",
- RewardPickup = "reward-pickup",
- Withdraw = "withdraw",
- Deposit = "deposit",
- Backup = "backup",
- PeerPushDebit = "peer-push-debit",
- PeerPullCredit = "peer-pull-credit",
- PeerPushCredit = "peer-push-credit",
- PeerPullDebit = "peer-pull-debit",
-}
-
declare const __taskIdStr: unique symbol;
export type TaskIdStr = string & { [__taskIdStr]: true };
@@ -799,7 +809,7 @@ export async function genericWaitForState(
flag.raise();
}
});
- const unregisterOnCancelled = wex.cancellationToken.onCancelled(() => {
+ const unregisterOnCancelled = wex.cancellationToken.onCancelled((reason) => {
cancelNotif();
flag.raise();
});
@@ -819,5 +829,25 @@ export async function genericWaitForState(
} catch (e) {
unregisterOnCancelled();
cancelNotif();
+ throw e;
+ }
+}
+
+export function requireExchangeTosAcceptedOrThrow(
+ exchange: ReadyExchangeSummary,
+): void {
+ switch (exchange.tosStatus) {
+ case ExchangeTosStatus.Accepted:
+ case ExchangeTosStatus.MissingTos:
+ break;
+ default:
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_EXCHANGE_TOS_NOT_ACCEPTED,
+ {
+ exchangeBaseUrl: exchange.exchangeBaseUrl,
+ currentEtag: exchange.tosCurrentEtag,
+ tosStatus: exchange.tosStatus,
+ },
+ );
}
}
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index 44c241aed..5c381eea7 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -248,6 +248,9 @@ export function timestampOptionalAbsoluteFromDb(
* 0x0103_nnnn: aborting
* 0x0110_nnnn: suspended
* 0x0113_nnnn: suspended-aborting
+ * a=2: finalizing
+ * 0x0200_nnnn: finalizing
+ * 0x0210_nnnn: suspended-finalizing
* a=5: final
* 0x0500_nnnn: done
* 0x0501_nnnn: failed
@@ -260,12 +263,12 @@ export function timestampOptionalAbsoluteFromDb(
/**
* First possible operation status in the active range (inclusive).
*/
-export const OPERATION_STATUS_ACTIVE_FIRST = 0x0100_0000;
+export const OPERATION_STATUS_NONFINAL_FIRST = 0x0100_0000;
/**
* LAST possible operation status in the active range (inclusive).
*/
-export const OPERATION_STATUS_ACTIVE_LAST = 0x0113_ffff;
+export const OPERATION_STATUS_NONFINAL_LAST = 0x0210_ffff;
/**
* Status of a withdrawal.
@@ -395,6 +398,8 @@ export interface ReserveBankInfo {
timestampBankConfirmed: DbPreciseTimestamp | undefined;
wireTypes: string[] | undefined;
+
+ currency: string | undefined;
}
/**
@@ -918,92 +923,6 @@ export interface CoinAllocation {
amount: AmountString;
}
-/**
- * Status of a reward we got from a merchant.
- */
-export interface RewardRecord {
- /**
- * Has the user accepted the tip? Only after the tip has been accepted coins
- * withdrawn from the tip may be used.
- */
- acceptedTimestamp: DbPreciseTimestamp | undefined;
-
- /**
- * The tipped amount.
- */
- rewardAmountRaw: AmountString;
-
- /**
- * Effect on the balance (including fees etc).
- */
- rewardAmountEffective: AmountString;
-
- /**
- * Timestamp, the tip can't be picked up anymore after this deadline.
- */
- rewardExpiration: DbProtocolTimestamp;
-
- /**
- * The exchange that will sign our coins, chosen by the merchant.
- */
- exchangeBaseUrl: string;
-
- /**
- * Base URL of the merchant that is giving us the tip.
- */
- merchantBaseUrl: string;
-
- /**
- * Denomination selection made by the wallet for picking up
- * this tip.
- *
- * FIXME: Put this into some DenomSelectionCacheRecord instead of
- * storing it here!
- */
- denomsSel: DenomSelectionState;
-
- denomSelUid: string;
-
- /**
- * Tip ID chosen by the wallet.
- */
- walletRewardId: string;
-
- /**
- * Secret seed used to derive planchets for this tip.
- */
- secretSeed: string;
-
- /**
- * The merchant's identifier for this reward.
- */
- merchantRewardId: string;
-
- createdTimestamp: DbPreciseTimestamp;
-
- /**
- * The url to be redirected after the tip is accepted.
- */
- next_url: string | undefined;
-
- /**
- * Timestamp for when the wallet finished picking up the tip
- * from the merchant.
- */
- pickedUpTimestamp: DbPreciseTimestamp | undefined;
-
- status: RewardRecordStatus;
-}
-
-export enum RewardRecordStatus {
- PendingPickup = 0x0100_0000,
- SuspendedPickup = 0x0110_0000,
- DialogAccept = 0x0101_0000,
- Done = 0x0500_0000,
- Aborted = 0x0500_0000,
- Failed = 0x0501_000,
-}
-
export enum RefreshCoinStatus {
Pending = 0x0100_0000,
Finished = 0x0500_0000,
@@ -1178,10 +1097,15 @@ export enum PurchaseStatus {
/**
* Query for refund (until auto-refund deadline is reached).
+ *
+ * Legacy state for compatibility.
*/
PendingQueryingAutoRefund = 0x0100_0004,
SuspendedQueryingAutoRefund = 0x0110_0004,
+ FinalizingQueryingAutoRefund = 0x0200_0001,
+ SuspendedFinalizingQueryingAutoRefund = 0x0210_0001,
+
PendingAcceptRefund = 0x0100_0005,
SuspendedPendingAcceptRefund = 0x0110_0005,
@@ -1197,11 +1121,6 @@ export enum PurchaseStatus {
DialogShared = 0x0101_0001,
/**
- * The user has rejected the proposal.
- */
- AbortedProposalRefused = 0x0503_0000,
-
- /**
* Downloading or processing the proposal has failed permanently.
*/
FailedClaim = 0x0501_0000,
@@ -1224,13 +1143,18 @@ export enum PurchaseStatus {
DoneRepurchaseDetected = 0x0500_0001,
/**
- * The payment has been aborted.
+ * The user has rejected the proposal.
*/
- AbortedIncompletePayment = 0x0503_0000,
+ AbortedProposalRefused = 0x0503_0000,
AbortedRefunded = 0x0503_0001,
AbortedOrderDeleted = 0x0503_0002,
+
+ /**
+ * The payment has been aborted.
+ */
+ AbortedIncompletePayment = 0x0503_0003,
}
/**
@@ -1439,6 +1363,7 @@ export interface WgInfoBankIntegrated {
* a Taler-integrated bank.
*/
bankInfo: ReserveBankInfo;
+
/**
* Info about withdrawal accounts, possibly including currency conversion.
*/
@@ -1530,7 +1455,7 @@ export interface WithdrawalGroupRecord {
* The exchange base URL that we're withdrawing from.
* (Redundantly stored, as the reserve record also has this info.)
*/
- exchangeBaseUrl: string;
+ exchangeBaseUrl?: string;
/**
* When was the withdrawal operation started started?
@@ -1976,7 +1901,7 @@ export enum PeerPullPaymentCreditStatus {
SuspendedCreatePurse = 0x0110_0000,
SuspendedReady = 0x0110_0001,
SuspendedMergeKycRequired = 0x0110_0002,
- SuspendedWithdrawing = 0x0110_0000,
+ SuspendedWithdrawing = 0x0110_0003,
SuspendedAbortingDeletePurse = 0x0113_0000,
@@ -2630,9 +2555,10 @@ export const WalletStoresV1 = {
]),
},
),
+ // Just a tombstone at this point.
rewards: describeStore(
"rewards",
- describeContents<RewardRecord>({ keyPath: "walletRewardId" }),
+ describeContents<any>({ keyPath: "walletRewardId" }),
{
byMerchantTipIdAndBaseUrl: describeIndex("byMerchantRewardIdAndBaseUrl", [
"merchantRewardId",
@@ -2940,6 +2866,8 @@ export interface DbDump {
};
}
+const logger = new Logger("db.ts");
+
export async function exportSingleDb(
idb: IDBFactory,
dbName: string,
@@ -3081,8 +3009,6 @@ export interface FixupDescription {
*/
export const walletDbFixups: FixupDescription[] = [];
-const logger = new Logger("db.ts");
-
export async function applyFixups(
db: DbAccess<typeof WalletStoresV1>,
): Promise<void> {
diff --git a/packages/taler-wallet-core/src/dbless.ts b/packages/taler-wallet-core/src/dbless.ts
index d3085ecb4..ec9655e6f 100644
--- a/packages/taler-wallet-core/src/dbless.ts
+++ b/packages/taler-wallet-core/src/dbless.ts
@@ -123,7 +123,7 @@ export async function topupReserveWithBank(args: TopupReserveWithBankArgs) {
);
const bankInfo = await getBankWithdrawalInfo(http, wopi.taler_withdraw_uri);
const bankStatusUrl = getBankStatusUrl(wopi.taler_withdraw_uri);
- if (!bankInfo.suggestedExchange) {
+ if (!bankInfo.exchange) {
throw Error("no suggested exchange");
}
const plainPaytoUris =
diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts
index c4cd98d73..2004c12cb 100644
--- a/packages/taler-wallet-core/src/deposits.ts
+++ b/packages/taler-wallet-core/src/deposits.ts
@@ -387,11 +387,19 @@ export function computeDepositTransactionActions(
case DepositOperationStatus.Finished:
return [TransactionAction.Delete];
case DepositOperationStatus.PendingDeposit:
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case DepositOperationStatus.SuspendedDeposit:
return [TransactionAction.Resume];
case DepositOperationStatus.Aborting:
- return [TransactionAction.Fail, TransactionAction.Suspend];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Fail,
+ TransactionAction.Suspend,
+ ];
case DepositOperationStatus.Aborted:
return [TransactionAction.Delete];
case DepositOperationStatus.Failed:
@@ -399,9 +407,17 @@ export function computeDepositTransactionActions(
case DepositOperationStatus.SuspendedAborting:
return [TransactionAction.Resume, TransactionAction.Fail];
case DepositOperationStatus.PendingKyc:
- return [TransactionAction.Suspend, TransactionAction.Fail];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Fail,
+ ];
case DepositOperationStatus.PendingTrack:
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case DepositOperationStatus.SuspendedKyc:
return [TransactionAction.Resume, TransactionAction.Fail];
case DepositOperationStatus.SuspendedTrack:
@@ -441,7 +457,7 @@ async function refundDepositGroup(
{ storeNames: ["coins"] },
async (tx) => {
const coinRecord = await tx.coins.get(coinPub);
- checkDbInvariant(!!coinRecord);
+ checkDbInvariant(!!coinRecord, `coin ${coinPub} not found in DB`);
return coinRecord.exchangeBaseUrl;
},
);
diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts
index d8063d561..dd88fa836 100644
--- a/packages/taler-wallet-core/src/exchanges.ts
+++ b/packages/taler-wallet-core/src/exchanges.ts
@@ -28,7 +28,6 @@ import {
AgeRestriction,
Amount,
Amounts,
- AsyncFlag,
CancellationToken,
CoinRefreshRequest,
CoinStatus,
@@ -53,6 +52,7 @@ import {
GetExchangeResourcesResponse,
GetExchangeTosResult,
GlobalFees,
+ HttpStatusCode,
LibtoolVersion,
Logger,
NotificationType,
@@ -79,6 +79,7 @@ import {
WireInfo,
assertUnreachable,
checkDbInvariant,
+ checkLogicInvariant,
codecForExchangeKeysJson,
durationMul,
encodeCrock,
@@ -93,6 +94,8 @@ import {
getExpiry,
readSuccessResponseJsonOrThrow,
readSuccessResponseTextOrThrow,
+ readTalerErrorResponse,
+ throwUnexpectedRequestError,
} from "@gnu-taler/taler-util/http";
import {
PendingTaskType,
@@ -103,6 +106,7 @@ import {
TransactionContext,
computeDbBackoff,
constructTaskIdentifier,
+ genericWaitForState,
getAutoRefreshExecuteThreshold,
getExchangeEntryStatusFromRecord,
getExchangeState,
@@ -861,6 +865,41 @@ async function downloadExchangeKeysInfo(
};
}
+type TosMetaResult = { type: "not-found" } | { type: "ok"; etag: string };
+
+/**
+ * Download metadata about an exchange's terms of service.
+ */
+async function downloadTosMeta(
+ wex: WalletExecutionContext,
+ exchangeBaseUrl: string,
+): Promise<TosMetaResult> {
+ logger.trace(`downloading exchange tos metadata for ${exchangeBaseUrl}`);
+ const reqUrl = new URL("terms", exchangeBaseUrl);
+
+ // FIXME: We can/should make a HEAD request here.
+ // Not sure if qtart supports it at the moment.
+ const resp = await wex.http.fetch(reqUrl.href, {
+ cancellationToken: wex.cancellationToken,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NotFound:
+ case HttpStatusCode.NotImplemented:
+ return { type: "not-found" };
+ case HttpStatusCode.Ok:
+ break;
+ default:
+ throwUnexpectedRequestError(resp, await readTalerErrorResponse(resp));
+ }
+
+ const etag = resp.headers.get("etag") || "unknown";
+ return {
+ type: "ok",
+ etag,
+ };
+}
+
async function downloadTosFromAcceptedFormat(
wex: WalletExecutionContext,
baseUrl: string,
@@ -977,9 +1016,7 @@ async function startUpdateExchangeEntry(
wex.ws.exchangeCache.clear();
await tx.exchanges.put(r);
const newExchangeState = getExchangeState(r);
- // Reset retries for updating the exchange entry.
const taskId = TaskIdentifiers.forExchangeUpdate(r);
- await tx.operationRetries.delete(taskId);
return { oldExchangeState, newExchangeState, taskId };
},
);
@@ -989,6 +1026,8 @@ async function startUpdateExchangeEntry(
newExchangeState: newExchangeState,
oldExchangeState: oldExchangeState,
});
+ logger.info(`start update ${exchangeBaseUrl} task ${taskId}`);
+
await wex.taskScheduler.resetTaskRetries(taskId);
}
@@ -1008,132 +1047,6 @@ export interface ReadyExchangeSummary {
scopeInfo: ScopeInfo;
}
-async function internalWaitReadyExchange(
- wex: WalletExecutionContext,
- canonUrl: string,
- exchangeNotifFlag: AsyncFlag,
- options: {
- cancellationToken?: CancellationToken;
- forceUpdate?: boolean;
- expectedMasterPub?: string;
- } = {},
-): Promise<ReadyExchangeSummary> {
- const operationId = constructTaskIdentifier({
- tag: PendingTaskType.ExchangeUpdate,
- exchangeBaseUrl: canonUrl,
- });
- while (true) {
- if (wex.cancellationToken.isCancelled) {
- throw Error("cancelled");
- }
- logger.info(`waiting for ready exchange ${canonUrl}`);
- const { exchange, exchangeDetails, retryInfo, scopeInfo } =
- await wex.db.runReadOnlyTx(
- {
- storeNames: [
- "exchanges",
- "exchangeDetails",
- "operationRetries",
- "globalCurrencyAuditors",
- "globalCurrencyExchanges",
- ],
- },
- async (tx) => {
- const exchange = await tx.exchanges.get(canonUrl);
- const exchangeDetails = await getExchangeRecordsInternal(
- tx,
- canonUrl,
- );
- const retryInfo = await tx.operationRetries.get(operationId);
- let scopeInfo: ScopeInfo | undefined = undefined;
- if (exchange && exchangeDetails) {
- scopeInfo = await internalGetExchangeScopeInfo(tx, exchangeDetails);
- }
- return { exchange, exchangeDetails, retryInfo, scopeInfo };
- },
- );
-
- if (!exchange) {
- throw Error("exchange entry does not exist anymore");
- }
-
- let ready = false;
-
- switch (exchange.updateStatus) {
- case ExchangeEntryDbUpdateStatus.Ready:
- ready = true;
- break;
- case ExchangeEntryDbUpdateStatus.ReadyUpdate:
- // If the update is forced,
- // we wait until we're in a full "ready" state,
- // as we're not happy with the stale information.
- if (!options.forceUpdate) {
- ready = true;
- }
- break;
- case ExchangeEntryDbUpdateStatus.UnavailableUpdate:
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_EXCHANGE_UNAVAILABLE,
- {
- exchangeBaseUrl: canonUrl,
- innerError: retryInfo?.lastError,
- },
- );
- default: {
- if (retryInfo) {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_EXCHANGE_UNAVAILABLE,
- {
- exchangeBaseUrl: canonUrl,
- innerError: retryInfo?.lastError,
- },
- );
- }
- }
- }
-
- if (!ready) {
- logger.info("waiting for exchange update notification");
- await exchangeNotifFlag.wait();
- logger.info("done waiting for exchange update notification");
- exchangeNotifFlag.reset();
- continue;
- }
-
- if (!exchangeDetails) {
- throw Error("invariant failed");
- }
-
- if (!scopeInfo) {
- throw Error("invariant failed");
- }
-
- const res: ReadyExchangeSummary = {
- currency: exchangeDetails.currency,
- exchangeBaseUrl: canonUrl,
- masterPub: exchangeDetails.masterPublicKey,
- tosStatus: getExchangeTosStatusFromRecord(exchange),
- tosAcceptedEtag: exchange.tosAcceptedEtag,
- wireInfo: exchangeDetails.wireInfo,
- protocolVersionRange: exchangeDetails.protocolVersionRange,
- tosCurrentEtag: exchange.tosCurrentEtag,
- tosAcceptedTimestamp: timestampOptionalPreciseFromDb(
- exchange.tosAcceptedTimestamp,
- ),
- scopeInfo,
- };
-
- if (options.expectedMasterPub) {
- if (res.masterPub !== options.expectedMasterPub) {
- throw Error(
- "public key of the exchange does not match expected public key",
- );
- }
- }
- return res;
- }
-}
-
/**
* Ensure that a fresh exchange entry exists for the given
* exchange base URL.
@@ -1155,6 +1068,8 @@ export async function fetchFreshExchange(
forceUpdate?: boolean;
} = {},
): Promise<ReadyExchangeSummary> {
+ logger.info(`fetch fresh ${baseUrl} forced ${options.forceUpdate}`);
+
if (!options.forceUpdate) {
const cachedResp = wex.ws.exchangeCache.get(baseUrl);
if (cachedResp) {
@@ -1184,39 +1099,131 @@ async function waitReadyExchange(
} = {},
): Promise<ReadyExchangeSummary> {
logger.trace(`waiting for exchange ${canonUrl} to become ready`);
- // FIXME: We should use Symbol.dispose magic here for cleanup!
- const exchangeNotifFlag = new AsyncFlag();
- // Raise exchangeNotifFlag whenever we get a notification
- // about our exchange.
- const cancelNotif = wex.ws.addNotificationListener((notif) => {
- if (
- notif.type === NotificationType.ExchangeStateTransition &&
- notif.exchangeBaseUrl === canonUrl
- ) {
- logger.info(`raising update notification: ${j2s(notif)}`);
- exchangeNotifFlag.raise();
- }
+ const operationId = constructTaskIdentifier({
+ tag: PendingTaskType.ExchangeUpdate,
+ exchangeBaseUrl: canonUrl,
});
- const unregisterOnCancelled = wex.cancellationToken.onCancelled(() => {
- cancelNotif();
- exchangeNotifFlag.raise();
+ let res: ReadyExchangeSummary | undefined = undefined;
+
+ await genericWaitForState(wex, {
+ filterNotification(notif): boolean {
+ return (
+ notif.type === NotificationType.ExchangeStateTransition &&
+ notif.exchangeBaseUrl === canonUrl
+ );
+ },
+ async checkState(): Promise<boolean> {
+ const { exchange, exchangeDetails, retryInfo, scopeInfo } =
+ await wex.db.runReadOnlyTx(
+ {
+ storeNames: [
+ "exchanges",
+ "exchangeDetails",
+ "operationRetries",
+ "globalCurrencyAuditors",
+ "globalCurrencyExchanges",
+ ],
+ },
+ async (tx) => {
+ const exchange = await tx.exchanges.get(canonUrl);
+ const exchangeDetails = await getExchangeRecordsInternal(
+ tx,
+ canonUrl,
+ );
+ const retryInfo = await tx.operationRetries.get(operationId);
+ let scopeInfo: ScopeInfo | undefined = undefined;
+ if (exchange && exchangeDetails) {
+ scopeInfo = await internalGetExchangeScopeInfo(
+ tx,
+ exchangeDetails,
+ );
+ }
+ return { exchange, exchangeDetails, retryInfo, scopeInfo };
+ },
+ );
+
+ if (!exchange) {
+ throw Error("exchange entry does not exist anymore");
+ }
+
+ let ready = false;
+
+ switch (exchange.updateStatus) {
+ case ExchangeEntryDbUpdateStatus.Ready:
+ ready = true;
+ break;
+ case ExchangeEntryDbUpdateStatus.ReadyUpdate:
+ // If the update is forced,
+ // we wait until we're in a full "ready" state,
+ // as we're not happy with the stale information.
+ if (!options.forceUpdate) {
+ ready = true;
+ }
+ break;
+ case ExchangeEntryDbUpdateStatus.UnavailableUpdate:
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_EXCHANGE_UNAVAILABLE,
+ {
+ exchangeBaseUrl: canonUrl,
+ innerError: retryInfo?.lastError,
+ },
+ );
+ default: {
+ if (retryInfo) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_EXCHANGE_UNAVAILABLE,
+ {
+ exchangeBaseUrl: canonUrl,
+ innerError: retryInfo?.lastError,
+ },
+ );
+ }
+ }
+ }
+
+ if (!ready) {
+ return false;
+ }
+
+ if (!exchangeDetails) {
+ throw Error("invariant failed");
+ }
+
+ if (!scopeInfo) {
+ throw Error("invariant failed");
+ }
+
+ const mySummary: ReadyExchangeSummary = {
+ currency: exchangeDetails.currency,
+ exchangeBaseUrl: canonUrl,
+ masterPub: exchangeDetails.masterPublicKey,
+ tosStatus: getExchangeTosStatusFromRecord(exchange),
+ tosAcceptedEtag: exchange.tosAcceptedEtag,
+ wireInfo: exchangeDetails.wireInfo,
+ protocolVersionRange: exchangeDetails.protocolVersionRange,
+ tosCurrentEtag: exchange.tosCurrentEtag,
+ tosAcceptedTimestamp: timestampOptionalPreciseFromDb(
+ exchange.tosAcceptedTimestamp,
+ ),
+ scopeInfo,
+ };
+
+ if (options.expectedMasterPub) {
+ if (mySummary.masterPub !== options.expectedMasterPub) {
+ throw Error(
+ "public key of the exchange does not match expected public key",
+ );
+ }
+ }
+ res = mySummary;
+ return true;
+ },
});
- try {
- const res = await internalWaitReadyExchange(
- wex,
- canonUrl,
- exchangeNotifFlag,
- options,
- );
- logger.info("done waiting for ready exchange");
- return res;
- } finally {
- unregisterOnCancelled();
- cancelNotif();
- }
+ checkLogicInvariant(!!res);
+ return res;
}
function checkPeerPaymentsDisabled(
@@ -1359,7 +1366,6 @@ export async function updateExchangeFromUrlHandler(
);
refreshCheckNecessary = false;
}
-
if (!(updateNecessary || refreshCheckNecessary)) {
logger.trace("update not necessary, running again later");
return TaskRunResult.runAgainAt(
@@ -1421,15 +1427,7 @@ export async function updateExchangeFromUrlHandler(
logger.trace("finished validating exchange /wire info");
- // We download the text/plain version here,
- // because that one needs to exist, and we
- // will get the current etag from the response.
- const tosDownload = await downloadTosFromAcceptedFormat(
- wex,
- exchangeBaseUrl,
- timeout,
- ["text/plain"],
- );
+ const tosMeta = await downloadTosMeta(wex, exchangeBaseUrl);
logger.trace("updating exchange info in database");
@@ -1522,7 +1520,14 @@ export async function updateExchangeFromUrlHandler(
};
r.noFees = noFees;
r.peerPaymentsDisabled = peerPaymentsDisabled;
- r.tosCurrentEtag = tosDownload.tosEtag;
+ switch (tosMeta.type) {
+ case "not-found":
+ r.tosCurrentEtag = undefined;
+ break;
+ case "ok":
+ r.tosCurrentEtag = tosMeta.etag;
+ break;
+ }
if (existingDetails?.rowId) {
newDetails.rowId = existingDetails.rowId;
}
@@ -1548,7 +1553,10 @@ export async function updateExchangeFromUrlHandler(
r.cachebreakNextUpdate = false;
await tx.exchanges.put(r);
const drRowId = await tx.exchangeDetails.put(newDetails);
- checkDbInvariant(typeof drRowId.key === "number");
+ checkDbInvariant(
+ typeof drRowId.key === "number",
+ "exchange details key is not a number",
+ );
for (const sk of keysInfo.signingKeys) {
// FIXME: validate signing keys before inserting them
@@ -2227,10 +2235,12 @@ export async function markExchangeUsed(
logger.info(`marking exchange ${exchangeBaseUrl} as used`);
const exch = await tx.exchanges.get(exchangeBaseUrl);
if (!exch) {
+ logger.info(`exchange ${exchangeBaseUrl} NOT found`);
return {
notif: undefined,
};
}
+
const oldExchangeState = getExchangeState(exch);
switch (exch.entryStatus) {
case ExchangeEntryDbRecordStatus.Ephemeral:
diff --git a/packages/taler-wallet-core/src/instructedAmountConversion.ts b/packages/taler-wallet-core/src/instructedAmountConversion.ts
index 1f7d95959..5b399a0a7 100644
--- a/packages/taler-wallet-core/src/instructedAmountConversion.ts
+++ b/packages/taler-wallet-core/src/instructedAmountConversion.ts
@@ -283,7 +283,7 @@ async function getAvailableDenoms(
coinAvail.exchangeBaseUrl,
coinAvail.denomPubHash,
]);
- checkDbInvariant(!!denom);
+ checkDbInvariant(!!denom, `denomination of a coin is missing hash: ${coinAvail.denomPubHash}`);
if (denom.isRevoked || !denom.isOffered) {
continue;
}
@@ -472,7 +472,7 @@ export async function getMaxDepositAmount(
export function getMaxDepositAmountForAvailableCoins(
denoms: AvailableCoins,
currency: string,
-) {
+): AmountWithFee {
const zero = Amounts.zeroOfCurrency(currency);
if (!denoms.list.length) {
// no coins in the database
@@ -663,8 +663,13 @@ function rankDenominationForWithdrawals(
//different exchanges may have different wireFee
//ranking should take the relative contribution in the exchange
//which is (value - denomFee / fixedFee)
- const rate1 = Amounts.divmod(d1.value, d1.denomWithdraw).quotient;
- const rate2 = Amounts.divmod(d2.value, d2.denomWithdraw).quotient;
+
+ const rate1 = Amounts.isZero(d1.denomWithdraw)
+ ? Number.MIN_SAFE_INTEGER
+ : Amounts.divmod(d1.value, d1.denomWithdraw).quotient;
+ const rate2 = Amounts.isZero(d2.denomWithdraw)
+ ? Number.MIN_SAFE_INTEGER
+ : Amounts.divmod(d2.value, d2.denomWithdraw).quotient;
const contribCmp = rate1 === rate2 ? 0 : rate1 < rate2 ? 1 : -1;
return (
contribCmp ||
@@ -719,8 +724,13 @@ function rankDenominationForDeposit(
//different exchanges may have different wireFee
//ranking should take the relative contribution in the exchange
//which is (value - denomFee / fixedFee)
- const rate1 = Amounts.divmod(d1.value, d1.denomDeposit).quotient;
- const rate2 = Amounts.divmod(d2.value, d2.denomDeposit).quotient;
+ const rate1 = Amounts.isZero(d1.denomDeposit)
+ ? Number.MIN_SAFE_INTEGER
+ : Amounts.divmod(d1.value, d1.denomDeposit).quotient;
+ const rate2 = Amounts.isZero(d2.denomDeposit)
+ ? Number.MIN_SAFE_INTEGER
+ : Amounts.divmod(d2.value, d2.denomDeposit).quotient;
+
const contribCmp = rate1 === rate2 ? 0 : rate1 < rate2 ? 1 : -1;
return (
contribCmp ||
diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts
index 090a11cf0..ee154252f 100644
--- a/packages/taler-wallet-core/src/pay-merchant.ts
+++ b/packages/taler-wallet-core/src/pay-merchant.ts
@@ -34,7 +34,6 @@ import {
assertUnreachable,
AsyncFlag,
checkDbInvariant,
- CheckPaymentResponse,
CheckPayTemplateReponse,
CheckPayTemplateRequest,
codecForAbortResponse,
@@ -342,7 +341,6 @@ export class PayMerchantTransactionContext implements TransactionContext {
return;
}
await tx.purchases.put(purchase);
- await tx.operationRetries.delete(this.taskId);
const newTxState = computePayMerchantTransactionState(purchase);
return { oldTxState, newTxState };
},
@@ -1028,11 +1026,17 @@ async function storeFirstPaySuccess(
purchase.merchantPaySig = payResponse.sig;
purchase.posConfirmation = payResponse.pos_confirmation;
const dl = purchase.download;
- checkDbInvariant(!!dl);
+ checkDbInvariant(
+ !!dl,
+ `purchase ${purchase.orderId} without ct downloaded`,
+ );
const contractTermsRecord = await tx.contractTerms.get(
dl.contractTermsHash,
);
- checkDbInvariant(!!contractTermsRecord);
+ checkDbInvariant(
+ !!contractTermsRecord,
+ `no contract terms found for purchase ${purchase.orderId}`,
+ );
const contractData = extractContractData(
contractTermsRecord.contractTermsRaw,
dl.contractTermsHash,
@@ -1625,6 +1629,9 @@ export async function checkPayForTemplate(
throw TalerError.fromUncheckedDetail(cfg.detail);
}
+ // FIXME: Put body.currencies *and* body.currency in the set of
+ // supported currencies.
+
return {
templateDetails,
supportedCurrencies: Object.keys(cfg.body.currencies),
@@ -2086,6 +2093,7 @@ export async function processPurchase(
case PurchaseStatus.PendingPayingReplay:
return processPurchasePay(wex, proposalId);
case PurchaseStatus.PendingQueryingRefund:
+ case PurchaseStatus.FinalizingQueryingAutoRefund:
return processPurchaseQueryRefund(wex, purchase);
case PurchaseStatus.PendingQueryingAutoRefund:
return processPurchaseAutoRefund(wex, purchase);
@@ -2110,6 +2118,7 @@ export async function processPurchase(
case PurchaseStatus.SuspendedPendingAcceptRefund:
case PurchaseStatus.SuspendedQueryingAutoRefund:
case PurchaseStatus.SuspendedQueryingRefund:
+ case PurchaseStatus.SuspendedFinalizingQueryingAutoRefund:
case PurchaseStatus.FailedAbort:
case PurchaseStatus.FailedPaidByOther:
return TaskRunResult.finished();
@@ -2155,7 +2164,7 @@ async function processPurchasePay(
logger.trace(`paying with session ID ${sessionId}`);
const payInfo = purchase.payInfo;
- checkDbInvariant(!!payInfo, "payInfo");
+ checkDbInvariant(!!payInfo, `purchase ${purchase.orderId} without payInfo`);
const download = await expectProposalDownload(wex, purchase);
@@ -2487,6 +2496,9 @@ const transitionSuspend: {
[PurchaseStatus.PendingQueryingAutoRefund]: {
next: PurchaseStatus.SuspendedQueryingAutoRefund,
},
+ [PurchaseStatus.FinalizingQueryingAutoRefund]: {
+ next: PurchaseStatus.SuspendedFinalizingQueryingAutoRefund,
+ },
};
const transitionResume: {
@@ -2509,6 +2521,9 @@ const transitionResume: {
[PurchaseStatus.SuspendedQueryingAutoRefund]: {
next: PurchaseStatus.PendingQueryingAutoRefund,
},
+ [PurchaseStatus.SuspendedFinalizingQueryingAutoRefund]: {
+ next: PurchaseStatus.FinalizingQueryingAutoRefund,
+ },
};
export function computePayMerchantTransactionState(
@@ -2637,6 +2652,16 @@ export function computePayMerchantTransactionState(
major: TransactionMajorState.Failed,
minor: TransactionMinorState.PaidByOther,
};
+ case PurchaseStatus.FinalizingQueryingAutoRefund:
+ return {
+ major: TransactionMajorState.Finalizing,
+ minor: TransactionMinorState.AutoRefund,
+ };
+ case PurchaseStatus.SuspendedFinalizingQueryingAutoRefund:
+ return {
+ major: TransactionMajorState.SuspendedFinalizing,
+ minor: TransactionMinorState.AutoRefund,
+ };
default:
assertUnreachable(purchaseRecord.purchaseStatus);
}
@@ -2648,21 +2673,45 @@ export function computePayMerchantTransactionActions(
switch (purchaseRecord.purchaseStatus) {
// Pending States
case PurchaseStatus.PendingDownloadingProposal:
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case PurchaseStatus.PendingPaying:
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case PurchaseStatus.PendingPayingReplay:
// Special "abort" since it goes back to "done".
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case PurchaseStatus.PendingQueryingAutoRefund:
// Special "abort" since it goes back to "done".
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case PurchaseStatus.PendingQueryingRefund:
// Special "abort" since it goes back to "done".
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case PurchaseStatus.PendingAcceptRefund:
// Special "abort" since it goes back to "done".
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
// Suspended Pending States
case PurchaseStatus.SuspendedDownloadingProposal:
return [TransactionAction.Resume, TransactionAction.Abort];
@@ -2682,14 +2731,18 @@ export function computePayMerchantTransactionActions(
return [TransactionAction.Resume, TransactionAction.Abort];
// Aborting States
case PurchaseStatus.AbortingWithRefund:
- return [TransactionAction.Fail, TransactionAction.Suspend];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Fail,
+ TransactionAction.Suspend,
+ ];
case PurchaseStatus.SuspendedAbortingWithRefund:
return [TransactionAction.Fail, TransactionAction.Resume];
// Dialog States
case PurchaseStatus.DialogProposed:
- return [];
+ return [TransactionAction.Retry];
case PurchaseStatus.DialogShared:
- return [];
+ return [TransactionAction.Retry];
// Final States
case PurchaseStatus.AbortedProposalRefused:
case PurchaseStatus.AbortedOrderDeleted:
@@ -2707,6 +2760,14 @@ export function computePayMerchantTransactionActions(
return [TransactionAction.Delete];
case PurchaseStatus.FailedPaidByOther:
return [TransactionAction.Delete];
+ case PurchaseStatus.FinalizingQueryingAutoRefund:
+ return [
+ TransactionAction.Suspend,
+ TransactionAction.Retry,
+ TransactionAction.Delete,
+ ];
+ case PurchaseStatus.SuspendedFinalizingQueryingAutoRefund:
+ return [TransactionAction.Resume, TransactionAction.Delete];
default:
assertUnreachable(purchaseRecord.purchaseStatus);
}
@@ -2909,8 +2970,12 @@ async function processPurchaseAutoRefund(
logger.warn("purchase does not exist anymore");
return;
}
- if (p.purchaseStatus !== PurchaseStatus.PendingQueryingAutoRefund) {
- return;
+ switch (p.purchaseStatus) {
+ case PurchaseStatus.PendingQueryingAutoRefund:
+ case PurchaseStatus.FinalizingQueryingAutoRefund:
+ break;
+ default:
+ return;
}
const oldTxState = computePayMerchantTransactionState(p);
p.purchaseStatus = PurchaseStatus.Done;
@@ -2956,8 +3021,12 @@ async function processPurchaseAutoRefund(
logger.warn("purchase does not exist anymore");
return;
}
- if (p.purchaseStatus !== PurchaseStatus.PendingQueryingAutoRefund) {
- return;
+ switch (p.purchaseStatus) {
+ case PurchaseStatus.PendingQueryingAutoRefund:
+ case PurchaseStatus.FinalizingQueryingAutoRefund:
+ break;
+ default:
+ return;
}
const oldTxState = computePayMerchantTransactionState(p);
p.purchaseStatus = PurchaseStatus.PendingAcceptRefund;
@@ -2997,7 +3066,7 @@ async function processPurchaseAbortingRefund(
for (let i = 0; i < payCoinSelection.coinPubs.length; i++) {
const coinPub = payCoinSelection.coinPubs[i];
const coin = await tx.coins.get(coinPub);
- checkDbInvariant(!!coin, "expected coin to be present");
+ checkDbInvariant(!!coin, `coin not found for ${coinPub}`);
abortingCoins.push({
coin_pub: coinPub,
contribution: Amounts.stringify(payCoinSelection.coinContributions[i]),
@@ -3501,7 +3570,8 @@ async function storeRefunds(
if (isAborting) {
myPurchase.purchaseStatus = PurchaseStatus.AbortedRefunded;
} else if (shouldCheckAutoRefund) {
- myPurchase.purchaseStatus = PurchaseStatus.PendingQueryingAutoRefund;
+ myPurchase.purchaseStatus =
+ PurchaseStatus.FinalizingQueryingAutoRefund;
} else {
myPurchase.purchaseStatus = PurchaseStatus.Done;
}
diff --git a/packages/taler-wallet-core/src/pay-peer-common.ts b/packages/taler-wallet-core/src/pay-peer-common.ts
index bfd39b657..a1729ced7 100644
--- a/packages/taler-wallet-core/src/pay-peer-common.ts
+++ b/packages/taler-wallet-core/src/pay-peer-common.ts
@@ -140,10 +140,10 @@ export async function getMergeReserveInfo(
{ storeNames: ["exchanges", "reserves"] },
async (tx) => {
const ex = await tx.exchanges.get(req.exchangeBaseUrl);
- checkDbInvariant(!!ex);
+ checkDbInvariant(!!ex, `no exchange record for ${req.exchangeBaseUrl}`);
if (ex.currentMergeReserveRowId != null) {
const reserve = await tx.reserves.get(ex.currentMergeReserveRowId);
- checkDbInvariant(!!reserve);
+ checkDbInvariant(!!reserve, `reserver ${ex.currentMergeReserveRowId} missing in db`);
return reserve;
}
const reserve: ReserveRecord = {
@@ -151,7 +151,7 @@ export async function getMergeReserveInfo(
reservePub: newReservePair.pub,
};
const insertResp = await tx.reserves.put(reserve);
- checkDbInvariant(typeof insertResp.key === "number");
+ checkDbInvariant(typeof insertResp.key === "number", `reserve key is not a number`);
reserve.rowId = insertResp.key;
ex.currentMergeReserveRowId = reserve.rowId;
await tx.exchanges.put(ex);
diff --git a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts
index 840c244d0..3e7fdd36b 100644
--- a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts
@@ -59,6 +59,7 @@ import {
TombstoneTag,
TransactionContext,
constructTaskIdentifier,
+ requireExchangeTosAcceptedOrThrow,
} from "./common.js";
import {
KycPendingInfo,
@@ -933,6 +934,11 @@ export async function checkPeerPullPaymentInitiation(
Amounts.parseOrThrow(req.amount),
undefined,
);
+ if (wi.selectedDenoms.selectedDenoms.length === 0) {
+ throw Error(
+ `unable to check pull payment from ${exchangeUrl}, can't select denominations for instructed amount (${req.amount}`,
+ );
+ }
logger.trace(`got withdrawal info`);
@@ -1021,7 +1027,8 @@ export async function initiatePeerPullPayment(
const exchangeBaseUrl = maybeExchangeBaseUrl;
- await fetchFreshExchange(wex, exchangeBaseUrl);
+ const exchange = await fetchFreshExchange(wex, exchangeBaseUrl);
+ requireExchangeTosAcceptedOrThrow(exchange);
const mergeReserveInfo = await getMergeReserveInfo(wex, {
exchangeBaseUrl: exchangeBaseUrl,
@@ -1039,7 +1046,10 @@ export async function initiatePeerPullPayment(
const withdrawalGroupId = encodeCrock(getRandomBytes(32));
const mergeReserveRowId = mergeReserveInfo.rowId;
- checkDbInvariant(!!mergeReserveRowId);
+ checkDbInvariant(
+ !!mergeReserveRowId,
+ `merge reserve for ${exchangeBaseUrl} without rowid`,
+ );
const contractEncNonce = encodeCrock(getRandomBytes(24));
@@ -1049,6 +1059,11 @@ export async function initiatePeerPullPayment(
Amounts.parseOrThrow(req.partialContractTerms.amount),
undefined,
);
+ if (wi.selectedDenoms.selectedDenoms.length === 0) {
+ throw Error(
+ `unable to initiate pull payment from ${exchangeBaseUrl}, can't select denominations for instructed amount (${req.partialContractTerms.amount}`,
+ );
+ }
const mergeTimestamp = TalerPreciseTimestamp.now();
@@ -1184,15 +1199,31 @@ export function computePeerPullCreditTransactionActions(
): TransactionAction[] {
switch (pullCreditRecord.status) {
case PeerPullPaymentCreditStatus.PendingCreatePurse:
- return [TransactionAction.Abort, TransactionAction.Suspend];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Abort,
+ TransactionAction.Suspend,
+ ];
case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
- return [TransactionAction.Abort, TransactionAction.Suspend];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Abort,
+ TransactionAction.Suspend,
+ ];
case PeerPullPaymentCreditStatus.PendingReady:
- return [TransactionAction.Abort, TransactionAction.Suspend];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Abort,
+ TransactionAction.Suspend,
+ ];
case PeerPullPaymentCreditStatus.Done:
return [TransactionAction.Delete];
case PeerPullPaymentCreditStatus.PendingWithdrawing:
- return [TransactionAction.Abort, TransactionAction.Suspend];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Abort,
+ TransactionAction.Suspend,
+ ];
case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
return [TransactionAction.Resume, TransactionAction.Abort];
case PeerPullPaymentCreditStatus.SuspendedReady:
@@ -1204,7 +1235,11 @@ export function computePeerPullCreditTransactionActions(
case PeerPullPaymentCreditStatus.Aborted:
return [TransactionAction.Delete];
case PeerPullPaymentCreditStatus.AbortingDeletePurse:
- return [TransactionAction.Suspend, TransactionAction.Fail];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Fail,
+ ];
case PeerPullPaymentCreditStatus.Failed:
return [TransactionAction.Delete];
case PeerPullPaymentCreditStatus.Expired:
diff --git a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
index 0355b58ad..e9be15026 100644
--- a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
@@ -1000,7 +1000,7 @@ export function computePeerPullDebitTransactionActions(
): TransactionAction[] {
switch (pullDebitRecord.status) {
case PeerPullDebitRecordStatus.DialogProposed:
- return [];
+ return [TransactionAction.Retry, TransactionAction.Delete];
case PeerPullDebitRecordStatus.PendingDeposit:
return [TransactionAction.Abort, TransactionAction.Suspend];
case PeerPullDebitRecordStatus.Done:
diff --git a/packages/taler-wallet-core/src/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/pay-peer-push-credit.ts
index 93f1a63a7..5a1bfbdbd 100644
--- a/packages/taler-wallet-core/src/pay-peer-push-credit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-push-credit.ts
@@ -61,6 +61,7 @@ import {
TombstoneTag,
TransactionContext,
constructTaskIdentifier,
+ requireExchangeTosAcceptedOrThrow,
} from "./common.js";
import {
KycPendingInfo,
@@ -407,7 +408,8 @@ export async function preparePeerPushCredit(
const exchangeBaseUrl = uri.exchangeBaseUrl;
- await fetchFreshExchange(wex, exchangeBaseUrl);
+ const exchange = await fetchFreshExchange(wex, exchangeBaseUrl);
+ requireExchangeTosAcceptedOrThrow(exchange);
const contractPriv = uri.contractPriv;
const contractPub = encodeCrock(eddsaGetPublic(decodeCrock(contractPriv)));
@@ -459,6 +461,12 @@ export async function preparePeerPushCredit(
undefined,
);
+ if (wi.selectedDenoms.selectedDenoms.length === 0) {
+ throw Error(
+ `unable to prepare push credit from ${exchangeBaseUrl}, can't select denominations for instructed amount (${purseStatus.balance}`,
+ );
+ }
+
const transitionInfo = await wex.db.runReadWriteTx(
{ storeNames: ["contractTerms", "peerPushCredit"] },
async (tx) => {
@@ -872,7 +880,10 @@ export async function processPeerPushCredit(
`processing peerPushCredit in state ${peerInc.status.toString(16)}`,
);
- checkDbInvariant(!!contractTerms);
+ checkDbInvariant(
+ !!contractTerms,
+ `not contract terms for peer push ${peerPushCreditId}`,
+ );
switch (peerInc.status) {
case PeerPushCreditStatus.PendingMergeKycRequired: {
@@ -1011,15 +1022,27 @@ export function computePeerPushCreditTransactionActions(
): TransactionAction[] {
switch (pushCreditRecord.status) {
case PeerPushCreditStatus.DialogProposed:
- return [TransactionAction.Delete];
+ return [TransactionAction.Retry, TransactionAction.Delete];
case PeerPushCreditStatus.PendingMerge:
- return [TransactionAction.Abort, TransactionAction.Suspend];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Abort,
+ TransactionAction.Suspend,
+ ];
case PeerPushCreditStatus.Done:
return [TransactionAction.Delete];
case PeerPushCreditStatus.PendingMergeKycRequired:
- return [TransactionAction.Abort, TransactionAction.Suspend];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Abort,
+ TransactionAction.Suspend,
+ ];
case PeerPushCreditStatus.PendingWithdrawing:
- return [TransactionAction.Suspend, TransactionAction.Fail];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Fail,
+ ];
case PeerPushCreditStatus.SuspendedMerge:
return [TransactionAction.Resume, TransactionAction.Abort];
case PeerPushCreditStatus.SuspendedMergeKycRequired:
diff --git a/packages/taler-wallet-core/src/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/pay-peer-push-debit.ts
index 6452407ff..f8e6adb3c 100644
--- a/packages/taler-wallet-core/src/pay-peer-push-debit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-push-debit.ts
@@ -406,7 +406,10 @@ async function handlePurseCreationConflict(
const instructedAmount = Amounts.parseOrThrow(peerPushInitiation.amount);
const sel = peerPushInitiation.coinSel;
- checkDbInvariant(!!sel);
+ checkDbInvariant(
+ !!sel,
+ `no coin selected for peer push initiation ${peerPushInitiation.pursePub}`,
+ );
const repair: PreviousPayCoins = [];
@@ -1218,17 +1221,37 @@ export function computePeerPushDebitTransactionActions(
): TransactionAction[] {
switch (ppiRecord.status) {
case PeerPushDebitStatus.PendingCreatePurse:
- return [TransactionAction.Abort, TransactionAction.Suspend];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Abort,
+ TransactionAction.Suspend,
+ ];
case PeerPushDebitStatus.PendingReady:
- return [TransactionAction.Abort, TransactionAction.Suspend];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Abort,
+ TransactionAction.Suspend,
+ ];
case PeerPushDebitStatus.Aborted:
return [TransactionAction.Delete];
case PeerPushDebitStatus.AbortingDeletePurse:
- return [TransactionAction.Suspend, TransactionAction.Fail];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Fail,
+ ];
case PeerPushDebitStatus.AbortingRefreshDeleted:
- return [TransactionAction.Suspend, TransactionAction.Fail];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Fail,
+ ];
case PeerPushDebitStatus.AbortingRefreshExpired:
- return [TransactionAction.Suspend, TransactionAction.Fail];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Fail,
+ ];
case PeerPushDebitStatus.SuspendedAbortingRefreshExpired:
return [TransactionAction.Resume, TransactionAction.Fail];
case PeerPushDebitStatus.SuspendedAbortingDeletePurse:
diff --git a/packages/taler-wallet-core/src/recoup.ts b/packages/taler-wallet-core/src/recoup.ts
index 6a09f9a0e..be5731b0b 100644
--- a/packages/taler-wallet-core/src/recoup.ts
+++ b/packages/taler-wallet-core/src/recoup.ts
@@ -199,8 +199,8 @@ async function recoupRefreshCoin(
revokedCoin.exchangeBaseUrl,
revokedCoin.denomPubHash,
);
- checkDbInvariant(!!oldCoinDenom);
- checkDbInvariant(!!revokedCoinDenom);
+ checkDbInvariant(!!oldCoinDenom, `no denom for coin, hash ${oldCoin.denomPubHash}`);
+ checkDbInvariant(!!revokedCoinDenom, `no revoked denom for coin, hash ${revokedCoin.denomPubHash}`);
revokedCoin.status = CoinStatus.Dormant;
if (!revokedCoin.spendAllocation) {
// We don't know what happened to this coin
diff --git a/packages/taler-wallet-core/src/refresh.ts b/packages/taler-wallet-core/src/refresh.ts
index 7800967e6..05c65f6b6 100644
--- a/packages/taler-wallet-core/src/refresh.ts
+++ b/packages/taler-wallet-core/src/refresh.ts
@@ -29,7 +29,6 @@ import {
Amounts,
amountToPretty,
assertUnreachable,
- AsyncFlag,
checkDbInvariant,
codecForCoinHistoryResponse,
codecForExchangeMeltResponse,
@@ -68,12 +67,14 @@ import {
WalletNotification,
} from "@gnu-taler/taler-util";
import {
+ HttpResponse,
readSuccessResponseJsonOrThrow,
readTalerErrorResponse,
throwUnexpectedRequestError,
} from "@gnu-taler/taler-util/http";
import {
constructTaskIdentifier,
+ genericWaitForState,
makeCoinsVisible,
PendingTaskType,
TaskIdStr,
@@ -386,7 +387,6 @@ async function getCoinAvailabilityForDenom(
denom: DenominationInfo,
ageRestriction: number,
): Promise<CoinAvailabilityRecord> {
- checkDbInvariant(!!denom);
let car = await tx.coinAvailability.get([
denom.exchangeBaseUrl,
denom.denomPubHash,
@@ -537,7 +537,10 @@ async function destroyRefreshSession(
denom,
oldCoin.maxAge,
);
- checkDbInvariant(car.pendingRefreshOutputCount != null);
+ checkDbInvariant(
+ car.pendingRefreshOutputCount != null,
+ `no pendingRefreshOutputCount for denom ${dph}`,
+ );
car.pendingRefreshOutputCount =
car.pendingRefreshOutputCount - refreshSession.newDenoms[i].count;
await tx.coinAvailability.put(car);
@@ -693,7 +696,7 @@ async function refreshMelt(
switch (resp.status) {
case HttpStatusCode.NotFound: {
const errDetail = await readTalerErrorResponse(resp);
- await handleRefreshMeltNotFound(ctx, coinIndex, errDetail);
+ await handleRefreshMeltNotFound(ctx, coinIndex, resp, errDetail);
return;
}
case HttpStatusCode.Gone: {
@@ -898,9 +901,18 @@ async function handleRefreshMeltConflict(
async function handleRefreshMeltNotFound(
ctx: RefreshTransactionContext,
coinIndex: number,
+ resp: HttpResponse,
errDetails: TalerErrorDetail,
): Promise<void> {
- // FIXME: Validate the exchange's error response
+ // Make sure that we only act on a 404 that indicates a final problem
+ // with the coin.
+ switch (errDetails.code) {
+ case TalerErrorCode.EXCHANGE_GENERIC_COIN_UNKNOWN:
+ case TalerErrorCode.EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN:
+ break;
+ default:
+ throwUnexpectedRequestError(resp, errDetails);
+ }
await ctx.wex.db.runReadWriteTx(
{
storeNames: [
@@ -1242,7 +1254,10 @@ async function refreshReveal(
coin.exchangeBaseUrl,
coin.denomPubHash,
);
- checkDbInvariant(!!denomInfo);
+ checkDbInvariant(
+ !!denomInfo,
+ `no denom with hash ${coin.denomPubHash}`,
+ );
const car = await getCoinAvailabilityForDenom(
wex,
tx,
@@ -1252,6 +1267,7 @@ async function refreshReveal(
checkDbInvariant(
car.pendingRefreshOutputCount != null &&
car.pendingRefreshOutputCount > 0,
+ `no pendingRefreshOutputCount for denom ${coin.denomPubHash} age ${coin.maxAge}`,
);
car.pendingRefreshOutputCount--;
car.freshCoinCount++;
@@ -1559,9 +1575,22 @@ async function applyRefreshToOldCoins(
coin.denomPubHash,
coin.maxAge,
]);
- checkDbInvariant(!!coinAv);
- checkDbInvariant(coinAv.freshCoinCount > 0);
+ checkDbInvariant(
+ !!coinAv,
+ `no denom info for ${coin.denomPubHash} age ${coin.maxAge}`,
+ );
+ checkDbInvariant(
+ coinAv.freshCoinCount > 0,
+ `no fresh coins for ${coin.denomPubHash}`,
+ );
coinAv.freshCoinCount--;
+ if (coin.visible) {
+ if (!coinAv.visibleCoinCount) {
+ logger.error("coin availability inconsistent");
+ } else {
+ coinAv.visibleCoinCount--;
+ }
+ }
await tx.coinAvailability.put(coinAv);
break;
}
@@ -1770,7 +1799,7 @@ export async function forceRefresh(
],
},
async (tx) => {
- let coinPubs: CoinRefreshRequest[] = [];
+ const coinPubs: CoinRefreshRequest[] = [];
for (const c of req.refreshCoinSpecs) {
const coin = await tx.coins.get(c.coinPub);
if (!coin) {
@@ -1782,7 +1811,7 @@ export async function forceRefresh(
coin.exchangeBaseUrl,
coin.denomPubHash,
);
- checkDbInvariant(!!denom);
+ checkDbInvariant(!!denom, `no denom hash: ${coin.denomPubHash}`);
coinPubs.push({
coinPub: c.coinPub,
amount: c.amount ?? denom.value,
@@ -1818,66 +1847,38 @@ export async function waitRefreshFinal(
const ctx = new RefreshTransactionContext(wex, refreshGroupId);
wex.taskScheduler.startShepherdTask(ctx.taskId);
- // FIXME: Clean up using the new JS "using" / Symbol.dispose syntax.
- const refreshNotifFlag = new AsyncFlag();
- // Raise purchaseNotifFlag whenever we get a notification
- // about our refresh.
- const cancelNotif = wex.ws.addNotificationListener((notif) => {
- if (
- notif.type === NotificationType.TransactionStateTransition &&
- notif.transactionId === ctx.transactionId
- ) {
- refreshNotifFlag.raise();
- }
- });
- const unregisterOnCancelled = wex.cancellationToken.onCancelled(() => {
- cancelNotif();
- refreshNotifFlag.raise();
+ await genericWaitForState(wex, {
+ async checkState(): Promise<boolean> {
+ // Check if refresh is final
+ const res = await ctx.wex.db.runReadOnlyTx(
+ { storeNames: ["refreshGroups"] },
+ async (tx) => {
+ return {
+ rg: await tx.refreshGroups.get(ctx.refreshGroupId),
+ };
+ },
+ );
+ const { rg } = res;
+ if (!rg) {
+ // Must've been deleted, we consider that final.
+ return true;
+ }
+ switch (rg.operationStatus) {
+ case RefreshOperationStatus.Failed:
+ case RefreshOperationStatus.Finished:
+ // Transaction is final
+ return true;
+ case RefreshOperationStatus.Pending:
+ case RefreshOperationStatus.Suspended:
+ break;
+ }
+ return false;
+ },
+ filterNotification(notif): boolean {
+ return (
+ notif.type === NotificationType.TransactionStateTransition &&
+ notif.transactionId === ctx.transactionId
+ );
+ },
});
-
- try {
- await internalWaitRefreshFinal(ctx, refreshNotifFlag);
- } catch (e) {
- unregisterOnCancelled();
- cancelNotif();
- }
-}
-
-async function internalWaitRefreshFinal(
- ctx: RefreshTransactionContext,
- flag: AsyncFlag,
-): Promise<void> {
- while (true) {
- if (ctx.wex.cancellationToken.isCancelled) {
- throw Error("cancelled");
- }
-
- // Check if refresh is final
- const res = await ctx.wex.db.runReadOnlyTx(
- { storeNames: ["refreshGroups", "operationRetries"] },
- async (tx) => {
- return {
- rg: await tx.refreshGroups.get(ctx.refreshGroupId),
- };
- },
- );
- const { rg } = res;
- if (!rg) {
- // Must've been deleted, we consider that final.
- return;
- }
- switch (rg.operationStatus) {
- case RefreshOperationStatus.Failed:
- case RefreshOperationStatus.Finished:
- // Transaction is final
- return;
- case RefreshOperationStatus.Pending:
- case RefreshOperationStatus.Suspended:
- break;
- }
-
- // Wait for the next transition
- await flag.wait();
- flag.reset();
- }
}
diff --git a/packages/taler-wallet-core/src/shepherd.ts b/packages/taler-wallet-core/src/shepherd.ts
index 3b160d97f..470f45aff 100644
--- a/packages/taler-wallet-core/src/shepherd.ts
+++ b/packages/taler-wallet-core/src/shepherd.ts
@@ -50,12 +50,13 @@ import {
parseTaskIdentifier,
} from "./common.js";
import {
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
OperationRetryRecord,
WalletDbAllStoresReadOnlyTransaction,
WalletDbReadOnlyTransaction,
timestampAbsoluteFromDb,
+ timestampPreciseToDb,
} from "./db.js";
import {
computeDepositTransactionStatus,
@@ -113,6 +114,8 @@ const logger = new Logger("shepherd.ts");
*/
interface ShepherdInfo {
cts: CancellationToken.Source;
+ latch?: Promise<void>;
+ stopped: boolean;
}
/**
@@ -256,29 +259,36 @@ export class TaskSchedulerImpl implements TaskScheduler {
async reload(): Promise<void> {
await this.ensureRunning();
const tasksIds = [...this.sheps.keys()];
- logger.info(`reloading sheperd with ${tasksIds.length} tasks`);
+ logger.info(`reloading shepherd with ${tasksIds.length} tasks`);
for (const taskId of tasksIds) {
- this.stopShepherdTask(taskId);
+ await this.stopShepherdTask(taskId);
}
for (const taskId of tasksIds) {
this.startShepherdTask(taskId);
}
}
-
private async internalStartShepherdTask(taskId: TaskIdStr): Promise<void> {
logger.trace(`Starting to shepherd task ${taskId}`);
const oldShep = this.sheps.get(taskId);
if (oldShep) {
- logger.trace(`Already have a shepherd for ${taskId}`);
- return;
+ if (!oldShep.stopped) {
+ logger.trace(`Already have a shepherd for ${taskId}`);
+ return;
+ }
+ logger.trace(
+ `Waiting old task to complete the loop in cancel mode ${taskId}`,
+ );
+ await oldShep.latch;
}
logger.trace(`Creating new shepherd for ${taskId}`);
const newShep: ShepherdInfo = {
cts: CancellationToken.create(),
+ stopped: false,
};
this.sheps.set(taskId, newShep);
try {
- await this.internalShepherdTask(taskId, newShep);
+ newShep.latch = this.internalShepherdTask(taskId, newShep);
+ await newShep.latch;
} finally {
logger.trace(`Done shepherding ${taskId}`);
this.sheps.delete(taskId);
@@ -291,8 +301,8 @@ export class TaskSchedulerImpl implements TaskScheduler {
const oldShep = this.sheps.get(taskId);
if (oldShep) {
logger.trace(`Cancelling old shepherd for ${taskId}`);
- oldShep.cts.cancel();
- this.sheps.delete(taskId);
+ oldShep.cts.cancel(`stopping task ${taskId}`);
+ oldShep.stopped = true;
this.iterCond.trigger();
}
}
@@ -306,6 +316,7 @@ export class TaskSchedulerImpl implements TaskScheduler {
const maybeNotification = await this.ws.db.runAllStoresReadWriteTx(
{},
async (tx) => {
+ logger.trace(`storing task [reset] for ${taskId}`);
await tx.operationRetries.delete(taskId);
return taskToRetryNotification(this.ws, tx, taskId, undefined);
},
@@ -325,7 +336,13 @@ export class TaskSchedulerImpl implements TaskScheduler {
try {
await info.cts.token.racePromise(this.ws.timerGroup.resolveAfter(delay));
} catch (e) {
- logger.info(`waiting for ${taskId} interrupted`);
+ if (e instanceof CancellationToken.CancellationError) {
+ logger.info(
+ `waiting for ${taskId} interrupted: ${e.message} ${j2s(e.reason)}`,
+ );
+ } else {
+ logger.info(`waiting for ${taskId} interrupted: ${e}`);
+ }
}
}
@@ -363,13 +380,14 @@ export class TaskSchedulerImpl implements TaskScheduler {
try {
res = await callOperationHandlerForTaskId(wex, taskId);
} catch (e) {
+ logger.trace(`Shepherd error ${taskId} saving response ${e}`);
res = {
type: TaskRunResultType.Error,
errorDetail: getErrorDetailFromException(e),
};
}
if (info.cts.token.isCancelled) {
- logger.trace("task cancelled, not processing result");
+ logger.trace(`task ${taskId} cancelled, not processing result`);
return;
}
if (this.ws.stopped) {
@@ -382,7 +400,9 @@ export class TaskSchedulerImpl implements TaskScheduler {
});
switch (res.type) {
case TaskRunResultType.Error: {
- logger.trace(`Shepherd for ${taskId} got error result.`);
+ logger.trace(
+ `Shepherd for ${taskId} got error result: ${j2s(res.errorDetail)}`,
+ );
const retryRecord = await storePendingTaskError(
this.ws,
taskId,
@@ -412,8 +432,13 @@ export class TaskSchedulerImpl implements TaskScheduler {
}
case TaskRunResultType.ScheduleLater: {
logger.trace(`Shepherd for ${taskId} got schedule-later result.`);
- await storeTaskProgress(this.ws, taskId);
- const delay = AbsoluteTime.remaining(res.runAt);
+ const retryRecord = await storePendingTaskPending(
+ this.ws,
+ taskId,
+ res.runAt,
+ );
+ const t = timestampAbsoluteFromDb(retryRecord.retryInfo.nextRetry);
+ const delay = AbsoluteTime.remaining(t);
logger.trace(`Waiting for ${delay.d_ms} ms`);
await this.wait(taskId, info, delay);
break;
@@ -451,7 +476,7 @@ async function storePendingTaskError(
pendingTaskId: string,
e: TalerErrorDetail,
): Promise<OperationRetryRecord> {
- logger.info(`storing pending task error for ${pendingTaskId}`);
+ logger.trace(`storing task [pending] with ERROR for ${pendingTaskId}`);
const res = await ws.db.runAllStoresReadWriteTx({}, async (tx) => {
let retryRecord = await tx.operationRetries.get(pendingTaskId);
if (!retryRecord) {
@@ -483,6 +508,7 @@ async function storeTaskProgress(
ws: InternalWalletState,
pendingTaskId: string,
): Promise<void> {
+ logger.trace(`storing task [progress] for ${pendingTaskId}`);
await ws.db.runReadWriteTx(
{ storeNames: ["operationRetries"] },
async (tx) => {
@@ -494,7 +520,9 @@ async function storeTaskProgress(
async function storePendingTaskPending(
ws: InternalWalletState,
pendingTaskId: string,
+ schedTime?: AbsoluteTime,
): Promise<OperationRetryRecord> {
+ logger.trace(`storing task [pending] for ${pendingTaskId}`);
const res = await ws.db.runAllStoresReadWriteTx({}, async (tx) => {
let retryRecord = await tx.operationRetries.get(pendingTaskId);
let hadError = false;
@@ -510,6 +538,11 @@ async function storePendingTaskPending(
delete retryRecord.lastError;
retryRecord.retryInfo = DbRetryInfo.increment(retryRecord.retryInfo);
}
+ if (schedTime) {
+ retryRecord.retryInfo.nextRetry = timestampPreciseToDb(
+ AbsoluteTime.toPreciseTimestamp(schedTime),
+ );
+ }
await tx.operationRetries.put(retryRecord);
let notification: WalletNotification | undefined = undefined;
if (hadError) {
@@ -535,6 +568,7 @@ async function storePendingTaskFinished(
ws: InternalWalletState,
pendingTaskId: string,
): Promise<void> {
+ logger.trace(`storing task [finished] for ${pendingTaskId}`);
await ws.db.runReadWriteTx(
{ storeNames: ["operationRetries"] },
async (tx) => {
@@ -978,8 +1012,8 @@ export async function getActiveTaskIds(
},
async (tx) => {
const active = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
// Withdrawals
diff --git a/packages/taler-wallet-core/src/transactions.ts b/packages/taler-wallet-core/src/transactions.ts
index 9a9fb524f..7782d09ba 100644
--- a/packages/taler-wallet-core/src/transactions.ts
+++ b/packages/taler-wallet-core/src/transactions.ts
@@ -62,8 +62,8 @@ import {
DenomLossEventRecord,
DepositElementStatus,
DepositGroupRecord,
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
OperationRetryRecord,
PeerPullCreditRecord,
PeerPullDebitRecordStatus,
@@ -93,7 +93,6 @@ import {
computeDenomLossTransactionStatus,
DenomLossTransactionContext,
ExchangeWireDetails,
- fetchFreshExchange,
getExchangeWireDetailsInTx,
} from "./exchanges.js";
import {
@@ -244,11 +243,14 @@ export async function getTransactionById(
const opId = TaskIdentifiers.forWithdrawal(withdrawalGroupRecord);
const ort = await tx.operationRetries.get(opId);
- const exchangeDetails = await getExchangeWireDetailsInTx(
- tx,
- withdrawalGroupRecord.exchangeBaseUrl,
- );
- if (!exchangeDetails) throw Error("not exchange details");
+ const exchangeDetails =
+ withdrawalGroupRecord.exchangeBaseUrl === undefined
+ ? undefined
+ : await getExchangeWireDetailsInTx(
+ tx,
+ withdrawalGroupRecord.exchangeBaseUrl,
+ );
+ // if (!exchangeDetails) throw Error("not exchange details");
if (
withdrawalGroupRecord.wgInfo.withdrawalType ===
@@ -260,7 +262,10 @@ export async function getTransactionById(
ort,
);
}
-
+ checkDbInvariant(
+ exchangeDetails !== undefined,
+ "manual withdrawal without exchange",
+ );
return buildTransactionForManualWithdraw(
withdrawalGroupRecord,
exchangeDetails,
@@ -405,7 +410,10 @@ export async function getTransactionById(
const debit = await tx.peerPushDebit.get(parsedTx.pursePub);
if (!debit) throw Error("not found");
const ct = await tx.contractTerms.get(debit.contractTermsHash);
- checkDbInvariant(!!ct);
+ checkDbInvariant(
+ !!ct,
+ `no contract terms for p2p push ${parsedTx.pursePub}`,
+ );
return buildTransactionForPushPaymentDebit(
debit,
ct.contractTermsRaw,
@@ -429,7 +437,10 @@ export async function getTransactionById(
const pushInc = await tx.peerPushCredit.get(peerPushCreditId);
if (!pushInc) throw Error("not found");
const ct = await tx.contractTerms.get(pushInc.contractTermsHash);
- checkDbInvariant(!!ct);
+ checkDbInvariant(
+ !!ct,
+ `no contract terms for p2p push ${peerPushCreditId}`,
+ );
let wg: WithdrawalGroupRecord | undefined = undefined;
let wgOrt: OperationRetryRecord | undefined = undefined;
@@ -441,7 +452,7 @@ export async function getTransactionById(
}
}
const pushIncOpId = TaskIdentifiers.forPeerPushCredit(pushInc);
- let pushIncOrt = await tx.operationRetries.get(pushIncOpId);
+ const pushIncOrt = await tx.operationRetries.get(pushIncOpId);
return buildTransactionForPeerPushCredit(
pushInc,
@@ -469,7 +480,7 @@ export async function getTransactionById(
const pushInc = await tx.peerPullCredit.get(pursePub);
if (!pushInc) throw Error("not found");
const ct = await tx.contractTerms.get(pushInc.contractTermsHash);
- checkDbInvariant(!!ct);
+ checkDbInvariant(!!ct, `no contract terms for p2p push ${pursePub}`);
let wg: WithdrawalGroupRecord | undefined = undefined;
let wgOrt: OperationRetryRecord | undefined = undefined;
@@ -594,6 +605,7 @@ function buildTransactionForPeerPullCredit(
const txState = computePeerPullCreditTransactionState(pullCredit);
checkDbInvariant(wsr.instructedAmount !== undefined, "wg uninitialized");
checkDbInvariant(wsr.denomsSel !== undefined, "wg uninitialized");
+ checkDbInvariant(wsr.exchangeBaseUrl !== undefined, "wg uninitialized");
return {
type: TransactionType.PeerPullCredit,
txState,
@@ -668,6 +680,7 @@ function buildTransactionForPeerPushCredit(
}
checkDbInvariant(wg.instructedAmount !== undefined, "wg uninitialized");
checkDbInvariant(wg.denomsSel !== undefined, "wg uninitialized");
+ checkDbInvariant(wg.exchangeBaseUrl !== undefined, "wg uninitialized");
const txState = computePeerPushCreditTransactionState(pushInc);
return {
@@ -720,16 +733,21 @@ function buildTransactionForPeerPushCredit(
function buildTransactionForBankIntegratedWithdraw(
wg: WithdrawalGroupRecord,
- exchangeDetails: ExchangeWireDetails,
+ exchangeDetails: ExchangeWireDetails | undefined,
ort?: OperationRetryRecord,
): TransactionWithdrawal {
if (wg.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) {
throw Error("");
}
+ const instructedCurrency =
+ wg.instructedAmount === undefined
+ ? undefined
+ : Amounts.currencyOf(wg.instructedAmount);
+ const currency = wg.wgInfo.bankInfo.currency ?? instructedCurrency;
+ checkDbInvariant(currency !== undefined, "wg uninitialized (missing currency)");
const txState = computeWithdrawalTransactionStatus(wg);
- const zero = Amounts.stringify(
- Amounts.zeroOfCurrency(exchangeDetails.currency),
- );
+
+ const zero = Amounts.stringify(Amounts.zeroOfCurrency(currency));
return {
type: TransactionType.Withdrawal,
txState,
@@ -785,6 +803,7 @@ function buildTransactionForManualWithdraw(
checkDbInvariant(wg.instructedAmount !== undefined, "wg uninitialized");
checkDbInvariant(wg.denomsSel !== undefined, "wg uninitialized");
+ checkDbInvariant(wg.exchangeBaseUrl !== undefined, "wg uninitialized");
const exchangePaytoUris = augmentPaytoUrisForWithdrawal(
plainPaytoUris,
wg.reservePub,
@@ -1035,8 +1054,14 @@ function buildTransactionForPurchase(
}));
const timestamp = purchaseRecord.timestampAccept;
- checkDbInvariant(!!timestamp);
- checkDbInvariant(!!purchaseRecord.payInfo);
+ checkDbInvariant(
+ !!timestamp,
+ `purchase ${purchaseRecord.orderId} without accepted time`,
+ );
+ checkDbInvariant(
+ !!purchaseRecord.payInfo,
+ `purchase ${purchaseRecord.orderId} without payinfo`,
+ );
const txState = computePayMerchantTransactionState(purchaseRecord);
return {
@@ -1090,6 +1115,10 @@ export async function getWithdrawalTransactionByUri(
if (!withdrawalGroupRecord) {
return undefined;
}
+ if (withdrawalGroupRecord.exchangeBaseUrl === undefined) {
+ // prepared and unconfirmed withdrawals are hidden
+ return undefined;
+ }
const opId = TaskIdentifiers.forWithdrawal(withdrawalGroupRecord);
const ort = await tx.operationRetries.get(opId);
@@ -1176,7 +1205,7 @@ export async function getTransactions(
return;
}
const ct = await tx.contractTerms.get(pi.contractTermsHash);
- checkDbInvariant(!!ct);
+ checkDbInvariant(!!ct, `no contract terms for p2p push ${pi.pursePub}`);
transactions.push(
buildTransactionForPushPaymentDebit(pi, ct.contractTermsRaw),
);
@@ -1250,9 +1279,9 @@ export async function getTransactions(
}
}
const pushIncOpId = TaskIdentifiers.forPeerPushCredit(pi);
- let pushIncOrt = await tx.operationRetries.get(pushIncOpId);
+ const pushIncOrt = await tx.operationRetries.get(pushIncOpId);
- checkDbInvariant(!!ct);
+ checkDbInvariant(!!ct, `no contract terms for p2p push ${pi.pursePub}`);
transactions.push(
buildTransactionForPeerPushCredit(
pi,
@@ -1284,9 +1313,9 @@ export async function getTransactions(
}
}
const pushIncOpId = TaskIdentifiers.forPeerPullPaymentInitiation(pi);
- let pushIncOrt = await tx.operationRetries.get(pushIncOpId);
+ const pushIncOrt = await tx.operationRetries.get(pushIncOpId);
- checkDbInvariant(!!ct);
+ checkDbInvariant(!!ct, `no contract terms for p2p push ${pi.pursePub}`);
transactions.push(
buildTransactionForPeerPullCredit(
pi,
@@ -1772,7 +1801,11 @@ export async function retryTransaction(
}
}
+/**
+ * Reset the task retry counter for all tasks.
+ */
export async function retryAll(wex: WalletExecutionContext): Promise<void> {
+ await wex.taskScheduler.ensureRunning();
const tasks = wex.taskScheduler.getActiveTasks();
for (const task of tasks) {
await wex.taskScheduler.resetTaskRetries(task);
@@ -1935,8 +1968,8 @@ async function iterRecordsForWithdrawal(
let withdrawalGroupRecords: WithdrawalGroupRecord[];
if (filter.onlyState === "nonfinal") {
const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
withdrawalGroupRecords =
await tx.withdrawalGroups.indexes.byStatus.getAll(keyRange);
@@ -1957,8 +1990,8 @@ async function iterRecordsForDeposit(
let dgs: DepositGroupRecord[];
if (filter.onlyState === "nonfinal") {
const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
dgs = await tx.depositGroups.indexes.byStatus.getAll(keyRange);
} else {
@@ -1978,8 +2011,8 @@ async function iterRecordsForDenomLoss(
let dgs: DenomLossEventRecord[];
if (filter.onlyState === "nonfinal") {
const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
dgs = await tx.denomLossEvents.indexes.byStatus.getAll(keyRange);
} else {
@@ -1998,8 +2031,8 @@ async function iterRecordsForRefund(
): Promise<void> {
if (filter.onlyState === "nonfinal") {
const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
await tx.refundGroups.indexes.byStatus.iter(keyRange).forEachAsync(f);
} else {
@@ -2014,8 +2047,8 @@ async function iterRecordsForPurchase(
): Promise<void> {
if (filter.onlyState === "nonfinal") {
const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
await tx.purchases.indexes.byStatus.iter(keyRange).forEachAsync(f);
} else {
@@ -2030,8 +2063,8 @@ async function iterRecordsForPeerPullCredit(
): Promise<void> {
if (filter.onlyState === "nonfinal") {
const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
await tx.peerPullCredit.indexes.byStatus.iter(keyRange).forEachAsync(f);
} else {
@@ -2046,8 +2079,8 @@ async function iterRecordsForPeerPullDebit(
): Promise<void> {
if (filter.onlyState === "nonfinal") {
const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
await tx.peerPullDebit.indexes.byStatus.iter(keyRange).forEachAsync(f);
} else {
@@ -2062,8 +2095,8 @@ async function iterRecordsForPeerPushDebit(
): Promise<void> {
if (filter.onlyState === "nonfinal") {
const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
await tx.peerPushDebit.indexes.byStatus.iter(keyRange).forEachAsync(f);
} else {
@@ -2078,8 +2111,8 @@ async function iterRecordsForPeerPushCredit(
): Promise<void> {
if (filter.onlyState === "nonfinal") {
const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
await tx.peerPushCredit.indexes.byStatus.iter(keyRange).forEachAsync(f);
} else {
diff --git a/packages/taler-wallet-core/src/versions.ts b/packages/taler-wallet-core/src/versions.ts
index d33a23cdd..8b4b24351 100644
--- a/packages/taler-wallet-core/src/versions.ts
+++ b/packages/taler-wallet-core/src/versions.ts
@@ -29,13 +29,6 @@ export const WALLET_EXCHANGE_PROTOCOL_VERSION = "17:0:0";
export const WALLET_MERCHANT_PROTOCOL_VERSION = "5:0:1";
/**
- * Protocol version spoken with the bank (bank integration API).
- *
- * Uses libtool's current:revision:age versioning.
- */
-export const WALLET_BANK_INTEGRATION_PROTOCOL_VERSION = "1:0:0";
-
-/**
* Protocol version spoken with the bank (corebank API).
*
* Uses libtool's current:revision:age versioning.
@@ -52,7 +45,7 @@ export const WALLET_BANK_CONVERSION_API_PROTOCOL_VERSION = "2:0:0";
/**
* Libtool version of the wallet-core API.
*/
-export const WALLET_CORE_API_PROTOCOL_VERSION = "5:0:0";
+export const WALLET_CORE_API_PROTOCOL_VERSION = "7:0:0";
/**
* Libtool rules:
diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts
index 1bcab801c..aa88331ea 100644
--- a/packages/taler-wallet-core/src/wallet-api-types.ts
+++ b/packages/taler-wallet-core/src/wallet-api-types.ts
@@ -123,7 +123,6 @@ import {
StartRefundQueryForUriResponse,
StartRefundQueryRequest,
StoredBackupList,
- TalerMerchantApi,
TestPayArgs,
TestPayResult,
TestingGetDenomStatsRequest,
@@ -277,6 +276,7 @@ export enum WalletApiOperation {
TestingGetDenomStats = "testingGetDenomStats",
TestingPing = "testingPing",
TestingGetReserveHistory = "testingGetReserveHistory",
+ TestingResetAllRetries = "testingResetAllRetries",
}
// group: Initialization
@@ -1213,6 +1213,16 @@ export type TestingGetReserveHistoryOp = {
};
/**
+ * Reset all task/transaction retries,
+ * resulting in immediate re-try of all operations.
+ */
+export type TestingResetAllRetriesOp = {
+ op: WalletApiOperation.TestingResetAllRetries;
+ request: EmptyObject;
+ response: EmptyObject;
+};
+
+/**
* Get stats about an exchange denomination.
*/
export type TestingGetDenomStatsOp = {
@@ -1356,6 +1366,7 @@ export type WalletOperations = {
[WalletApiOperation.ConfirmWithdrawal]: ConfirmWithdrawalOp;
[WalletApiOperation.CanonicalizeBaseUrl]: CanonicalizeBaseUrlOp;
[WalletApiOperation.TestingGetReserveHistory]: TestingGetReserveHistoryOp;
+ [WalletApiOperation.TestingResetAllRetries]: TestingResetAllRetriesOp;
[WalletApiOperation.HintNetworkAvailability]: HintNetworkAvailabilityOp;
};
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index 68da15410..7a69fcb21 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -56,6 +56,7 @@ import {
PrepareWithdrawExchangeResponse,
RecoverStoredBackupRequest,
StoredBackupList,
+ TalerBankIntegrationHttpClient,
TalerError,
TalerErrorCode,
TalerProtocolTimestamp,
@@ -107,6 +108,7 @@ import {
codecForGetExchangeTosRequest,
codecForGetWithdrawalDetailsForAmountRequest,
codecForGetWithdrawalDetailsForUri,
+ codecForHintNetworkAvailabilityRequest,
codecForImportDbRequest,
codecForInitRequest,
codecForInitiatePeerPullPaymentRequest,
@@ -265,6 +267,7 @@ import {
TaskScheduler,
TaskSchedulerImpl,
convertTaskToTransactionId,
+ getActiveTaskIds,
listTaskForTransactionId,
} from "./shepherd.js";
import {
@@ -287,12 +290,12 @@ import {
getWithdrawalTransactionByUri,
parseTransactionIdentifier,
resumeTransaction,
+ retryAll,
retryTransaction,
suspendTransaction,
} from "./transactions.js";
import {
WALLET_BANK_CONVERSION_API_PROTOCOL_VERSION,
- WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
WALLET_COREBANK_API_PROTOCOL_VERSION,
WALLET_CORE_API_PROTOCOL_VERSION,
WALLET_EXCHANGE_PROTOCOL_VERSION,
@@ -476,7 +479,10 @@ async function setCoinSuspended(
c.denomPubHash,
c.maxAge,
]);
- checkDbInvariant(!!coinAvailability);
+ checkDbInvariant(
+ !!coinAvailability,
+ `no denom info for ${c.denomPubHash} age ${c.maxAge}`,
+ );
if (suspended) {
if (c.status !== CoinStatus.Fresh) {
return;
@@ -721,12 +727,11 @@ async function dispatchRequestInternal(
case WalletApiOperation.InitWallet: {
const req = codecForInitRequest().decode(payload);
- logger.info(`init request: ${j2s(req)}`);
-
- if (wex.ws.initCalled) {
- logger.info("initializing wallet (repeat initialization)");
- } else {
- logger.info("initializing wallet (first initialization)");
+ if (logger.shouldLogTrace()) {
+ const initType = wex.ws.initCalled
+ ? "repeat initialization"
+ : "first initialization";
+ logger.trace(`init request (${initType}): ${j2s(req)}`);
}
// Write to the DB to make sure that we're failing early in
@@ -744,7 +749,6 @@ async function dispatchRequestInternal(
innerError: getErrorDetailFromException(e),
});
}
-
wex.ws.initWithConfig(applyRunConfigDefaults(req.config));
if (wex.ws.config.testing.skipDefaults) {
@@ -757,8 +761,11 @@ async function dispatchRequestInternal(
versionInfo: getVersion(wex),
};
- // After initialization, task loop should run.
- await wex.taskScheduler.ensureRunning();
+ if (req.config?.lazyTaskLoop) {
+ logger.trace("lazily starting task loop");
+ } else {
+ await wex.taskScheduler.ensureRunning();
+ }
wex.ws.initCalled = true;
return resp;
@@ -996,6 +1003,7 @@ async function dispatchRequestInternal(
talerWithdrawUri: req.talerWithdrawUri,
forcedDenomSel: req.forcedDenomSel,
restrictAge: req.restrictAge,
+ amount: req.amount,
});
}
case WalletApiOperation.ConfirmWithdrawal: {
@@ -1005,10 +1013,7 @@ async function dispatchRequestInternal(
case WalletApiOperation.PrepareBankIntegratedWithdrawal: {
const req =
codecForPrepareBankIntegratedWithdrawalRequest().decode(payload);
- return prepareBankIntegratedWithdrawal(wex, {
- talerWithdrawUri: req.talerWithdrawUri,
- selectedExchange: req.selectedExchange,
- });
+ return prepareBankIntegratedWithdrawal(wex, req);
}
case WalletApiOperation.GetExchangeTos: {
const req = codecForGetExchangeTosRequest().decode(payload);
@@ -1046,6 +1051,10 @@ async function dispatchRequestInternal(
const req = codecForPrepareWithdrawExchangeRequest().decode(payload);
return handlePrepareWithdrawExchange(wex, req);
}
+ case WalletApiOperation.CheckPayForTemplate: {
+ const req = codecForCheckPayTemplateRequest().decode(payload);
+ return await checkPayForTemplate(wex, req);
+ }
case WalletApiOperation.PreparePayForUri: {
const req = codecForPreparePayRequest().decode(payload);
return await preparePayForUri(wex, req.talerPayUri);
@@ -1054,10 +1063,6 @@ async function dispatchRequestInternal(
const req = codecForPreparePayTemplateRequest().decode(payload);
return preparePayForTemplate(wex, req);
}
- case WalletApiOperation.CheckPayForTemplate: {
- const req = codecForCheckPayTemplateRequest().decode(payload);
- return checkPayForTemplate(wex, req);
- }
case WalletApiOperation.ConfirmPay: {
const req = codecForConfirmPayRequest().decode(payload);
let transactionId;
@@ -1085,7 +1090,7 @@ async function dispatchRequestInternal(
return {};
}
case WalletApiOperation.GetActiveTasks: {
- const allTasksId = wex.taskScheduler.getActiveTasks();
+ const allTasksId = (await getActiveTaskIds(wex.ws)).taskIds;
const tasksInfo = await Promise.all(
allTasksId.map(async (id) => {
@@ -1234,10 +1239,16 @@ async function dispatchRequestInternal(
await loadBackupRecovery(wex, req);
return {};
}
- // case WalletApiOperation.GetPlanForOperation: {
- // const req = codecForGetPlanForOperationRequest().decode(payload);
- // return await getPlanForOperation(ws, req);
- // }
+ case WalletApiOperation.HintNetworkAvailability: {
+ const req = codecForHintNetworkAvailabilityRequest().decode(payload);
+ if (req.isNetworkAvailable) {
+ await retryAll(wex);
+ } else {
+ // We're not doing anything right now, but we could stop showing
+ // certain errors!
+ }
+ return {};
+ }
case WalletApiOperation.ConvertDepositAmount: {
const req = codecForConvertAmountRequest.decode(payload);
return await convertDepositAmount(wex, req);
@@ -1388,7 +1399,10 @@ async function dispatchRequestInternal(
return;
}
wex.ws.exchangeCache.clear();
- checkDbInvariant(!!existingRec.id);
+ checkDbInvariant(
+ !!existingRec.id,
+ `no global exchange for ${j2s(key)}`,
+ );
await tx.globalCurrencyExchanges.delete(existingRec.id);
},
);
@@ -1421,6 +1435,9 @@ async function dispatchRequestInternal(
await waitTasksDone(wex);
return {};
}
+ case WalletApiOperation.TestingResetAllRetries:
+ await retryAll(wex);
+ return {};
case WalletApiOperation.RemoveGlobalCurrencyAuditor: {
const req = codecForRemoveGlobalCurrencyAuditorRequest().decode(payload);
await wex.db.runReadWriteTx(
@@ -1434,7 +1451,10 @@ async function dispatchRequestInternal(
if (!existingRec) {
return;
}
- checkDbInvariant(!!existingRec.id);
+ checkDbInvariant(
+ !!existingRec.id,
+ `no global currency for ${j2s(key)}`,
+ );
await tx.globalCurrencyAuditors.delete(existingRec.id);
wex.ws.exchangeCache.clear();
},
@@ -1569,9 +1589,9 @@ export function getVersion(wex: WalletExecutionContext): WalletCoreVersion {
exchange: WALLET_EXCHANGE_PROTOCOL_VERSION,
merchant: WALLET_MERCHANT_PROTOCOL_VERSION,
bankConversionApiRange: WALLET_BANK_CONVERSION_API_PROTOCOL_VERSION,
- bankIntegrationApiRange: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
+ bankIntegrationApiRange: TalerBankIntegrationHttpClient.PROTOCOL_VERSION,
corebankApiRange: WALLET_COREBANK_API_PROTOCOL_VERSION,
- bank: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
+ bank: TalerBankIntegrationHttpClient.PROTOCOL_VERSION,
devMode: wex.ws.config.testing.devModeActive,
};
return result;
@@ -1629,10 +1649,10 @@ async function handleCoreApiRequest(
if (!ws.initCalled) {
throw Error("init must be called first");
}
- // Might be lazily initialized!
- await ws.taskScheduler.ensureRunning();
}
+ await ws.ensureWalletDbOpen();
+
let wex: WalletExecutionContext;
let oc: ObservabilityContext;
@@ -1832,7 +1852,7 @@ class WalletDbTriggerSpec implements TriggerSpec {
if (info.mode !== "readwrite") {
return;
}
- logger.info(
+ logger.trace(
`in after commit callback for readwrite, modified ${j2s([
...info.modifiedStores,
])}`,
@@ -1924,8 +1944,6 @@ export class InternalWalletState {
initWithConfig(newConfig: WalletRunConfig): void {
this._config = newConfig;
- logger.info(`setting new config to ${j2s(newConfig)}`);
-
this._http = this.httpFactory(newConfig);
if (this.config.testing.devModeActive) {
diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts
index 4a7c7873c..8bc4aafd1 100644
--- a/packages/taler-wallet-core/src/withdraw.ts
+++ b/packages/taler-wallet-core/src/withdraw.ts
@@ -44,6 +44,8 @@ import {
Duration,
EddsaPrivateKeyString,
ExchangeBatchWithdrawRequest,
+ ExchangeListItem,
+ ExchangeTosStatus,
ExchangeUpdateStatus,
ExchangeWireAccount,
ExchangeWithdrawBatchResponse,
@@ -114,8 +116,10 @@ import {
TransitionResult,
TransitionResultType,
constructTaskIdentifier,
+ genericWaitForState,
makeCoinAvailable,
makeCoinsVisible,
+ requireExchangeTosAcceptedOrThrow,
} from "./common.js";
import { EddsaKeypair } from "./crypto/cryptoImplementation.js";
import {
@@ -149,6 +153,7 @@ import {
getExchangePaytoUri,
getExchangeWireDetailsInTx,
listExchanges,
+ lookupExchangeByUri,
markExchangeUsed,
} from "./exchanges.js";
import { DbAccess } from "./query.js";
@@ -159,10 +164,7 @@ import {
notifyTransition,
parseTransactionIdentifier,
} from "./transactions.js";
-import {
- WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
- WALLET_EXCHANGE_PROTOCOL_VERSION,
-} from "./versions.js";
+import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "./versions.js";
import { WalletExecutionContext, getDenomInfo } from "./wallet.js";
/**
@@ -343,9 +345,11 @@ export class WithdrawTransactionContext implements TransactionContext {
"exchanges" as const,
"exchangeDetails" as const,
];
- let stores = opts.extraStores
+ const stores = opts.extraStores
? [...baseStores, ...opts.extraStores]
: baseStores;
+
+ let errorThrown: Error | undefined;
const transitionInfo = await this.wex.db.runReadWriteTx(
{ storeNames: stores },
async (tx) => {
@@ -358,7 +362,17 @@ export class WithdrawTransactionContext implements TransactionContext {
major: TransactionMajorState.None,
};
}
- const res = await f(wgRec, tx);
+ let res: TransitionResult<WithdrawalGroupRecord> | undefined;
+ try {
+ res = await f(wgRec, tx);
+ } catch (error) {
+ if (error instanceof Error) {
+ errorThrown = error;
+ }
+ return undefined;
+ }
+
+ // const res = await f(wgRec, tx);
switch (res.type) {
case TransitionResultType.Transition: {
await tx.withdrawalGroups.put(res.rec);
@@ -383,6 +397,9 @@ export class WithdrawTransactionContext implements TransactionContext {
}
},
);
+ if (errorThrown) {
+ throw errorThrown;
+ }
notifyTransition(this.wex, this.transactionId, transitionInfo);
return transitionInfo;
}
@@ -715,15 +732,35 @@ export function computeWithdrawalTransactionActions(
case WithdrawalGroupStatus.Done:
return [TransactionAction.Delete];
case WithdrawalGroupStatus.PendingRegisteringBank:
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case WithdrawalGroupStatus.PendingReady:
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case WithdrawalGroupStatus.PendingQueryingStatus:
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case WithdrawalGroupStatus.PendingWaitConfirmBank:
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case WithdrawalGroupStatus.AbortingBank:
- return [TransactionAction.Suspend, TransactionAction.Fail];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Fail,
+ ];
case WithdrawalGroupStatus.SuspendedAbortingBank:
return [TransactionAction.Resume, TransactionAction.Fail];
case WithdrawalGroupStatus.SuspendedQueryingStatus:
@@ -735,9 +772,17 @@ export function computeWithdrawalTransactionActions(
case WithdrawalGroupStatus.SuspendedReady:
return [TransactionAction.Resume, TransactionAction.Abort];
case WithdrawalGroupStatus.PendingAml:
- return [TransactionAction.Resume, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Resume,
+ TransactionAction.Abort,
+ ];
case WithdrawalGroupStatus.PendingKyc:
- return [TransactionAction.Resume, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Resume,
+ TransactionAction.Abort,
+ ];
case WithdrawalGroupStatus.SuspendedAml:
return [TransactionAction.Resume, TransactionAction.Abort];
case WithdrawalGroupStatus.SuspendedKyc:
@@ -842,7 +887,7 @@ export async function getBankWithdrawalInfo(
TalerErrorCode.WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE,
{
bankProtocolVersion: config.version,
- walletProtocolVersion: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
+ walletProtocolVersion: bankApi.PROTOCOL_VERSION,
},
"bank integration protocol version not compatible with wallet",
);
@@ -857,13 +902,48 @@ export async function getBankWithdrawalInfo(
}
const { body: status } = resp;
+ const maxAmount =
+ status.max_amount === undefined
+ ? undefined
+ : Amounts.parseOrThrow(status.max_amount);
+
+ let amount: AmountJson | undefined;
+ let editableAmount = false;
+ if (status.amount !== undefined) {
+ amount = Amounts.parseOrThrow(status.amount);
+ } else {
+ amount =
+ status.suggested_amount === undefined
+ ? undefined
+ : Amounts.parseOrThrow(status.suggested_amount);
+ editableAmount = true;
+ }
+
+ let wireFee: AmountJson | undefined;
+ if (status.card_fees) {
+ wireFee = Amounts.parseOrThrow(status.card_fees);
+ }
+
+ let exchange: string | undefined = undefined;
+ let editableExchange = false;
+ if (status.required_exchange !== undefined) {
+ exchange = status.required_exchange;
+ } else {
+ exchange = status.suggested_exchange;
+ editableExchange = true;
+ }
return {
operationId: uriResult.withdrawalOperationId,
apiBaseUrl: uriResult.bankIntegrationApiBaseUrl,
- amount: Amounts.parseOrThrow(status.amount),
+ currency: config.currency,
+ amount,
+ wireFee,
confirmTransferUrl: status.confirm_transfer_url,
senderWire: status.sender_wire,
- suggestedExchange: status.suggested_exchange,
+ exchange,
+ editableAmount,
+ editableExchange,
+ maxAmount,
wireTypes: status.wire_types,
status: status.status,
};
@@ -917,6 +997,10 @@ async function processPlanchetGenerate(
withdrawalGroup.denomsSel !== undefined,
"can't process uninitialized exchange",
);
+ checkDbInvariant(
+ withdrawalGroup.exchangeBaseUrl !== undefined,
+ "can't get funding uri from uninitialized wg",
+ );
const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl;
let planchet = await wex.db.runReadOnlyTx(
{ storeNames: ["planchets"] },
@@ -958,7 +1042,7 @@ async function processPlanchetGenerate(
return getDenomInfo(wex, tx, exchangeBaseUrl, denomPubHash);
},
);
- checkDbInvariant(!!denom);
+ checkDbInvariant(!!denom, `no denom info for ${denomPubHash}`);
const r = await wex.cryptoApi.createPlanchet({
denomPub: denom.denomPub,
feeWithdraw: Amounts.parseOrThrow(denom.feeWithdraw),
@@ -1121,6 +1205,10 @@ async function processPlanchetExchangeBatchRequest(
logger.info(
`processing planchet exchange batch request ${withdrawalGroup.withdrawalGroupId}, start=${args.coinStartIndex}, len=${args.batchSize}`,
);
+ checkDbInvariant(
+ withdrawalGroup.exchangeBaseUrl !== undefined,
+ "can't get funding uri from uninitialized wg",
+ );
const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl;
const batchReq: ExchangeBatchWithdrawRequest = { planchets: [] };
@@ -1256,6 +1344,10 @@ async function processPlanchetVerifyAndStoreCoin(
resp: ExchangeWithdrawResponse,
): Promise<void> {
const withdrawalGroup = wgContext.wgRecord;
+ checkDbInvariant(
+ withdrawalGroup.exchangeBaseUrl !== undefined,
+ "can't get funding uri from uninitialized wg",
+ );
const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl;
logger.trace(`checking and storing planchet idx=${coinIdx}`);
@@ -1505,6 +1597,10 @@ async function processQueryReserve(
return TaskRunResult.backoff();
}
checkDbInvariant(
+ withdrawalGroup.exchangeBaseUrl !== undefined,
+ "can't get funding uri from uninitialized wg",
+ );
+ checkDbInvariant(
withdrawalGroup.denomsSel !== undefined,
"can't process uninitialized exchange",
);
@@ -1740,6 +1836,10 @@ async function redenominateWithdrawal(
return;
}
checkDbInvariant(
+ wg.exchangeBaseUrl !== undefined,
+ "can't get funding uri from uninitialized wg",
+ );
+ checkDbInvariant(
wg.denomsSel !== undefined,
"can't process uninitialized exchange",
);
@@ -1882,7 +1982,12 @@ async function processWithdrawalGroupPendingReady(
withdrawalGroup.denomsSel !== undefined,
"can't process uninitialized exchange",
);
+ checkDbInvariant(
+ withdrawalGroup.exchangeBaseUrl !== undefined,
+ "can't get funding uri from uninitialized wg",
+ );
const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl;
+ logger.trace(`updating exchange beofre processing wg`);
await fetchFreshExchange(wex, withdrawalGroup.exchangeBaseUrl);
if (withdrawalGroup.denomsSel.selectedDenoms.length === 0) {
@@ -2162,14 +2267,6 @@ export async function getExchangeWithdrawalInfo(
logger.trace("selection done");
- if (selectedDenoms.selectedDenoms.length === 0) {
- throw Error(
- `unable to withdraw from ${exchangeBaseUrl}, can't select denominations for instructed amount (${Amounts.stringify(
- instructedAmount,
- )}`,
- );
- }
-
const exchangeWireAccounts: string[] = [];
for (const account of exchange.wireInfo.accounts) {
@@ -2248,38 +2345,52 @@ export async function getWithdrawalDetailsForUri(
logger.trace(`getting withdrawal details for URI ${talerWithdrawUri}`);
const info = await getBankWithdrawalInfo(wex.http, talerWithdrawUri);
logger.trace(`got bank info`);
- if (info.suggestedExchange) {
+ if (info.exchange) {
try {
// If the exchange entry doesn't exist yet,
// it'll be created as an ephemeral entry.
- await fetchFreshExchange(wex, info.suggestedExchange);
+ await fetchFreshExchange(wex, info.exchange);
} catch (e) {
// We still continued if it failed, as other exchanges might be available.
// We don't want to fail if the bank-suggested exchange is broken/offline.
logger.trace(
- `querying bank-suggested exchange (${info.suggestedExchange}) failed`,
+ `querying bank-suggested exchange (${info.exchange}) failed`,
);
}
}
- const currency = Amounts.currencyOf(info.amount);
+ const currency = info.currency;
- const listExchangesResp = await listExchanges(wex);
- const possibleExchanges = listExchangesResp.exchanges.filter((x) => {
- return (
- x.currency === currency &&
- (x.exchangeUpdateStatus === ExchangeUpdateStatus.Ready ||
- x.exchangeUpdateStatus === ExchangeUpdateStatus.ReadyUpdate)
- );
- });
+ let possibleExchanges: ExchangeListItem[];
+ if (!info.editableExchange && info.exchange !== undefined) {
+ const ex: ExchangeListItem = await lookupExchangeByUri(wex, {
+ exchangeBaseUrl: info.exchange,
+ });
+ possibleExchanges = [ex];
+ } else {
+ const listExchangesResp = await listExchanges(wex);
+
+ possibleExchanges = listExchangesResp.exchanges.filter((x) => {
+ return (
+ x.currency === currency &&
+ (x.exchangeUpdateStatus === ExchangeUpdateStatus.Ready ||
+ x.exchangeUpdateStatus === ExchangeUpdateStatus.ReadyUpdate)
+ );
+ });
+ }
return {
operationId: info.operationId,
confirmTransferUrl: info.confirmTransferUrl,
status: info.status,
- amount: Amounts.stringify(info.amount),
- defaultExchangeBaseUrl: info.suggestedExchange,
+ currency,
+ editableAmount: info.editableAmount,
+ editableExchange: info.editableExchange,
+ maxAmount: info.maxAmount ? Amounts.stringify(info.maxAmount) : undefined,
+ amount: info.amount ? Amounts.stringify(info.amount) : undefined,
+ defaultExchangeBaseUrl: info.exchange,
possibleExchanges,
+ wireFee: info.wireFee ? Amounts.stringify(info.wireFee) : undefined,
};
}
@@ -2306,7 +2417,11 @@ export async function getFundingPaytoUris(
withdrawalGroupId: string,
): Promise<string[]> {
const withdrawalGroup = await tx.withdrawalGroups.get(withdrawalGroupId);
- checkDbInvariant(!!withdrawalGroup);
+ checkDbInvariant(!!withdrawalGroup, `no withdrawal for ${withdrawalGroupId}`);
+ checkDbInvariant(
+ withdrawalGroup.exchangeBaseUrl !== undefined,
+ "can't get funding uri from uninitialized wg",
+ );
checkDbInvariant(
withdrawalGroup.instructedAmount !== undefined,
"can't get funding uri from uninitialized wg",
@@ -2379,6 +2494,7 @@ export function getBankAbortUrl(talerWithdrawUri: string): string {
async function registerReserveWithBank(
wex: WalletExecutionContext,
withdrawalGroupId: string,
+ isFlexibleAmount: boolean,
): Promise<void> {
const withdrawalGroup = await wex.db.runReadOnlyTx(
{ storeNames: ["withdrawalGroups"] },
@@ -2407,7 +2523,11 @@ async function registerReserveWithBank(
const reqBody = {
reserve_pub: withdrawalGroup.reservePub,
selected_exchange: bankInfo.exchangePaytoUri,
- };
+ } as any;
+ if (isFlexibleAmount) {
+ reqBody.amount = withdrawalGroup.instructedAmount;
+ }
+ logger.trace(`isFlexibleAmount: ${isFlexibleAmount}`);
logger.info(`registering reserve with bank: ${j2s(reqBody)}`);
const httpResp = await wex.http.fetch(bankStatusUrl, {
method: "POST",
@@ -2516,7 +2636,9 @@ async function processBankRegisterReserve(
// FIXME: Put confirm transfer URL in the DB!
- await registerReserveWithBank(wex, withdrawalGroupId);
+ const isFlexibleAmount = status.amount == null;
+
+ await registerReserveWithBank(wex, withdrawalGroupId, isFlexibleAmount);
return TaskRunResult.progress();
}
@@ -2553,6 +2675,7 @@ async function processReserveBankStatus(
uriResult.bankIntegrationApiBaseUrl,
);
bankStatusUrl.searchParams.set("long_poll_ms", "30000");
+ bankStatusUrl.searchParams.set("old_state", "selected");
logger.info(`long-polling for withdrawal operation at ${bankStatusUrl.href}`);
const statusResp = await wex.http.fetch(bankStatusUrl.href, {
@@ -2655,7 +2778,7 @@ export async function internalPrepareCreateWithdrawalGroup(
args: {
reserveStatus: WithdrawalGroupStatus;
amount?: AmountJson;
- exchangeBaseUrl: string;
+ exchangeBaseUrl: string | undefined;
forcedWithdrawalGroupId?: string;
forcedDenomSel?: ForcedDenomSel;
reserveKeyPair?: EddsaKeypair;
@@ -2696,7 +2819,7 @@ export async function internalPrepareCreateWithdrawalGroup(
let initialDenomSel: DenomSelectionState | undefined;
const denomSelUid = encodeCrock(getRandomBytes(16));
- if (amount !== undefined) {
+ if (amount !== undefined && exchangeBaseUrl !== undefined) {
initialDenomSel = await getInitialDenomsSelection(
wex,
exchangeBaseUrl,
@@ -2727,7 +2850,9 @@ export async function internalPrepareCreateWithdrawalGroup(
wgInfo: args.wgInfo,
};
- await fetchFreshExchange(wex, exchangeBaseUrl);
+ if (exchangeBaseUrl !== undefined) {
+ await fetchFreshExchange(wex, exchangeBaseUrl);
+ }
const transactionId = constructTransactionIdentifier({
tag: TransactionType.Withdrawal,
@@ -2737,12 +2862,13 @@ export async function internalPrepareCreateWithdrawalGroup(
return {
withdrawalGroup,
transactionId,
- creationInfo: !amount
- ? undefined
- : {
- amount,
- canonExchange: exchangeBaseUrl,
- },
+ creationInfo:
+ !amount || !exchangeBaseUrl
+ ? undefined
+ : {
+ amount,
+ canonExchange: exchangeBaseUrl,
+ },
};
}
@@ -2772,8 +2898,8 @@ export async function internalPerformCreateWithdrawalGroup(
if (existingWg) {
return {
withdrawalGroup: existingWg,
- exchangeNotif: undefined,
transitionInfo: undefined,
+ exchangeNotif: undefined,
};
}
await tx.withdrawalGroups.add(withdrawalGroup);
@@ -2789,7 +2915,21 @@ export async function internalPerformCreateWithdrawalGroup(
exchangeNotif: undefined,
};
}
- const exchange = await tx.exchanges.get(prep.creationInfo.canonExchange);
+ return internalPerformExchangeWasUsed(
+ wex,
+ tx,
+ prep.creationInfo.canonExchange,
+ withdrawalGroup,
+ );
+}
+
+export async function internalPerformExchangeWasUsed(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadWriteTransaction<["exchanges"]>,
+ canonExchange: string,
+ withdrawalGroup: WithdrawalGroupRecord,
+): Promise<PerformCreateWithdrawalGroupResult> {
+ const exchange = await tx.exchanges.get(canonExchange);
if (exchange) {
exchange.lastWithdrawal = timestampPreciseToDb(TalerPreciseTimestamp.now());
await tx.exchanges.put(exchange);
@@ -2805,11 +2945,7 @@ export async function internalPerformCreateWithdrawalGroup(
newTxState,
};
- const exchangeUsedRes = await markExchangeUsed(
- wex,
- tx,
- prep.creationInfo.canonExchange,
- );
+ const exchangeUsedRes = await markExchangeUsed(wex, tx, canonExchange);
const ctx = new WithdrawTransactionContext(
wex,
@@ -2837,7 +2973,7 @@ export async function internalCreateWithdrawalGroup(
wex: WalletExecutionContext,
args: {
reserveStatus: WithdrawalGroupStatus;
- exchangeBaseUrl: string;
+ exchangeBaseUrl: string | undefined;
amount?: AmountJson;
forcedWithdrawalGroupId?: string;
forcedDenomSel?: ForcedDenomSel;
@@ -2883,7 +3019,6 @@ export async function prepareBankIntegratedWithdrawal(
wex: WalletExecutionContext,
req: {
talerWithdrawUri: string;
- selectedExchange?: string;
},
): Promise<PrepareBankIntegratedWithdrawalResponse> {
const existingWithdrawalGroup = await wex.db.runReadOnlyTx(
@@ -2912,12 +3047,6 @@ export async function prepareBankIntegratedWithdrawal(
const info = await getWithdrawalDetailsForUri(wex, req.talerWithdrawUri);
- const exchangeBaseUrl =
- req.selectedExchange ?? withdrawInfo.suggestedExchange;
- if (!exchangeBaseUrl) {
- return { info };
- }
-
/**
* Withdrawal group without exchange and amount
* this is an special case when the user haven't yet
@@ -2926,7 +3055,7 @@ export async function prepareBankIntegratedWithdrawal(
* same URI
*/
const withdrawalGroup = await internalCreateWithdrawalGroup(wex, {
- exchangeBaseUrl,
+ exchangeBaseUrl: undefined,
wgInfo: {
withdrawalType: WithdrawalRecordType.BankIntegrated,
bankInfo: {
@@ -2935,6 +3064,7 @@ export async function prepareBankIntegratedWithdrawal(
timestampBankConfirmed: undefined,
timestampReserveInfoPosted: undefined,
wireTypes: withdrawInfo.wireTypes,
+ currency: withdrawInfo.currency,
},
},
reserveStatus: WithdrawalGroupStatus.DialogProposed,
@@ -2957,6 +3087,9 @@ export async function confirmWithdrawal(
req: ConfirmWithdrawalRequest,
): Promise<void> {
const parsedTx = parseTransactionIdentifier(req.transactionId);
+ const selectedExchange = req.exchangeBaseUrl;
+ const instructedAmount = Amounts.parseOrThrow(req.amount);
+
if (parsedTx?.tag !== TransactionType.Withdrawal) {
throw Error("invalid withdrawal transaction ID");
}
@@ -2978,38 +3111,44 @@ export async function confirmWithdrawal(
throw Error("not a bank integrated withdrawal");
}
- const selectedExchange = req.exchangeBaseUrl;
const exchange = await fetchFreshExchange(wex, selectedExchange);
+ requireExchangeTosAcceptedOrThrow(exchange);
const talerWithdrawUri = withdrawalGroup.wgInfo.bankInfo.talerWithdrawUri;
const confirmUrl = withdrawalGroup.wgInfo.bankInfo.confirmUrl;
/**
- * The only reasong this to be undefined is because it is an old wallet
- * database before adding the wireType field was added
+ * The only reason this could be undefined is because it is an old wallet
+ * database before adding the prepareWithdrawal feature
*/
- let wtypes: string[];
- if (withdrawalGroup.wgInfo.bankInfo.wireTypes === undefined) {
+ let bankWireTypes: string[];
+ let bankCurrency: string;
+ if (
+ withdrawalGroup.wgInfo.bankInfo.wireTypes === undefined ||
+ withdrawalGroup.wgInfo.bankInfo.currency === undefined
+ ) {
const withdrawInfo = await getBankWithdrawalInfo(
wex.http,
talerWithdrawUri,
);
- wtypes = withdrawInfo.wireTypes;
+ bankWireTypes = withdrawInfo.wireTypes;
+ bankCurrency = withdrawInfo.currency;
} else {
- wtypes = withdrawalGroup.wgInfo.bankInfo.wireTypes;
+ bankWireTypes = withdrawalGroup.wgInfo.bankInfo.wireTypes;
+ bankCurrency = withdrawalGroup.wgInfo.bankInfo.currency;
}
const exchangePaytoUri = await getExchangePaytoUri(
wex,
selectedExchange,
- wtypes,
+ bankWireTypes,
);
const withdrawalAccountList = await fetchWithdrawalAccountInfo(
wex,
{
exchange,
- instructedAmount: Amounts.parseOrThrow(req.amount),
+ instructedAmount,
},
wex.cancellationToken,
);
@@ -3020,23 +3159,34 @@ export async function confirmWithdrawal(
);
const initalDenoms = await getInitialDenomsSelection(
wex,
- req.exchangeBaseUrl,
- Amounts.parseOrThrow(req.amount),
+ exchange.exchangeBaseUrl,
+ instructedAmount,
req.forcedDenomSel,
);
- ctx.transition({}, async (rec) => {
+ let pending = false;
+ await ctx.transition({}, async (rec) => {
if (!rec) {
return TransitionResult.stay();
}
switch (rec.status) {
+ case WithdrawalGroupStatus.PendingWaitConfirmBank: {
+ pending = true;
+ return TransitionResult.stay();
+ }
+ case WithdrawalGroupStatus.AbortedOtherWallet: {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
+ {},
+ );
+ }
case WithdrawalGroupStatus.DialogProposed: {
- rec.exchangeBaseUrl = req.exchangeBaseUrl;
+ rec.exchangeBaseUrl = exchange.exchangeBaseUrl;
rec.instructedAmount = req.amount;
+ rec.restrictAge = req.restrictAge;
rec.denomsSel = initalDenoms;
rec.rawWithdrawalAmount = initalDenoms.totalWithdrawCost;
rec.effectiveWithdrawalAmount = initalDenoms.totalCoinValue;
- rec.restrictAge = req.restrictAge;
rec.wgInfo = {
withdrawalType: WithdrawalRecordType.BankIntegrated,
@@ -3047,20 +3197,50 @@ export async function confirmWithdrawal(
confirmUrl: confirmUrl,
timestampBankConfirmed: undefined,
timestampReserveInfoPosted: undefined,
- wireTypes: wtypes,
+ wireTypes: bankWireTypes,
+ currency: bankCurrency,
},
};
-
+ pending = true;
rec.status = WithdrawalGroupStatus.PendingRegisteringBank;
return TransitionResult.transition(rec);
}
- default:
- throw Error("unable to confirm withdrawal in current state");
+ default: {
+ throw Error(
+ `unable to confirm withdrawal in current state: ${rec.status}`,
+ );
+ }
}
});
await wex.taskScheduler.resetTaskRetries(ctx.taskId);
- wex.taskScheduler.startShepherdTask(ctx.taskId);
+
+ wex.ws.notify({
+ type: NotificationType.BalanceChange,
+ hintTransactionId: ctx.transactionId,
+ });
+
+ const res = await wex.db.runReadWriteTx(
+ {
+ storeNames: ["exchanges"],
+ },
+ async (tx) => {
+ const r = await internalPerformExchangeWasUsed(
+ wex,
+ tx,
+ exchange.exchangeBaseUrl,
+ withdrawalGroup,
+ );
+ return r;
+ },
+ );
+ if (res.exchangeNotif) {
+ wex.ws.notify(res.exchangeNotif);
+ }
+
+ if (pending) {
+ await waitWithdrawalRegistered(wex, ctx);
+ }
}
/**
@@ -3080,181 +3260,119 @@ export async function acceptWithdrawalFromUri(
selectedExchange: string;
forcedDenomSel?: ForcedDenomSel;
restrictAge?: number;
+ amount?: AmountLike;
},
): Promise<AcceptWithdrawalResponse> {
const selectedExchange = req.selectedExchange;
logger.info(
- `accepting withdrawal via ${req.talerWithdrawUri}, canonicalized selected exchange ${selectedExchange}`,
- );
- const existingWithdrawalGroup = await wex.db.runReadOnlyTx(
- { storeNames: ["withdrawalGroups"] },
- async (tx) => {
- return await tx.withdrawalGroups.indexes.byTalerWithdrawUri.get(
- req.talerWithdrawUri,
- );
- },
+ `preparing withdrawal via ${req.talerWithdrawUri}, canonicalized selected exchange ${selectedExchange}`,
);
- if (existingWithdrawalGroup) {
- let url: string | undefined;
- if (
- existingWithdrawalGroup.wgInfo.withdrawalType ===
- WithdrawalRecordType.BankIntegrated
- ) {
- url = existingWithdrawalGroup.wgInfo.bankInfo.confirmUrl;
+ const p = await prepareBankIntegratedWithdrawal(wex, {
+ talerWithdrawUri: req.talerWithdrawUri,
+ });
+
+ let amount: AmountString;
+ if (p.info.amount == null) {
+ if (req.amount == null) {
+ throw Error(
+ "amount required, as withdrawal operation has flexible amount",
+ );
}
- return {
- reservePub: existingWithdrawalGroup.reservePub,
- confirmTransferUrl: url,
- transactionId: constructTransactionIdentifier({
- tag: TransactionType.Withdrawal,
- withdrawalGroupId: existingWithdrawalGroup.withdrawalGroupId,
- }),
- };
+ amount = req.amount as AmountString;
+ } else {
+ if (req.amount != null && Amounts.cmp(req.amount, p.info.amount) != 0) {
+ throw Error(
+ "mismatched amount, amount is fixed by bank but client provided different amount",
+ );
+ }
+ amount = p.info.amount;
}
- const exchange = await fetchFreshExchange(wex, selectedExchange);
- const withdrawInfo = await getBankWithdrawalInfo(
- wex.http,
- req.talerWithdrawUri,
- );
- const exchangePaytoUri = await getExchangePaytoUri(
- wex,
- selectedExchange,
- withdrawInfo.wireTypes,
- );
-
- const withdrawalAccountList = await fetchWithdrawalAccountInfo(
- wex,
- {
- exchange,
- instructedAmount: withdrawInfo.amount,
- },
- CancellationToken.CONTINUE,
- );
-
- const withdrawalGroup = await internalCreateWithdrawalGroup(wex, {
- amount: withdrawInfo.amount,
- exchangeBaseUrl: req.selectedExchange,
- wgInfo: {
- withdrawalType: WithdrawalRecordType.BankIntegrated,
- exchangeCreditAccounts: withdrawalAccountList,
- bankInfo: {
- exchangePaytoUri,
- talerWithdrawUri: req.talerWithdrawUri,
- confirmUrl: withdrawInfo.confirmTransferUrl,
- timestampBankConfirmed: undefined,
- timestampReserveInfoPosted: undefined,
- wireTypes: withdrawInfo.wireTypes,
- },
- },
+ logger.info(`confirming withdrawal with tx ${p.transactionId}`);
+ await confirmWithdrawal(wex, {
+ amount: Amounts.stringify(amount),
+ exchangeBaseUrl: selectedExchange,
+ transactionId: p.transactionId,
restrictAge: req.restrictAge,
forcedDenomSel: req.forcedDenomSel,
- reserveStatus: WithdrawalGroupStatus.PendingRegisteringBank,
});
- const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;
-
- const ctx = new WithdrawTransactionContext(wex, withdrawalGroupId);
-
- wex.ws.notify({
- type: NotificationType.BalanceChange,
- hintTransactionId: ctx.transactionId,
- });
-
- await waitWithdrawalRegistered(wex, ctx);
+ const newWithdrawralGroup = await wex.db.runReadOnlyTx(
+ { storeNames: ["withdrawalGroups"] },
+ async (tx) => {
+ return await tx.withdrawalGroups.indexes.byTalerWithdrawUri.get(
+ req.talerWithdrawUri,
+ );
+ },
+ );
- wex.taskScheduler.startShepherdTask(ctx.taskId);
+ checkDbInvariant(
+ newWithdrawralGroup !== undefined,
+ "withdrawal don't exist after confirm",
+ );
return {
- reservePub: withdrawalGroup.reservePub,
- confirmTransferUrl: withdrawInfo.confirmTransferUrl,
- transactionId: ctx.transactionId,
+ reservePub: newWithdrawralGroup.reservePub,
+ confirmTransferUrl: p.info.confirmTransferUrl,
+ transactionId: p.transactionId,
};
}
-async function internalWaitWithdrawalRegistered(
+async function waitWithdrawalRegistered(
wex: WalletExecutionContext,
ctx: WithdrawTransactionContext,
- withdrawalNotifFlag: AsyncFlag,
): Promise<void> {
- while (true) {
- const { withdrawalRec, retryRec } = await wex.db.runReadOnlyTx(
- { storeNames: ["withdrawalGroups", "operationRetries"] },
- async (tx) => {
- return {
- withdrawalRec: await tx.withdrawalGroups.get(ctx.withdrawalGroupId),
- retryRec: await tx.operationRetries.get(ctx.taskId),
- };
- },
- );
+ await genericWaitForState(wex, {
+ async checkState(): Promise<boolean> {
+ const { withdrawalRec, retryRec } = await wex.db.runReadOnlyTx(
+ { storeNames: ["withdrawalGroups", "operationRetries"] },
+ async (tx) => {
+ return {
+ withdrawalRec: await tx.withdrawalGroups.get(ctx.withdrawalGroupId),
+ retryRec: await tx.operationRetries.get(ctx.taskId),
+ };
+ },
+ );
- if (!withdrawalRec) {
- throw Error("withdrawal not found anymore");
- }
+ if (!withdrawalRec) {
+ throw Error("withdrawal not found anymore");
+ }
- switch (withdrawalRec.status) {
- case WithdrawalGroupStatus.FailedBankAborted:
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
- {},
- );
- case WithdrawalGroupStatus.PendingKyc:
- case WithdrawalGroupStatus.PendingAml:
- case WithdrawalGroupStatus.PendingQueryingStatus:
- case WithdrawalGroupStatus.PendingReady:
- case WithdrawalGroupStatus.Done:
- case WithdrawalGroupStatus.PendingWaitConfirmBank:
- return;
- case WithdrawalGroupStatus.PendingRegisteringBank:
- break;
- default: {
- if (retryRec) {
- if (retryRec.lastError) {
- throw TalerError.fromUncheckedDetail(retryRec.lastError);
- } else {
- throw Error("withdrawal unexpectedly pending");
+ switch (withdrawalRec.status) {
+ case WithdrawalGroupStatus.FailedBankAborted:
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
+ {},
+ );
+ case WithdrawalGroupStatus.PendingKyc:
+ case WithdrawalGroupStatus.PendingAml:
+ case WithdrawalGroupStatus.PendingQueryingStatus:
+ case WithdrawalGroupStatus.PendingReady:
+ case WithdrawalGroupStatus.Done:
+ case WithdrawalGroupStatus.PendingWaitConfirmBank:
+ return true;
+ case WithdrawalGroupStatus.PendingRegisteringBank:
+ break;
+ default: {
+ if (retryRec) {
+ if (retryRec.lastError) {
+ throw TalerError.fromUncheckedDetail(retryRec.lastError);
+ } else {
+ throw Error("withdrawal unexpectedly pending");
+ }
}
}
}
- }
-
- await withdrawalNotifFlag.wait();
- withdrawalNotifFlag.reset();
- }
-}
-
-async function waitWithdrawalRegistered(
- wex: WalletExecutionContext,
- ctx: WithdrawTransactionContext,
-): Promise<void> {
- // FIXME: Doesn't support cancellation yet
- // FIXME: We should use Symbol.dispose magic here for cleanup!
-
- const withdrawalNotifFlag = new AsyncFlag();
- // Raise exchangeNotifFlag whenever we get a notification
- // about our exchange.
- const cancelNotif = wex.ws.addNotificationListener((notif) => {
- if (
- notif.type === NotificationType.TransactionStateTransition &&
- notif.transactionId === ctx.transactionId
- ) {
- logger.info(`raising update notification: ${j2s(notif)}`);
- withdrawalNotifFlag.raise();
- }
+ return false;
+ },
+ filterNotification(notif) {
+ return (
+ notif.type === NotificationType.TransactionStateTransition &&
+ notif.transactionId === ctx.transactionId
+ );
+ },
});
-
- try {
- const res = await internalWaitWithdrawalRegistered(
- wex,
- ctx,
- withdrawalNotifFlag,
- );
- logger.info("done waiting for ready exchange");
- return res;
- } finally {
- cancelNotif();
- }
}
async function fetchAccount(
@@ -3422,7 +3540,7 @@ export async function createManualWithdrawal(
);
const withdrawalGroup = await internalCreateWithdrawalGroup(wex, {
- amount: Amounts.jsonifyAmount(req.amount),
+ amount: amount,
wgInfo: {
withdrawalType: WithdrawalRecordType.BankManual,
exchangeCreditAccounts: withdrawalAccountsList,
@@ -3507,7 +3625,7 @@ async function internalWaitWithdrawalFinal(
// Check if refresh is final
const res = await ctx.wex.db.runReadOnlyTx(
- { storeNames: ["withdrawalGroups", "operationRetries"] },
+ { storeNames: ["withdrawalGroups"] },
async (tx) => {
return {
wg: await tx.withdrawalGroups.get(ctx.withdrawalGroupId),
@@ -3550,7 +3668,7 @@ export async function getWithdrawalDetailsForAmount(
type: ObservabilityEventType.Message,
contents: `Cancelling previous key ${clientCancelKey}`,
});
- prevCts.cancel();
+ prevCts.cancel(`getting details amount`);
} else {
wex.oc.observe({
type: ObservabilityEventType.Message,
diff --git a/packages/taler-wallet-embedded/package.json b/packages/taler-wallet-embedded/package.json
index ee9efafdd..fe64396fb 100644
--- a/packages/taler-wallet-embedded/package.json
+++ b/packages/taler-wallet-embedded/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-wallet-embedded",
- "version": "0.10.7",
+ "version": "0.11.4",
"description": "",
"engines": {
"node": ">=0.18.0"
diff --git a/packages/taler-wallet-embedded/src/wallet-qjs-tests.ts b/packages/taler-wallet-embedded/src/wallet-qjs-tests.ts
new file mode 100644
index 000000000..ca4eb28c0
--- /dev/null
+++ b/packages/taler-wallet-embedded/src/wallet-qjs-tests.ts
@@ -0,0 +1,118 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+ (C) 2024 Taler Systems SA
+
+ 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 { userIdentifierDerive } from "@gnu-taler/anastasis-core/lib/crypto.js";
+import { AmountString, j2s } from "@gnu-taler/taler-util";
+import {
+ WalletApiOperation,
+ createNativeWalletHost2,
+} from "@gnu-taler/taler-wallet-core";
+
+export async function testWithGv() {
+ const w = await createNativeWalletHost2({});
+ await w.wallet.client.call(WalletApiOperation.InitWallet, {
+ config: {
+ features: {
+ allowHttp: true,
+ },
+ },
+ });
+ await w.wallet.client.call(WalletApiOperation.RunIntegrationTest, {
+ amountToSpend: "KUDOS:1" as AmountString,
+ amountToWithdraw: "KUDOS:3" as AmountString,
+ corebankApiBaseUrl: "https://bank.demo.taler.net/",
+ exchangeBaseUrl: "https://exchange.demo.taler.net/",
+ merchantBaseUrl: "https://backend.demo.taler.net/",
+ merchantAuthToken: "secret-token:sandbox",
+ });
+ await w.wallet.client.call(WalletApiOperation.TestingWaitTasksDone, {});
+ await w.wallet.client.call(WalletApiOperation.Shutdown, {});
+}
+
+export async function testWithFdold() {
+ const w = await createNativeWalletHost2({});
+ await w.wallet.client.call(WalletApiOperation.InitWallet, {
+ config: {
+ features: {
+ allowHttp: true,
+ },
+ },
+ });
+ await w.wallet.client.call(WalletApiOperation.RunIntegrationTest, {
+ amountToSpend: "TESTKUDOS:1" as AmountString,
+ amountToWithdraw: "TESTKUDOS:3" as AmountString,
+ corebankApiBaseUrl: "https://bank.taler.fdold.eu/",
+ exchangeBaseUrl: "https://exchange.taler.fdold.eu/",
+ merchantBaseUrl: "https://merchant.taler.fdold.eu/",
+ });
+ await w.wallet.client.call(WalletApiOperation.TestingWaitTasksDone, {});
+ await w.wallet.client.call(WalletApiOperation.Shutdown, {});
+}
+
+export async function testWithLocal(path: string) {
+ console.log("running local test");
+ const w = await createNativeWalletHost2({
+ persistentStoragePath: path ?? "walletdb.json",
+ });
+ console.log("created wallet");
+ await w.wallet.client.call(WalletApiOperation.InitWallet, {
+ config: {
+ features: {
+ allowHttp: true,
+ },
+ testing: {
+ skipDefaults: true,
+ },
+ },
+ });
+ console.log("initialized wallet");
+ await w.wallet.client.call(WalletApiOperation.RunIntegrationTest, {
+ amountToSpend: "TESTKUDOS:1" as AmountString,
+ amountToWithdraw: "TESTKUDOS:3" as AmountString,
+ corebankApiBaseUrl: "http://localhost:8082/taler-bank-access/",
+ exchangeBaseUrl: "http://localhost:8081/",
+ merchantBaseUrl: "http://localhost:8083/",
+ });
+ console.log("started integration test");
+ await w.wallet.client.call(WalletApiOperation.TestingWaitTasksDone, {});
+ console.log("done with task loop");
+ await w.wallet.client.call(WalletApiOperation.Shutdown, {});
+ console.log("DB stats:", j2s(w.getDbStats()));
+}
+
+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!");
+}
diff --git a/packages/taler-wallet-embedded/src/wallet-qjs.ts b/packages/taler-wallet-embedded/src/wallet-qjs.ts
index cbda401e9..2780a3cab 100644
--- a/packages/taler-wallet-embedded/src/wallet-qjs.ts
+++ b/packages/taler-wallet-embedded/src/wallet-qjs.ts
@@ -1,6 +1,7 @@
/*
This file is part of GNU Taler
(C) 2019 GNUnet e.V.
+ (C) 2024 Taler Systems SA
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
@@ -28,30 +29,27 @@ import {
mergeDiscoveryAggregate,
reduceAction,
} from "@gnu-taler/anastasis-core";
-import { userIdentifierDerive } from "@gnu-taler/anastasis-core/lib/crypto.js";
import {
- AmountString,
CoreApiMessageEnvelope,
CoreApiResponse,
CoreApiResponseSuccess,
Logger,
- PartialWalletRunConfig,
WalletNotification,
enableNativeLogging,
getErrorDetailFromException,
- j2s,
openPromise,
performanceNow,
setGlobalLogLevelFromString,
} from "@gnu-taler/taler-util";
import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
import { qjsOs } from "@gnu-taler/taler-util/qtart";
+import { Wallet, createNativeWalletHost2 } from "@gnu-taler/taler-wallet-core";
import {
- DefaultNodeWalletArgs,
- Wallet,
- WalletApiOperation,
- createNativeWalletHost2,
-} from "@gnu-taler/taler-wallet-core";
+ testArgon2id,
+ testWithFdold,
+ testWithGv,
+ testWithLocal,
+} from './wallet-qjs-tests.js';
setGlobalLogLevelFromString("trace");
@@ -68,9 +66,6 @@ function sendNativeMessage(ev: CoreApiMessageEnvelope): void {
}
class NativeWalletMessageHandler {
- walletArgs: DefaultNodeWalletArgs | undefined;
- walletConfig: PartialWalletRunConfig | undefined;
- maybeWallet: Wallet | undefined;
wp = openPromise<Wallet>();
httpLib = createPlatformHttpLib();
@@ -91,23 +86,9 @@ class NativeWalletMessageHandler {
};
};
- let initResponse: any = {};
-
- const reinit = async () => {
- logger.info("in reinit");
- const wR = await createNativeWalletHost2(this.walletArgs);
- const w = wR.wallet;
- this.maybeWallet = w;
- const resp = await w.handleCoreApiRequest("initWallet", "native-init", {
- config: this.walletConfig,
- });
- initResponse = resp.type == "response" ? resp.result : resp.error;
- this.wp.resolve(w);
- };
-
switch (operation) {
case "init": {
- this.walletArgs = {
+ const wR = await createNativeWalletHost2({
notifyHandler: async (notification: WalletNotification) => {
sendNativeMessage({ type: "notification", payload: notification });
},
@@ -115,38 +96,29 @@ class NativeWalletMessageHandler {
httpLib: this.httpLib,
cryptoWorkerType: args.cryptoWorkerType,
...args,
- };
- this.walletConfig = args.config ?? {};
- const logLevel = args.logLevel;
- if (logLevel) {
- setGlobalLogLevelFromString(logLevel);
+ });
+
+ if (args.logLevel) {
+ setGlobalLogLevelFromString(args.logLevel);
}
- const nativeLogging = args.useNativeLogging ?? false;
- if (nativeLogging) {
+
+ if (args.useNativeLogging === true) {
enableNativeLogging();
}
- await reinit();
+
+ const resp = await wR.wallet.handleCoreApiRequest("initWallet", "native-init", {
+ config: args.config ?? {},
+ });
+
+ let initResponse: any = resp.type == "response" ? resp.result : resp.error;
+
+ this.wp.resolve(wR.wallet);
+
return wrapSuccessResponse({
...initResponse,
});
}
- case "startTunnel": {
- // this.httpLib.useNfcTunnel = true;
- throw Error("not implemented");
- }
- case "stopTunnel": {
- // this.httpLib.useNfcTunnel = false;
- throw Error("not implemented");
- }
- case "tunnelResponse": {
- // httpLib.handleTunnelResponse(msg.args);
- throw Error("not implemented");
- }
- case "reset": {
- throw Error(
- "reset not supported anymore, please use the clearDb wallet-core request",
- );
- }
+
default: {
const wallet = await this.wp.promise;
return await wallet.handleCoreApiRequest(operation, id, args);
@@ -175,17 +147,22 @@ async function handleAnastasisRequest(
let req = args ?? {};
switch (operation) {
- case "anastasisReduce":
- // TODO: do some input validation here
+ case "anastasisReduce": {
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":
+ }
+
+ case "anastasisStartBackup": {
return wrapSuccessResponse(await getBackupStartState());
- case "anastasisStartRecovery":
+ }
+
+ case "anastasisStartRecovery": {
return wrapSuccessResponse(await getRecoveryStartState());
- case "anastasisDiscoverPolicies":
+ }
+
+ case "anastasisDiscoverPolicies": {
let discoverRes = await discoverPolicies(req.state, req.cursor);
let aggregatedPolicies = mergeDiscoveryAggregate(
discoverRes.policies ?? [],
@@ -199,19 +176,25 @@ async function handleAnastasisRequest(
cursor: discoverRes.cursor,
},
});
- default:
+ }
+
+ default: {
throw Error("unsupported anastasis operation");
+ }
}
}
export function installNativeWalletListener(): void {
setGlobalLogLevelFromString("trace");
+
const handler = new NativeWalletMessageHandler();
+
const onMessage = async (msgStr: any): Promise<void> => {
if (typeof msgStr !== "string") {
logger.error("expected string as message");
return;
}
+
const msg = JSON.parse(msgStr);
const operation = msg.operation;
if (typeof operation !== "string") {
@@ -220,20 +203,23 @@ export function installNativeWalletListener(): void {
);
return;
}
+
const id = msg.id;
logger.info(`native listener: got request for ${operation} (${id})`);
- const startTimeNs = performanceNow();
-
+ const startTimeMs = performanceNow();
let respMsg: CoreApiResponse;
+
try {
if (msg.operation.startsWith("anastasis")) {
+ // Entry point for Anastasis
respMsg = await handleAnastasisRequest(operation, id, msg.args ?? {});
} else if (msg.operation === "testing-dangerously-eval") {
// Eval code, used only for testing. No client may rely on this.
logger.info(`evaluating ${msg.args.jscode}`);
const f = new Function(msg.args.jscode);
f();
+
respMsg = {
type: "response",
result: {},
@@ -241,6 +227,7 @@ export function installNativeWalletListener(): void {
id: msg.id,
};
} else {
+ // Entry point for wallet-core
respMsg = await handler.handleMessage(operation, id, msg.args ?? {});
}
} catch (e) {
@@ -251,10 +238,12 @@ export function installNativeWalletListener(): void {
error: getErrorDetailFromException(e),
};
}
- const endTimeNs = performanceNow();
+
+ const endTimeMs = performanceNow();
const requestDurationMs = Math.round(
- Number((endTimeNs - startTimeNs) / 1000n / 1000n),
+ Number((endTimeMs - startTimeMs) / 1000n / 1000n),
);
+
logger.info(
`native listener: sending back ${respMsg.type} message for operation ${operation} (${id}) after ${requestDurationMs} ms`,
);
@@ -268,102 +257,6 @@ export function installNativeWalletListener(): void {
// @ts-ignore
globalThis.installNativeWalletListener = installNativeWalletListener;
-
-export async function testWithGv() {
- const w = await createNativeWalletHost2({});
- await w.wallet.client.call(WalletApiOperation.InitWallet, {
- config: {
- features: {
- allowHttp: true,
- },
- },
- });
- await w.wallet.client.call(WalletApiOperation.RunIntegrationTest, {
- amountToSpend: "KUDOS:1" as AmountString,
- amountToWithdraw: "KUDOS:3" as AmountString,
- corebankApiBaseUrl: "https://bank.demo.taler.net/",
- exchangeBaseUrl: "https://exchange.demo.taler.net/",
- merchantBaseUrl: "https://backend.demo.taler.net/",
- merchantAuthToken: "secret-token:sandbox",
- });
- await w.wallet.client.call(WalletApiOperation.TestingWaitTasksDone, {});
- await w.wallet.client.call(WalletApiOperation.Shutdown, {});
-}
-
-export async function testWithFdold() {
- const w = await createNativeWalletHost2({});
- await w.wallet.client.call(WalletApiOperation.InitWallet, {
- config: {
- features: {
- allowHttp: true,
- },
- },
- });
- await w.wallet.client.call(WalletApiOperation.RunIntegrationTest, {
- amountToSpend: "TESTKUDOS:1" as AmountString,
- amountToWithdraw: "TESTKUDOS:3" as AmountString,
- corebankApiBaseUrl: "https://bank.taler.fdold.eu/",
- exchangeBaseUrl: "https://exchange.taler.fdold.eu/",
- merchantBaseUrl: "https://merchant.taler.fdold.eu/",
- });
- await w.wallet.client.call(WalletApiOperation.TestingWaitTasksDone, {});
- await w.wallet.client.call(WalletApiOperation.Shutdown, {});
-}
-
-export async function testWithLocal(path: string) {
- console.log("running local test");
- const w = await createNativeWalletHost2({
- persistentStoragePath: path ?? "walletdb.json",
- });
- console.log("created wallet");
- await w.wallet.client.call(WalletApiOperation.InitWallet, {
- config: {
- features: {
- allowHttp: true,
- },
- testing: {
- skipDefaults: true,
- },
- },
- });
- console.log("initialized wallet");
- await w.wallet.client.call(WalletApiOperation.RunIntegrationTest, {
- amountToSpend: "TESTKUDOS:1" as AmountString,
- amountToWithdraw: "TESTKUDOS:3" as AmountString,
- corebankApiBaseUrl: "http://localhost:8082/taler-bank-access/",
- exchangeBaseUrl: "http://localhost:8081/",
- merchantBaseUrl: "http://localhost:8083/",
- });
- console.log("started integration test");
- await w.wallet.client.call(WalletApiOperation.TestingWaitTasksDone, {});
- console.log("done with task loop");
- await w.wallet.client.call(WalletApiOperation.Shutdown, {});
- console.log("DB stats:", j2s(w.getDbStats()));
-}
-
-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
diff --git a/packages/taler-wallet-webextension/manifest-common.json b/packages/taler-wallet-webextension/manifest-common.json
index 32bd5267f..88f152d50 100644
--- a/packages/taler-wallet-webextension/manifest-common.json
+++ b/packages/taler-wallet-webextension/manifest-common.json
@@ -2,7 +2,7 @@
"name": "GNU Taler Wallet (git)",
"description": "Privacy preserving and transparent payments",
"author": "GNU Taler Developers",
- "version": "0.10.7",
+ "version": "0.11.4",
"icons": {
"16": "static/img/taler-logo-16.png",
"19": "static/img/taler-logo-19.png",
@@ -14,5 +14,5 @@
"256": "static/img/taler-logo-256.png",
"512": "static/img/taler-logo-512.png"
},
- "version_name": "0.10.7"
+ "version_name": "0.11.4"
}
diff --git a/packages/taler-wallet-webextension/package.json b/packages/taler-wallet-webextension/package.json
index bf063d76e..90679cfdd 100644
--- a/packages/taler-wallet-webextension/package.json
+++ b/packages/taler-wallet-webextension/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-wallet-webextension",
- "version": "0.10.7",
+ "version": "0.11.4",
"description": "GNU Taler Wallet browser extension",
"main": "./build/index.js",
"types": "./build/index.d.ts",
diff --git a/packages/taler-wallet-webextension/src/components/HistoryItem.tsx b/packages/taler-wallet-webextension/src/components/HistoryItem.tsx
index 9be9326b2..8e48a2e9f 100644
--- a/packages/taler-wallet-webextension/src/components/HistoryItem.tsx
+++ b/packages/taler-wallet-webextension/src/components/HistoryItem.tsx
@@ -26,7 +26,7 @@ import {
DenomLossEventType,
parsePaytoUri,
} from "@gnu-taler/taler-util";
-import { h, VNode } from "preact";
+import { Fragment, h, VNode } from "preact";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Avatar } from "../mui/Avatar.js";
import { Pages } from "../NavigationBar.js";
@@ -49,6 +49,8 @@ export function HistoryItem(props: { tx: Transaction }): VNode {
*/
switch (tx.type) {
case TransactionType.Withdrawal:
+ //withdrawal that has not been confirmed are hidden
+ if (!tx.exchangeBaseUrl) return <Fragment />
return (
<Layout
id={tx.transactionId}
diff --git a/packages/taler-wallet-webextension/src/components/WalletActivity.tsx b/packages/taler-wallet-webextension/src/components/WalletActivity.tsx
index a77a69fa6..f29d0b0f7 100644
--- a/packages/taler-wallet-webextension/src/components/WalletActivity.tsx
+++ b/packages/taler-wallet-webextension/src/components/WalletActivity.tsx
@@ -22,7 +22,7 @@ import {
TalerErrorDetail,
TaskProgressNotification,
WalletNotification,
- assertUnreachable
+ assertUnreachable,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
@@ -83,7 +83,9 @@ export function WalletActivity(): VNode {
cursor: "pointer",
}}
>
- click here to open
+ <i18n.Translate>
+ Click here to open the wallet activity tab.
+ </i18n.Translate>
</div>
</div>
);
diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx b/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx
index 547d5ac9a..0d8035136 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx
@@ -24,12 +24,21 @@ import {
InvoicePaymentDetails,
} from "../../wallet/Transaction.js";
import { State } from "./index.js";
+import { AbsoluteTime, Duration } from "@gnu-taler/taler-util";
export function ReadyView(
state: State.Ready | State.NoBalanceForCurrency | State.NoEnoughBalance,
): VNode {
const { i18n } = useTranslationContext();
const { summary, effective, raw, expiration, uri, status, payStatus } = state;
+
+ const inFiveMinutes = AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ minutes: 5 }),
+ );
+ const willExpireSoon =
+ expiration && AbsoluteTime.cmp(expiration, inFiveMinutes) === -1;
+
return (
<Fragment>
<section style={{ textAlign: "left" }}>
@@ -42,11 +51,13 @@ export function ReadyView(
/>
}
/>
- <Part
- title={i18n.str`Valid until`}
- text={<Time timestamp={expiration} format="dd MMMM yyyy, HH:mm" />}
- kind="neutral"
- />
+ {willExpireSoon && (
+ <Part
+ title={i18n.str`Expires at`}
+ text={<Time timestamp={expiration} format="HH:mm" />}
+ kind="neutral"
+ />
+ )}
</section>
<PaymentButtons
amount={effective}
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
index 68d161ab2..b1eee85ec 100644
--- a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
@@ -18,6 +18,7 @@ import {
AbsoluteTime,
Amounts,
MerchantContractTerms as ContractTerms,
+ Duration,
PreparePayResultType,
TranslatedString,
} from "@gnu-taler/taler-util";
@@ -54,6 +55,17 @@ export function BaseView(state: SupportedStates): VNode {
: Amounts.zeroOfCurrency(state.amount.currency)
: state.amount;
+ const expiration = !contractTerms.pay_deadline
+ ? undefined
+ : AbsoluteTime.fromProtocolTimestamp(contractTerms.pay_deadline);
+ const inFiveMinutes = AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ minutes: 5 }),
+ );
+ const willExpireSoon =
+ !expiration || expiration.t_ms === "never"
+ ? undefined
+ : AbsoluteTime.cmp(expiration, inFiveMinutes) === -1;
return (
<Fragment>
<ShowImportantMessage state={state} />
@@ -65,7 +77,12 @@ export function BaseView(state: SupportedStates): VNode {
<Fragment>
<i18n.Translate>Purchase</i18n.Translate>
&nbsp;
- <AgeSign size={20} title={i18n.str`This purchase is age restricted.`}>{contractTerms.minimum_age}+</AgeSign>
+ <AgeSign
+ size={20}
+ title={i18n.str`This purchase is age restricted.`}
+ >
+ {contractTerms.minimum_age}+
+ </AgeSign>
</Fragment>
) : (
<i18n.Translate>Purchase</i18n.Translate>
@@ -79,17 +96,10 @@ export function BaseView(state: SupportedStates): VNode {
text={<MerchantDetails merchant={contractTerms.merchant} />}
kind="neutral"
/>
- {contractTerms.pay_deadline && (
+ {willExpireSoon && (
<Part
- title={i18n.str`Valid until`}
- text={
- <Time
- timestamp={AbsoluteTime.fromProtocolTimestamp(
- contractTerms.pay_deadline,
- )}
- format="dd MMMM yyyy, HH:mm"
- />
- }
+ title={i18n.str`Expires at`}
+ text={<Time timestamp={expiration} format="HH:mm" />}
kind="neutral"
/>
)}
diff --git a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts
index 1a92c4073..ba854a93c 100644
--- a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts
@@ -47,12 +47,18 @@ export function useComponentState({
const hook = useAsyncAsHook(async () => {
if (!talerTemplateUri) throw Error("ERROR_NO-URI-FOR-PAYMENT-TEMPLATE");
const templateP = await api.wallet.call(
- WalletApiOperation.CheckPayForTemplate, { talerPayTemplateUri: talerTemplateUri },
+ WalletApiOperation.CheckPayForTemplate,
+ { talerPayTemplateUri: talerTemplateUri },
);
- const requireMoreInfo = !templateP.templateDetails.template_contract.amount || !templateP.templateDetails.template_contract.summary;
+ const requireMoreInfo =
+ !templateP.templateDetails.template_contract.amount ||
+ !templateP.templateDetails.template_contract.summary;
let payStatus: PreparePayResult | undefined = undefined;
if (!requireMoreInfo) {
- payStatus = await api.wallet.call(WalletApiOperation.PreparePayForTemplate, { talerPayTemplateUri: talerTemplateUri });
+ payStatus = await api.wallet.call(
+ WalletApiOperation.PreparePayForTemplate,
+ { talerPayTemplateUri: talerTemplateUri },
+ );
}
const balance = await api.wallet.call(WalletApiOperation.GetBalances, {});
return { payStatus, balance, uri: talerTemplateUri, templateP };
@@ -102,20 +108,28 @@ export function useComponentState({
const cfg = hook.response.templateP.templateDetails.template_contract;
const def = hook.response.templateP.templateDetails.editable_defaults;
- const fixedAmount = cfg.amount !== undefined ? Amounts.parseOrThrow(cfg.amount) : undefined;
- const fixedSummary = cfg.summary !== undefined ? cfg.summary : undefined;
-
- const defaultAmount = def?.amount !== undefined ? Amounts.parseOrThrow(def.amount) : undefined;
- const defaultSummary = def?.summary !== undefined ? def.summary : undefined;
-
- const zero = fixedAmount ? Amounts.zeroOfAmount(fixedAmount) :
- cfg.currency !== undefined ? Amounts.zeroOfCurrency(cfg.currency) :
- defaultAmount !== undefined ? Amounts.zeroOfAmount(defaultAmount) :
- def?.currency !== undefined ? Amounts.zeroOfCurrency(def.currency) :
- Amounts.zeroOfCurrency(hook.response.templateP.supportedCurrencies[0]);
-
- const [amount, setAmount] = useState(defaultAmount ?? zero);
- const [summary, setSummary] = useState(defaultSummary ?? "");
+ const fixedAmount =
+ cfg.amount !== undefined ? Amounts.parseOrThrow(cfg.amount) : undefined;
+ const fixedSummary = cfg.summary;
+
+ const defaultAmount =
+ def?.amount !== undefined ? Amounts.parseOrThrow(def.amount) : undefined;
+ const defaultSummary = def?.summary;
+
+ const zero = fixedAmount
+ ? Amounts.zeroOfAmount(fixedAmount)
+ : cfg.currency !== undefined
+ ? Amounts.zeroOfCurrency(cfg.currency)
+ : defaultAmount !== undefined
+ ? Amounts.zeroOfAmount(defaultAmount)
+ : def?.currency !== undefined
+ ? Amounts.zeroOfCurrency(def.currency)
+ : Amounts.zeroOfCurrency(
+ hook.response.templateP.supportedCurrencies[0],
+ );
+
+ const [amount, setAmount] = useState(defaultAmount ?? fixedAmount ?? zero);
+ const [summary, setSummary] = useState(defaultSummary ?? fixedSummary ?? "");
async function createOrder() {
try {
@@ -140,41 +154,50 @@ export function useComponentState({
}
const errors = undefinedIfEmpty({
- amount: fixedAmount !== undefined ? undefined : amount && Amounts.isZero(amount) ? i18n.str`required` : undefined,
- summary: fixedSummary !== undefined ? undefined : summary !== undefined && !summary ? i18n.str`required` : undefined,
+ amount:
+ fixedAmount !== undefined
+ ? undefined
+ : amount && Amounts.isZero(amount)
+ ? i18n.str`required`
+ : undefined,
+ summary:
+ fixedSummary !== undefined
+ ? undefined
+ : summary !== undefined && !summary
+ ? i18n.str`required`
+ : undefined,
});
return {
status: "fill-template",
error: undefined,
minAge: cfg.minimum_age ?? 0,
- amount:
- fixedAmount === undefined
- ? ({
- onInput: (a) => {
- setAmount(a);
- },
- value: amount,
- error: errors?.amount,
- } as AmountFieldHandler)
- : undefined,
- summary:
- fixedSummary === undefined
- ? ({
- onInput: (t) => {
- setSummary(t);
- },
- value: summary,
- error: errors?.summary,
- } as TextFieldHandler)
- : undefined,
+ amount: {
+ onInput:
+ fixedAmount !== undefined
+ ? undefined
+ : (a) => {
+ setAmount(a);
+ },
+ value: amount,
+ error: errors?.amount,
+ } as AmountFieldHandler,
+ summary: {
+ onInput:
+ fixedSummary !== undefined
+ ? undefined
+ : (t) => {
+ setSummary(t);
+ },
+ value: summary,
+ error: errors?.summary,
+ } as TextFieldHandler,
onCreate: {
onClick: errors
? undefined
: safely("create order for pay template", createOrder),
},
};
- }
-
+ };
}
function undefinedIfEmpty<T extends object>(obj: T): T | undefined {
diff --git a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx
index ce53c3cf9..4a1cfe3ac 100644
--- a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx
@@ -33,24 +33,11 @@ export function ReadyView({
return (
<Fragment>
<section style={{ textAlign: "left" }}>
- {/* <Part
- title={
- <div
- style={{
- display: "flex",
- alignItems: "center",
- }}
- >
- <i18n.Translate>Merchant</i18n.Translate>
- </div>
- }
- text={<ExchangeDetails exchange={exchangeUrl} />}
- kind="neutral"
- big
- /> */}
{!amount ? undefined : (
<p>
- <AmountField label={i18n.str`Amount`} handler={amount} />
+ <AmountField label={i18n.str`Amount`}
+ handler={amount}
+ />
</p>
)}
{!summary ? undefined : (
@@ -60,6 +47,7 @@ export function ReadyView({
variant="filled"
required
fullWidth
+ disabled={summary.onInput === undefined}
error={summary.error}
value={summary.value}
onChange={summary.onInput}
@@ -67,12 +55,12 @@ export function ReadyView({
</p>
)}
</section>
- {minAge && (
+ {minAge ? (
<section>
<AgeSign size={25}>{minAge}+</AgeSign>
<i18n.Translate>This purchase is age restricted.</i18n.Translate>
</section>
- )}
+ ) : undefined}
<section>
<Button onClick={onCreate.onClick} variant="contained" color="success">
<i18n.Translate>Review order</i18n.Translate>
diff --git a/packages/taler-wallet-webextension/src/cta/TransferPickup/views.tsx b/packages/taler-wallet-webextension/src/cta/TransferPickup/views.tsx
index caa1b485a..e82c4fbd2 100644
--- a/packages/taler-wallet-webextension/src/cta/TransferPickup/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/TransferPickup/views.tsx
@@ -26,6 +26,7 @@ import {
} from "../../wallet/Transaction.js";
import { State } from "./index.js";
import { TermsOfService } from "../../components/TermsOfService/index.js";
+import { AbsoluteTime, Duration } from "@gnu-taler/taler-util";
export function ReadyView({
accept,
@@ -36,6 +37,12 @@ export function ReadyView({
raw,
}: State.Ready): VNode {
const { i18n } = useTranslationContext();
+ const inFiveMinutes = AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ minutes: 5 }),
+ );
+ const willExpireSoon =
+ expiration && AbsoluteTime.cmp(expiration, inFiveMinutes) === -1;
return (
<Fragment>
<section style={{ textAlign: "left" }}>
@@ -49,15 +56,16 @@ export function ReadyView({
/>
}
/>
-
- <Part
- title={i18n.str`Valid until`}
- text={<Time timestamp={expiration} format="dd MMMM yyyy, HH:mm" />}
- kind="neutral"
- />
+ {willExpireSoon && (
+ <Part
+ title={i18n.str`Expires at`}
+ text={<Time timestamp={expiration} format="HH:mm" />}
+ kind="neutral"
+ />
+ )}
</section>
<section>
- <TermsOfService key="terms" exchangeUrl={exchangeBaseUrl} >
+ <TermsOfService key="terms" exchangeUrl={exchangeBaseUrl}>
<Button variant="contained" color="success" onClick={accept.onClick}>
<i18n.Translate>
Receive &nbsp; {<Amount value={effective} />}
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts
index d33abffee..418fef505 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts
@@ -18,7 +18,7 @@ import {
AmountJson,
AmountString,
CurrencySpecification,
- ExchangeListItem
+ ExchangeListItem,
} from "@gnu-taler/taler-util";
import { Loading } from "../../components/Loading.js";
import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js";
@@ -85,7 +85,7 @@ export namespace State {
operationState: "confirmed" | "aborted" | "selected";
thisWallet: boolean;
redirectToTx: () => void;
- confirmTransferUrl?: string,
+ confirmTransferUrl?: string;
error: undefined;
}
@@ -95,20 +95,26 @@ export namespace State {
currentExchange: ExchangeListItem;
- chosenAmount: AmountJson;
- withdrawalFee: AmountJson;
+ amount: AmountFieldHandler;
+ editableAmount: boolean;
+
+ bankFee: AmountJson;
toBeReceived: AmountJson;
+ toBeSent: AmountJson;
doWithdrawal: ButtonHandler;
doSelectExchange: ButtonHandler;
+ editableExchange: boolean;
chooseCurrencies: string[];
selectedCurrency: string;
changeCurrency: (s: string) => void;
- conversionInfo: {
- spec: CurrencySpecification,
- amount: AmountJson,
- } | undefined;
+ conversionInfo:
+ | {
+ spec: CurrencySpecification;
+ amount: AmountJson;
+ }
+ | undefined;
ageRestriction?: SelectFieldHandler;
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts
index f592072ff..0541bbf3f 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts
@@ -54,7 +54,7 @@ export function useComponentStateFromParams({
? parseWithdrawExchangeUri(maybeTalerUri)
: undefined;
const exchangeByTalerUri = updatedExchangeByUser ?? uri?.exchangeBaseUrl;
-
+
let ex: ExchangeFullDetails | undefined;
if (exchangeByTalerUri) {
await api.wallet.call(WalletApiOperation.AddExchange, {
@@ -185,9 +185,16 @@ export function useComponentStateFromParams({
cancel,
onSuccess,
undefined,
- chosenAmount,
- exchangeList,
- exchangeByTalerUri,
+ {
+ amount: chosenAmount,
+ currency: chosenAmount.currency,
+ maxAmount: Amounts.zeroOfCurrency(chosenAmount.currency),
+ bankFee: Amounts.zeroOfCurrency(chosenAmount.currency),
+ editableAmount: true,
+ editableExchange: true,
+ exchange: exchangeByTalerUri,
+ exchangeList: exchangeList,
+ },
setUpdatedExchangeByUser,
);
}
@@ -212,33 +219,21 @@ export function useComponentStateFromURI({
const uriInfo = await api.wallet.call(
WalletApiOperation.PrepareBankIntegratedWithdrawal,
+ { talerWithdrawUri },
+ );
+ const { status } = uriInfo.info;
+ const txInfo = await api.wallet.call(
+ WalletApiOperation.GetTransactionById,
{
- talerWithdrawUri,
- selectedExchange: updatedExchangeByUser,
+ transactionId: uriInfo.transactionId,
},
);
- const {
- amount,
- defaultExchangeBaseUrl,
- possibleExchanges,
- confirmTransferUrl,
- status,
- } = uriInfo.info;
- const txInfo =
- uriInfo.transactionId === undefined
- ? undefined
- : await api.wallet.call(WalletApiOperation.GetTransactionById, {
- transactionId: uriInfo.transactionId,
- });
return {
talerWithdrawUri,
status,
transactionId: uriInfo.transactionId,
+ bankWithdrawalInfo: uriInfo.info,
txInfo: txInfo,
- confirmTransferUrl,
- amount: Amounts.parseOrThrow(amount),
- thisExchange: defaultExchangeBaseUrl,
- exchanges: possibleExchanges,
};
});
@@ -278,9 +273,22 @@ export function useComponentStateFromURI({
const uri = uriInfoHook.response.talerWithdrawUri;
const txId = uriInfoHook.response.transactionId;
- const chosenAmount = uriInfoHook.response.amount;
- const defaultExchange = uriInfoHook.response.thisExchange;
- const exchangeList = uriInfoHook.response.exchanges;
+ const bwi = uriInfoHook.response.bankWithdrawalInfo;
+
+ const amount =
+ bwi.amount === undefined
+ ? Amounts.zeroOfCurrency(bwi.currency)
+ : Amounts.parseOrThrow(bwi.amount);
+
+ const maxAmount =
+ bwi.maxAmount === undefined
+ ? Amounts.zeroOfCurrency(bwi.currency)
+ : Amounts.parseOrThrow(bwi.maxAmount);
+
+ const bankFee =
+ bwi.wireFee === undefined
+ ? Amounts.zeroOfCurrency(bwi.currency)
+ : Amounts.parseOrThrow(bwi.wireFee);
async function doManagedWithdraw(
exchange: string,
@@ -290,9 +298,6 @@ export function useComponentStateFromURI({
transactionId: string;
confirmTransferUrl: string | undefined;
}> {
- if (!txId) {
- throw Error("can't confirm transaction");
- }
const res = await api.wallet.call(WalletApiOperation.ConfirmWithdrawal, {
exchangeBaseUrl: exchange,
amount,
@@ -305,12 +310,15 @@ export function useComponentStateFromURI({
};
}
- if (uriInfoHook.response.txInfo && uriInfoHook.response.status !== "pending") {
+ if (
+ uriInfoHook.response.txInfo &&
+ uriInfoHook.response.status !== "pending"
+ ) {
const info = uriInfoHook.response.txInfo;
return {
status: "already-completed",
operationState: uriInfoHook.response.status,
- confirmTransferUrl: uriInfoHook.response.confirmTransferUrl,
+ confirmTransferUrl: bwi.confirmTransferUrl,
thisWallet: info.txState.major === TransactionMajorState.Pending,
redirectToTx: () => onSuccess(info.transactionId),
error: undefined,
@@ -323,14 +331,32 @@ export function useComponentStateFromURI({
cancel,
onSuccess,
uri,
- chosenAmount,
- exchangeList,
- defaultExchange,
+ {
+ amount,
+ bankFee,
+ maxAmount,
+ currency: bwi.currency,
+ editableAmount: bwi.editableAmount,
+ editableExchange: bwi.editableExchange,
+ exchange: bwi.defaultExchangeBaseUrl,
+ exchangeList: bwi.possibleExchanges,
+ },
setUpdatedExchangeByUser,
);
}, []);
}
+type WithdrawalInfo = {
+ currency: string;
+ amount: AmountJson;
+ bankFee: AmountJson;
+ maxAmount: AmountJson;
+ editableAmount: boolean;
+ exchange: string | undefined;
+ editableExchange: boolean;
+ exchangeList: ExchangeListItem[];
+};
+
type ManualOrManagedWithdrawFunction = (
exchange: string,
ageRestricted: number | undefined,
@@ -342,16 +368,14 @@ function exchangeSelectionState(
cancel: () => Promise<void>,
onSuccess: (txid: string) => Promise<void>,
talerWithdrawUri: string | undefined,
- chosenAmount: AmountJson,
- exchangeList: ExchangeListItem[],
- exchangeSuggestedByTheBank: string | undefined,
+ wInfo: WithdrawalInfo,
onExchangeUpdated: (ex: string) => void,
): RecursiveState<State> {
const api = useBackendContext();
const selectedExchange = useSelectedExchange({
- currency: chosenAmount.currency,
- defaultExchange: exchangeSuggestedByTheBank,
- list: exchangeList,
+ currency: wInfo.currency,
+ defaultExchange: wInfo.exchange,
+ list: wInfo.exchangeList,
});
const current =
@@ -364,6 +388,10 @@ function exchangeSelectionState(
}
}, [current]);
+ const safeAmount = wInfo.amount
+ ? wInfo.amount
+ : Amounts.zeroOfCurrency(wInfo.currency);
+
if (selectedExchange.status !== "ready") {
return selectedExchange;
}
@@ -374,11 +402,12 @@ function exchangeSelectionState(
| State.Loading => {
const { i18n } = useTranslationContext();
const { pushAlertOnError } = useAlertContext();
+
+ const [choosenAmount, setChoosenAmount] = useState(safeAmount);
const [ageRestricted, setAgeRestricted] = useState(0);
- const currentExchange = selectedExchange.selected;
const [selectedCurrency, setSelectedCurrency] = useState<string>(
- chosenAmount.currency,
+ wInfo.currency,
);
/**
* With the exchange and amount, ask the wallet the information
@@ -388,8 +417,8 @@ function exchangeSelectionState(
const info = await api.wallet.call(
WalletApiOperation.GetWithdrawalDetailsForAmount,
{
- exchangeBaseUrl: currentExchange.exchangeBaseUrl,
- amount: Amounts.stringify(chosenAmount),
+ exchangeBaseUrl: selectedExchange.selected.exchangeBaseUrl,
+ amount: Amounts.stringify(choosenAmount),
restrictAge: ageRestricted,
},
);
@@ -401,20 +430,40 @@ function exchangeSelectionState(
return {
amount: withdrawAmount,
+ currentExchange: selectedExchange.selected,
ageRestrictionOptions: info.ageRestrictionOptions,
accounts: info.withdrawalAccountsList,
};
- }, []);
+ }, [choosenAmount, selectedExchange.selected, ageRestricted]);
const [doingWithdraw, setDoingWithdraw] = useState<boolean>(false);
+ if (!amountHook) {
+ return { status: "loading", error: undefined };
+ }
+ if (amountHook.hasError) {
+ return {
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load the withdrawal details`,
+ amountHook,
+ ),
+ };
+ }
+ if (!amountHook.response) {
+ return { status: "loading", error: undefined };
+ }
+
+ const currentExchange = amountHook.response.currentExchange;
+
async function doWithdrawAndCheckError(): Promise<void> {
try {
setDoingWithdraw(true);
const res = await doWithdraw(
currentExchange.exchangeBaseUrl,
!ageRestricted ? undefined : ageRestricted,
- Amounts.stringify(chosenAmount),
+ Amounts.stringify(choosenAmount),
);
if (res.confirmTransferUrl) {
document.location.href = res.confirmTransferUrl;
@@ -429,32 +478,14 @@ function exchangeSelectionState(
setDoingWithdraw(false);
}
- if (!amountHook) {
- return { status: "loading", error: undefined };
- }
- if (amountHook.hasError) {
- return {
- status: "error",
- error: alertFromError(
- i18n,
- i18n.str`Could not load the withdrawal details`,
- amountHook,
- ),
- };
- }
- if (!amountHook.response) {
- return { status: "loading", error: undefined };
- }
-
- const withdrawalFee = Amounts.sub(
- amountHook.response.amount.raw,
- amountHook.response.amount.effective,
- ).amount;
+ const toBeSent = amountHook.response.amount.raw;
const toBeReceived = amountHook.response.amount.effective;
+ const bankFee = wInfo.bankFee;
+
const ageRestrictionOptions =
amountHook.response.ageRestrictionOptions?.reduce(
- (p, c) => ({ ...p, [c]: `under ${c}` }),
+ (p, c) => ({ ...p, [c]: i18n.str`under ${c}` }),
{} as Record<string, string>,
);
@@ -495,28 +526,50 @@ function exchangeSelectionState(
amount: Amounts.parseOrThrow(convAccount.transferAmount!),
};
+ const amountError = Amounts.isZero(choosenAmount)
+ ? i18n.str`should be greater than zero`
+ : Amounts.cmp(choosenAmount, wInfo.maxAmount) === -1
+ ? i18n.str`choose a lower value`
+ : undefined;
+
return {
status: "success",
error: undefined,
- doSelectExchange: selectedExchange.doSelect,
+ doSelectExchange: {
+ onClick: wInfo.editableExchange
+ ? selectedExchange.doSelect.onClick
+ : undefined,
+ },
+ editableAmount: wInfo.editableAmount,
+ editableExchange: wInfo.editableExchange,
currentExchange,
toBeReceived,
+ toBeSent,
chooseCurrencies,
+ bankFee,
selectedCurrency,
changeCurrency: (s) => {
setSelectedCurrency(s);
},
conversionInfo,
- withdrawalFee,
- chosenAmount,
+ amount: {
+ value: choosenAmount,
+ onInput: wInfo.editableAmount
+ ? pushAlertOnError(async (v) => {
+ setChoosenAmount(v);
+ })
+ : undefined,
+ error: amountError,
+ },
talerWithdrawUri,
ageRestriction,
doWithdrawal: {
- onClick: doingWithdraw
- ? undefined
- : pushAlertOnError(doWithdrawAndCheckError),
+ onClick:
+ doingWithdraw || amountError
+ ? undefined
+ : pushAlertOnError(doWithdrawAndCheckError),
},
cancel,
};
- }, []);
+ }, [selectedExchange.selected]);
}
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx
index 29f39054f..d9b7c380e 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx
@@ -43,17 +43,25 @@ const ageRestrictionSelectField = {
export const TermsOfServiceNotYetLoaded = tests.createExample(SuccessView, {
error: undefined,
status: "success",
- chosenAmount: {
- currency: "USD",
- value: 2,
- fraction: 10000000,
+ amount: {
+ value: {
+ currency: "USD",
+ value: 2,
+ fraction: 10000000,
+ },
},
+ bankFee: {
+ currency: "EUR",
+ fraction: 0,
+ value: 1,
+ },
+
doWithdrawal: { onClick: nullFunction },
currentExchange: {
exchangeBaseUrl: "https://exchange.demo.taler.net",
tos: {},
} as Partial<ExchangeListItem> as any,
- withdrawalFee: {
+ toBeSent: {
currency: "USD",
fraction: 10000000,
value: 1,
@@ -70,34 +78,41 @@ export const TermsOfServiceNotYetLoaded = tests.createExample(SuccessView, {
export const AlreadyAborted = tests.createExample(FinalStateOperation, {
error: undefined,
status: "already-completed",
- operationState: "aborted"
+ operationState: "aborted",
});
export const AlreadySelected = tests.createExample(FinalStateOperation, {
error: undefined,
status: "already-completed",
- operationState: "selected"
+ operationState: "selected",
});
export const AlreadyConfirmed = tests.createExample(FinalStateOperation, {
error: undefined,
status: "already-completed",
- operationState: "confirmed"
+ operationState: "confirmed",
});
-
export const WithSomeFee = tests.createExample(SuccessView, {
error: undefined,
status: "success",
- chosenAmount: {
- currency: "USD",
- value: 2,
- fraction: 10000000,
+ amount: {
+ value: {
+ currency: "USD",
+ value: 2,
+ fraction: 10000000,
+ },
},
+ bankFee: {
+ currency: "EUR",
+ fraction: 0,
+ value: 1,
+ },
+
doWithdrawal: { onClick: nullFunction },
currentExchange: {
exchangeBaseUrl: "https://exchange.demo.taler.net",
tos: {},
} as Partial<ExchangeListItem> as any,
- withdrawalFee: {
+ toBeSent: {
currency: "USD",
fraction: 10000000,
value: 1,
@@ -114,17 +129,25 @@ export const WithSomeFee = tests.createExample(SuccessView, {
export const WithoutFee = tests.createExample(SuccessView, {
error: undefined,
status: "success",
- chosenAmount: {
- currency: "USD",
- value: 2,
+ amount: {
+ value: {
+ currency: "USD",
+ value: 2,
+ fraction: 0,
+ },
+ },
+ bankFee: {
+ currency: "EUR",
fraction: 0,
+ value: 1,
},
+
doWithdrawal: { onClick: nullFunction },
currentExchange: {
exchangeBaseUrl: "https://exchange.demo.taler.net",
tos: {},
} as Partial<ExchangeListItem> as any,
- withdrawalFee: {
+ toBeSent: {
currency: "USD",
fraction: 0,
value: 0,
@@ -141,17 +164,25 @@ export const WithoutFee = tests.createExample(SuccessView, {
export const EditExchangeUntouched = tests.createExample(SuccessView, {
error: undefined,
status: "success",
- chosenAmount: {
- currency: "USD",
- value: 2,
- fraction: 10000000,
+ amount: {
+ value: {
+ currency: "USD",
+ value: 2,
+ fraction: 10000000,
+ },
},
+ bankFee: {
+ currency: "EUR",
+ fraction: 0,
+ value: 1,
+ },
+
doWithdrawal: { onClick: nullFunction },
currentExchange: {
exchangeBaseUrl: "https://exchange.demo.taler.net",
tos: {},
} as Partial<ExchangeListItem> as any,
- withdrawalFee: {
+ toBeSent: {
currency: "USD",
fraction: 0,
value: 0,
@@ -168,17 +199,25 @@ export const EditExchangeUntouched = tests.createExample(SuccessView, {
export const EditExchangeModified = tests.createExample(SuccessView, {
error: undefined,
status: "success",
- chosenAmount: {
- currency: "USD",
- value: 2,
- fraction: 10000000,
+ amount: {
+ value: {
+ currency: "USD",
+ value: 2,
+ fraction: 10000000,
+ },
},
+ bankFee: {
+ currency: "EUR",
+ fraction: 0,
+ value: 1,
+ },
+
doWithdrawal: { onClick: nullFunction },
currentExchange: {
exchangeBaseUrl: "https://exchange.demo.taler.net",
tos: {},
} as Partial<ExchangeListItem> as any,
- withdrawalFee: {
+ toBeSent: {
currency: "USD",
fraction: 0,
value: 0,
@@ -196,18 +235,26 @@ export const WithAgeRestriction = tests.createExample(SuccessView, {
error: undefined,
status: "success",
ageRestriction: ageRestrictionSelectField,
- chosenAmount: {
- currency: "USD",
- value: 2,
- fraction: 10000000,
+ amount: {
+ value: {
+ currency: "USD",
+ value: 2,
+ fraction: 10000000,
+ },
+ },
+ bankFee: {
+ currency: "EUR",
+ fraction: 0,
+ value: 1,
},
+
doSelectExchange: {},
doWithdrawal: { onClick: nullFunction },
currentExchange: {
exchangeBaseUrl: "https://exchange.demo.taler.net",
tos: {},
} as Partial<ExchangeListItem> as any,
- withdrawalFee: {
+ toBeSent: {
currency: "USD",
fraction: 0,
value: 0,
@@ -223,11 +270,19 @@ export const WithAgeRestriction = tests.createExample(SuccessView, {
export const WithAlternateCurrenciesNETZBON = tests.createExample(SuccessView, {
error: undefined,
status: "success",
- chosenAmount: {
- currency: "NETZBON",
- value: 2,
- fraction: 10000000,
+ amount: {
+ value: {
+ currency: "NETZBON",
+ value: 2,
+ fraction: 10000000,
+ },
},
+ bankFee: {
+ currency: "EUR",
+ fraction: 0,
+ value: 1,
+ },
+
chooseCurrencies: ["NETZBON", "EUR"],
selectedCurrency: "NETZBON",
doWithdrawal: { onClick: nullFunction },
@@ -235,7 +290,7 @@ export const WithAlternateCurrenciesNETZBON = tests.createExample(SuccessView, {
exchangeBaseUrl: "https://exchange.netzbon.ch",
tos: {},
} as Partial<ExchangeListItem> as any,
- withdrawalFee: {
+ toBeSent: {
currency: "NETZBON",
fraction: 10000000,
value: 1,
@@ -251,30 +306,38 @@ export const WithAlternateCurrenciesNETZBON = tests.createExample(SuccessView, {
export const WithAlternateCurrenciesEURO = tests.createExample(SuccessView, {
error: undefined,
status: "success",
- chosenAmount: {
- currency: "NETZBON",
- value: 2,
- fraction: 10000000,
+ amount: {
+ value: {
+ currency: "NETZBON",
+ value: 2,
+ fraction: 10000000,
+ },
+ },
+ bankFee: {
+ currency: "EUR",
+ fraction: 0,
+ value: 1,
},
+
chooseCurrencies: ["NETZBON", "EUR"],
selectedCurrency: "EUR",
- changeCurrency: () => { },
+ changeCurrency: () => {},
conversionInfo: {
spec: {
- name: "EUR"
+ name: "EUR",
} as CurrencySpecification,
amount: {
currency: "EUR",
fraction: 10000000,
value: 1,
- }
+ },
},
doWithdrawal: { onClick: nullFunction },
currentExchange: {
exchangeBaseUrl: "https://exchange.netzbon.ch",
tos: {},
} as Partial<ExchangeListItem> as any,
- withdrawalFee: {
+ toBeSent: {
currency: "NETZBON",
fraction: 10000000,
value: 1,
@@ -290,30 +353,37 @@ export const WithAlternateCurrenciesEURO = tests.createExample(SuccessView, {
export const WithAlternateCurrenciesEURO11 = tests.createExample(SuccessView, {
error: undefined,
status: "success",
- chosenAmount: {
- currency: "NETZBON",
- value: 2,
- fraction: 10000000,
+ amount: {
+ value: {
+ currency: "NETZBON",
+ value: 2,
+ fraction: 10000000,
+ },
},
chooseCurrencies: ["NETZBON", "EUR"],
selectedCurrency: "EUR",
- changeCurrency: () => { },
+ changeCurrency: () => {},
+ bankFee: {
+ currency: "EUR",
+ fraction: 0,
+ value: 1,
+ },
conversionInfo: {
spec: {
- name: "EUR"
+ name: "EUR",
} as CurrencySpecification,
amount: {
currency: "EUR",
fraction: 10000000,
value: 2,
- }
+ },
},
doWithdrawal: { onClick: nullFunction },
currentExchange: {
exchangeBaseUrl: "https://exchange.netzbon.ch",
tos: {},
} as Partial<ExchangeListItem> as any,
- withdrawalFee: {
+ toBeSent: {
currency: "NETZBON",
fraction: 10000000,
value: 1,
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts
index 860cf1099..5a75cb4be 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts
@@ -26,6 +26,7 @@ import {
ExchangeListItem,
ExchangeTosStatus,
ScopeType,
+ TransactionIdStr,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { expect } from "chai";
@@ -111,13 +112,18 @@ describe("Withdraw CTA states", () => {
WalletApiOperation.PrepareBankIntegratedWithdrawal,
undefined,
{
- transactionId: "123",
+ transactionId: "123" as TransactionIdStr,
info: {
status: "pending",
operationId: "123",
+ currency: "ARS",
amount: "EUR:2" as AmountString,
possibleExchanges: [],
- }
+ editableAmount: false,
+ editableExchange: false,
+ maxAmount: "ARS:1",
+ wireFee: "ARS:0",
+ },
},
);
@@ -152,14 +158,19 @@ describe("Withdraw CTA states", () => {
WalletApiOperation.PrepareBankIntegratedWithdrawal,
undefined,
{
- transactionId: "123",
+ transactionId: "123" as TransactionIdStr,
info: {
status: "pending",
operationId: "123",
+ currency: "ARS",
amount: "ARS:2" as AmountString,
possibleExchanges: exchanges,
defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl,
- }
+ editableAmount: false,
+ editableExchange: false,
+ maxAmount: "ARS:1",
+ wireFee: "ARS:0",
+ },
},
);
handler.addWalletCallResponse(
@@ -173,7 +184,7 @@ describe("Withdraw CTA states", () => {
scopeInfo: {
currency: "ARS",
type: ScopeType.Exchange,
- url: "http://asd"
+ url: "http://asd",
},
withdrawalAccountsList: [],
ageRestrictionOptions: [],
@@ -197,8 +208,8 @@ describe("Withdraw CTA states", () => {
if (state.status !== "success") return;
expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2"));
- expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0"));
- expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"));
+ expect(state.toBeSent).deep.equal(Amounts.parseOrThrow("ARS:2"));
+ expect(state.amount.value).deep.equal(Amounts.parseOrThrow("ARS:2"));
expect(state.doWithdrawal.onClick).not.undefined;
},
@@ -229,9 +240,14 @@ describe("Withdraw CTA states", () => {
{
status: "pending",
operationId: "123",
+ currency: "ARS",
amount: "ARS:2" as AmountString,
possibleExchanges: exchangeWithNewTos,
defaultExchangeBaseUrl: exchangeWithNewTos[0].exchangeBaseUrl,
+ editableAmount: false,
+ editableExchange: false,
+ maxAmount: "ARS:1",
+ wireFee: "ARS:0",
},
);
handler.addWalletCallResponse(
@@ -244,7 +260,7 @@ describe("Withdraw CTA states", () => {
scopeInfo: {
currency: "ARS",
type: ScopeType.Exchange,
- url: "http://asd"
+ url: "http://asd",
},
tosAccepted: false,
withdrawalAccountsList: [],
@@ -259,9 +275,14 @@ describe("Withdraw CTA states", () => {
{
status: "pending",
operationId: "123",
+ currency: "ARS",
amount: "ARS:2" as AmountString,
possibleExchanges: exchanges,
defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl,
+ editableAmount: false,
+ editableExchange: false,
+ maxAmount: "ARS:1",
+ wireFee: "ARS:0",
},
);
@@ -281,8 +302,8 @@ describe("Withdraw CTA states", () => {
if (state.status !== "success") return;
expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2"));
- expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0"));
- expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"));
+ expect(state.toBeSent).deep.equal(Amounts.parseOrThrow("ARS:2"));
+ expect(state.amount.value).deep.equal(Amounts.parseOrThrow("ARS:2"));
expect(state.doWithdrawal.onClick).not.undefined;
},
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx
index cdddd9bbc..b6a356de8 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx
@@ -19,6 +19,7 @@ import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { Amount } from "../../components/Amount.js";
import { AmountField } from "../../components/AmountField.js";
+import { EnabledBySettings } from "../../components/EnabledBySettings.js";
import { Part } from "../../components/Part.js";
import { QR } from "../../components/QR.js";
import { SelectList } from "../../components/SelectList.js";
@@ -38,7 +39,7 @@ import {
getAmountWithFee,
} from "../../wallet/Transaction.js";
import { State } from "./index.js";
-import { EnabledBySettings } from "../../components/EnabledBySettings.js";
+import { Amounts } from "@gnu-taler/taler-util";
export function FinalStateOperation(state: State.AlreadyCompleted): VNode {
const { i18n } = useTranslationContext();
@@ -143,8 +144,6 @@ export function FinalStateOperation(state: State.AlreadyCompleted): VNode {
export function SuccessView(state: State.Success): VNode {
const { i18n } = useTranslationContext();
- // const currentTosVersionIsAccepted =
- // state.currentExchange.tosStatus === ExchangeTosStatus.Accepted;
return (
<Fragment>
<section style={{ textAlign: "left" }}>
@@ -174,6 +173,11 @@ export function SuccessView(state: State.Success): VNode {
kind="neutral"
big
/>
+ {state.editableAmount ? (
+ <Fragment>
+ <AmountField handler={state.amount} label={i18n.str`Amount`} />
+ </Fragment>
+ ) : undefined}
{state.chooseCurrencies.length > 0 ? (
<Fragment>
<p>
@@ -207,9 +211,10 @@ export function SuccessView(state: State.Success): VNode {
conversion={state.conversionInfo?.amount}
amount={getAmountWithFee(
state.toBeReceived,
- state.chosenAmount,
+ state.toBeSent,
"credit",
)}
+ bankFee={state.bankFee}
/>
}
/>
@@ -227,7 +232,6 @@ export function SuccessView(state: State.Success): VNode {
</section>
<section>
- {/* <div> */}
<TermsOfService exchangeUrl={state.currentExchange.exchangeBaseUrl}>
<Button
variant="contained"
@@ -240,20 +244,6 @@ export function SuccessView(state: State.Success): VNode {
</i18n.Translate>
</Button>
</TermsOfService>
- {/* </div>
- <div style={{ marginTop: 20 }}>
- <Button
- variant="text"
- color="success"
-
- disabled={!state.doAbort.onClick}
- onClick={state.doAbort.onClick}
- >
- <i18n.Translate>
- Cancel
- </i18n.Translate>
- </Button>
- </div> */}
</section>
{state.talerWithdrawUri ? (
<WithdrawWithMobile talerWithdrawUri={state.talerWithdrawUri} />
diff --git a/packages/taler-wallet-webextension/src/hooks/useIsOnline.ts b/packages/taler-wallet-webextension/src/hooks/useIsOnline.ts
index 8d26bf3b6..719aa2f96 100644
--- a/packages/taler-wallet-webextension/src/hooks/useIsOnline.ts
+++ b/packages/taler-wallet-webextension/src/hooks/useIsOnline.ts
@@ -1,7 +1,21 @@
-import { codecForBoolean } from "@gnu-taler/taler-util";
-import { buildStorageKey, useMemoryStorage } from "@gnu-taler/web-util/browser";
-import { platform } from "../platform/foreground.js";
+/*
+ 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 { useMemoryStorage } from "@gnu-taler/web-util/browser";
import { useEffect } from "preact/hooks";
+import { platform } from "../platform/foreground.js";
export function useIsOnline(): boolean {
const { value, update } = useMemoryStorage("online", true);
diff --git a/packages/taler-wallet-webextension/src/platform/chrome.ts b/packages/taler-wallet-webextension/src/platform/chrome.ts
index e63040f5c..056351e3f 100644
--- a/packages/taler-wallet-webextension/src/platform/chrome.ts
+++ b/packages/taler-wallet-webextension/src/platform/chrome.ts
@@ -732,15 +732,35 @@ function listenNetworkConnectionState(
function notifyOnline() {
notify("on");
}
- notify(window.navigator.onLine ? "on" : "off");
- window.addEventListener("offline", notifyOffline);
- window.addEventListener("online", notifyOnline);
+ function notifyChange() {
+ if (nav.onLine) {
+ notifyOnline();
+ } else {
+ notifyOnline();
+ }
+ }
+ notify(navigator.onLine ? "on" : "off");
+
+ const nav: any = navigator;
+ if (typeof nav.connection !== "undefined") {
+ nav.connection.addEventListener("change", notifyChange);
+ }
+ if (typeof window !== "undefined") {
+ window.addEventListener("offline", notifyOffline);
+ window.addEventListener("online", notifyOnline);
+ }
return () => {
- window.removeEventListener("offline", notifyOffline);
- window.removeEventListener("online", notifyOnline);
+ if (typeof nav.connection !== "undefined") {
+ nav.connection.removeEventListener("change", notifyChange);
+ }
+ if (typeof window !== "undefined") {
+ window.removeEventListener("offline", notifyOffline);
+ window.removeEventListener("online", notifyOnline);
+ }
};
}
+
function runningOnPrivateMode(): boolean {
return chrome.extension.inIncognitoContext;
}
diff --git a/packages/taler-wallet-webextension/src/platform/dev.ts b/packages/taler-wallet-webextension/src/platform/dev.ts
index d6e743147..b53e8f3c4 100644
--- a/packages/taler-wallet-webextension/src/platform/dev.ts
+++ b/packages/taler-wallet-webextension/src/platform/dev.ts
@@ -35,11 +35,11 @@ const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
keepAlive: (cb: VoidFunction) => cb(),
findTalerUriInActiveTab: async () => undefined,
findTalerUriInClipboard: async () => undefined,
- listenNetworkConnectionState,
+ listenNetworkConnectionState: () => () => undefined,
openNewURLFromPopup: () => undefined,
triggerWalletEvent: () => undefined,
setAlertedIcon: () => undefined,
- setNormalIcon : () => undefined,
+ setNormalIcon: () => undefined,
getPermissionsApi: () => ({
containsClipboardPermissions: async () => true,
removeClipboardPermissions: async () => false,
@@ -200,19 +200,3 @@ interface IframeMessageCommand {
export default api;
-function listenNetworkConnectionState(
- notify: (state: "on" | "off") => void,
-): () => void {
- function notifyOffline() {
- notify("off");
- }
- function notifyOnline() {
- notify("on");
- }
- window.addEventListener("offline", notifyOffline);
- window.addEventListener("online", notifyOnline);
- return () => {
- window.removeEventListener("offline", notifyOffline);
- window.removeEventListener("online", notifyOnline);
- };
-}
diff --git a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
index 93770312e..73bd8e96d 100644
--- a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
+++ b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
@@ -180,7 +180,7 @@ export function BalanceView(state: State.Balances): VNode {
variant="contained"
onClick={state.goToWalletManualWithdraw.onClick}
>
- <i18n.Translate>Add</i18n.Translate>
+ <i18n.Translate>Receive</i18n.Translate>
</Button>
{currencyWithNonZeroAmount.length > 0 && (
<MultiActionButton
diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts
index 838739ad1..daba6aba4 100644
--- a/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts
+++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts
@@ -94,9 +94,9 @@ export namespace State {
currentAccount: PaytoUri;
totalFee: AmountJson;
- totalToDeposit: AmountJson;
amount: AmountFieldHandler;
+ totalToDeposit: AmountFieldHandler;
account: SelectFieldHandler;
cancelHandler: ButtonHandler;
depositHandler: ButtonHandler;
diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts
index 97b2ab517..b674665cf 100644
--- a/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts
+++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts
@@ -15,19 +15,18 @@
*/
import {
- AmountJson,
Amounts,
- DepositGroupFees,
KnownBankAccountsInfo,
parsePaytoUri,
PaytoUri,
stringifyPaytoUri,
+ TransactionAmountMode
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { useState } from "preact/hooks";
import { alertFromError, useAlertContext } from "../../context/alert.js";
import { useBackendContext } from "../../context/backend.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { RecursiveState } from "../../utils/index.js";
import { Props, State } from "./index.js";
@@ -83,8 +82,11 @@ export function useComponentState({
if (hook.hasError) {
return {
status: "error",
- error: alertFromError(i18n,
- i18n.str`Could not load balance information`, hook),
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load balance information`,
+ hook,
+ ),
};
}
const { accounts, balances } = hook.response;
@@ -141,21 +143,23 @@ export function useComponentState({
}
const firstAccount = accounts[0].uri;
const currentAccount = !selectedAccount ? firstAccount : selectedAccount;
-
- return () => {
- // eslint-disable-next-line react-hooks/rules-of-hooks
- const [amount, setAmount] = useState<AmountJson>(
- initialValue ?? ({} as any),
+ const zero = Amounts.zeroOfCurrency(currency)
+ return (): State => {
+ const [instructed, setInstructed] = useState(
+ {amount: initialValue ?? zero, type: TransactionAmountMode.Raw},
);
- const amountStr = Amounts.stringify(amount);
+ const amountStr = Amounts.stringify(instructed.amount);
const depositPaytoUri = stringifyPaytoUri(currentAccount);
- // eslint-disable-next-line react-hooks/rules-of-hooks
const hook = useAsyncAsHook(async () => {
- const fee = await api.wallet.call(WalletApiOperation.PrepareDeposit, {
- amount: amountStr,
- depositPaytoUri,
- });
+ const fee = await api.wallet.call(
+ WalletApiOperation.ConvertDepositAmount,
+ {
+ amount: amountStr,
+ type: instructed.type,
+ depositPaytoUri,
+ },
+ );
return { fee };
}, [amountStr, depositPaytoUri]);
@@ -183,18 +187,16 @@ export function useComponentState({
const totalFee =
fee !== undefined
- ? Amounts.sum([fee.fees.wire, fee.fees.coin, fee.fees.refresh]).amount
+ ? Amounts.sub(fee.effectiveAmount, fee.rawAmount).amount
: Amounts.zeroOfCurrency(currency);
- const totalToDeposit =
- fee !== undefined
- ? Amounts.sub(amount, totalFee).amount
- : Amounts.zeroOfCurrency(currency);
+ const totalToDeposit = Amounts.parseOrThrow(fee.rawAmount);
+ const totalEffective = Amounts.parseOrThrow(fee.effectiveAmount);
- const isDirty = amount !== initialValue;
+ const isDirty = instructed.amount !== initialValue;
const amountError = !isDirty
? undefined
- : Amounts.cmp(balance, amount) === -1
+ : Amounts.cmp(balance, totalEffective) === -1
? `Too much, your current balance is ${Amounts.stringifyValue(balance)}`
: undefined;
@@ -207,7 +209,7 @@ export function useComponentState({
if (!currency) return;
const depositPaytoUri = stringifyPaytoUri(currentAccount);
- const amountStr = Amounts.stringify(amount);
+ const amountStr = Amounts.stringify(totalEffective);
await api.wallet.call(WalletApiOperation.CreateDepositGroup, {
amount: amountStr,
depositPaytoUri,
@@ -220,8 +222,19 @@ export function useComponentState({
error: undefined,
currency,
amount: {
- value: amount,
- onInput: pushAlertOnError(async (a) => setAmount(a)),
+ value: totalEffective,
+ onInput: pushAlertOnError(async (a) => setInstructed({
+ amount: a,
+ type: TransactionAmountMode.Effective,
+ })),
+ error: amountError,
+ },
+ totalToDeposit: {
+ value: totalToDeposit,
+ onInput: pushAlertOnError(async (a) => setInstructed({
+ amount: a,
+ type: TransactionAmountMode.Raw,
+ })),
error: amountError,
},
onAddAccount: {
@@ -244,7 +257,6 @@ export function useComponentState({
onClick: unableToDeposit ? undefined : pushAlertOnError(doSend),
},
totalFee,
- totalToDeposit,
};
};
}
@@ -269,7 +281,7 @@ export function createLabelsForBankAccount(
): { [value: string]: string } {
const initialList: Record<string, string> = {};
if (!knownBankAccounts.length) return initialList;
- return knownBankAccounts.reduce((prev, cur, i) => {
+ return knownBankAccounts.reduce((prev, cur) => {
prev[stringifyPaytoUri(cur.uri)] = cur.alias;
return prev;
}, initialList);
diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx b/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx
index c23f83fdd..0ed62220b 100644
--- a/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx
@@ -53,7 +53,10 @@ export const WithNoAccountForIBAN = tests.createExample(ReadyView, {
onClick: nullFunction,
},
totalFee: Amounts.zeroOfCurrency("USD"),
- totalToDeposit: Amounts.parseOrThrow("USD:10"),
+ totalToDeposit: {
+ onInput:nullFunction,
+ value: Amounts.parseOrThrow("USD:10"),
+ },
// onCalculateFee: alwaysReturnFeeToOne,
});
@@ -82,7 +85,10 @@ export const WithIBANAccountTypeSelected = tests.createExample(ReadyView, {
onClick: nullFunction,
},
totalFee: Amounts.zeroOfCurrency("USD"),
- totalToDeposit: Amounts.parseOrThrow("USD:10"),
+ totalToDeposit: {
+ onInput:nullFunction,
+ value: Amounts.parseOrThrow("USD:10"),
+ },
// onCalculateFee: alwaysReturnFeeToOne,
});
@@ -111,6 +117,9 @@ export const NewBitcoinAccountTypeSelected = tests.createExample(ReadyView, {
onClick: nullFunction,
},
totalFee: Amounts.zeroOfCurrency("USD"),
- totalToDeposit: Amounts.parseOrThrow("USD:10"),
+ totalToDeposit: {
+ onInput:nullFunction,
+ value: Amounts.parseOrThrow("USD:10"),
+ },
// onCalculateFee: alwaysReturnFeeToOne,
});
diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts
index 157cb868a..1144095e1 100644
--- a/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts
+++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts
@@ -20,17 +20,16 @@
*/
import {
+ AmountResponse,
Amounts,
AmountString,
- DepositGroupFees,
parsePaytoUri,
- PrepareDepositResponse,
ScopeType,
- stringifyPaytoUri,
+ stringifyPaytoUri
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { expect } from "chai";
import * as tests from "@gnu-taler/web-util/testing";
+import { expect } from "chai";
import { nullFunction } from "../../mui/handlers.js";
import { createWalletApiMock } from "../../test-utils.js";
@@ -38,24 +37,14 @@ import { useComponentState } from "./state.js";
const currency = "EUR";
const amount = `${currency}:0`;
-const withoutFee = (): PrepareDepositResponse => ({
- effectiveDepositAmount: `${currency}:5` as AmountString,
- totalDepositCost: `${currency}:5` as AmountString,
- fees: {
- coin: Amounts.stringify(`${currency}:0`),
- wire: Amounts.stringify(`${currency}:0`),
- refresh: Amounts.stringify(`${currency}:0`),
- },
+const withoutFee = (value: number): AmountResponse => ({
+ effectiveAmount: `${currency}:${value}` as AmountString,
+ rawAmount: `${currency}:${value}` as AmountString,
});
-const withSomeFee = (): PrepareDepositResponse => ({
- effectiveDepositAmount: `${currency}:5` as AmountString,
- totalDepositCost: `${currency}:5` as AmountString,
- fees: {
- coin: Amounts.stringify(`${currency}:1`),
- wire: Amounts.stringify(`${currency}:1`),
- refresh: Amounts.stringify(`${currency}:1`),
- },
+const withSomeFee = (value: number, fee: number): AmountResponse => ({
+ effectiveAmount: `${currency}:${value}` as AmountString,
+ rawAmount: `${currency}:${value - fee}` as AmountString,
});
describe("DepositPage states", () => {
@@ -195,9 +184,9 @@ describe("DepositPage states", () => {
},
);
handler.addWalletCallResponse(
- WalletApiOperation.PrepareDeposit,
+ WalletApiOperation.ConvertDepositAmount,
undefined,
- withoutFee(),
+ withoutFee(0),
);
const hookBehavior = await tests.hookBehaveLikeThis(
@@ -255,15 +244,15 @@ describe("DepositPage states", () => {
},
);
handler.addWalletCallResponse(
- WalletApiOperation.PrepareDeposit,
+ WalletApiOperation.ConvertDepositAmount,
undefined,
- withoutFee(),
+ withoutFee(0),
);
handler.addWalletCallResponse(
- WalletApiOperation.PrepareDeposit,
+ WalletApiOperation.ConvertDepositAmount,
undefined,
- withoutFee(),
+ withoutFee(0),
);
const accountSelected = stringifyPaytoUri(ibanPayto.uri);
@@ -345,19 +334,19 @@ describe("DepositPage states", () => {
},
);
handler.addWalletCallResponse(
- WalletApiOperation.PrepareDeposit,
+ WalletApiOperation.ConvertDepositAmount,
undefined,
- withoutFee(),
+ withoutFee(0),
);
handler.addWalletCallResponse(
- WalletApiOperation.PrepareDeposit,
+ WalletApiOperation.ConvertDepositAmount,
undefined,
- withSomeFee(),
+ withSomeFee(10,3),
);
handler.addWalletCallResponse(
- WalletApiOperation.PrepareDeposit,
+ WalletApiOperation.ConvertDepositAmount,
undefined,
- withSomeFee(),
+ withSomeFee(10,3),
);
const accountSelected = stringifyPaytoUri(ibanPayto.uri);
@@ -404,7 +393,7 @@ describe("DepositPage states", () => {
expect(state.account.value).eq(accountSelected);
expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:10"));
expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`));
- expect(state.totalToDeposit).deep.eq(
+ expect(state.totalToDeposit.value).deep.eq(
Amounts.parseOrThrow(`${currency}:7`),
);
expect(state.depositHandler.onClick).not.undefined;
@@ -416,7 +405,7 @@ describe("DepositPage states", () => {
expect(state.account.value).eq(accountSelected);
expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:10"));
expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`));
- expect(state.totalToDeposit).deep.eq(
+ expect(state.totalToDeposit.value).deep.eq(
Amounts.parseOrThrow(`${currency}:7`),
);
expect(state.depositHandler.onClick).not.undefined;
diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx b/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx
index 908becb04..b3607ebba 100644
--- a/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx
@@ -26,7 +26,7 @@ import { Grid } from "../../mui/Grid.js";
import { State } from "./index.js";
export function AmountOrCurrencyErrorView(
- p: State.AmountOrCurrencyError,
+ _p: State.AmountOrCurrencyError,
): VNode {
const { i18n } = useTranslationContext();
@@ -145,7 +145,7 @@ export function ReadyView(state: State.Ready): VNode {
</p>
<Grid container spacing={2} columns={1}>
<Grid item xs={1}>
- <AmountField label={i18n.str`Amount`} handler={state.amount} />
+ <AmountField label={i18n.str`Brut amount`} handler={state.amount} />
</Grid>
<Grid item xs={1}>
<AmountField
@@ -156,12 +156,7 @@ export function ReadyView(state: State.Ready): VNode {
/>
</Grid>
<Grid item xs={1}>
- <AmountField
- label={i18n.str`Total deposit`}
- handler={{
- value: state.totalToDeposit,
- }}
- />
+ <AmountField label={i18n.str`Net amount`} handler={state.totalToDeposit} />
</Grid>
</Grid>
</section>
@@ -180,7 +175,7 @@ export function ReadyView(state: State.Ready): VNode {
) : (
<Button variant="contained" onClick={state.depositHandler.onClick}>
<i18n.Translate>
- Deposit&nbsp;{Amounts.stringifyValue(state.totalToDeposit)}{" "}
+ Deposit&nbsp;{Amounts.stringifyValue(state.totalToDeposit.value)}{" "}
{state.currency}
</i18n.Translate>
</Button>
diff --git a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
index 7b6ac8895..8f23c0685 100644
--- a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
@@ -17,13 +17,12 @@
import {
AbsoluteTime,
Amounts,
- CoinDumpJson,
CoinStatus,
ExchangeTosStatus,
LogLevel,
NotificationType,
ScopeType,
- stringifyWithdrawExchange,
+ stringifyWithdrawExchange
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
@@ -52,7 +51,6 @@ import { Grid } from "../mui/Grid.js";
import { Paper } from "../mui/Paper.js";
import { TextField } from "../mui/TextField.js";
-type CoinsInfo = CoinDumpJson["coins"];
type CalculatedCoinfInfo = {
// ageKeysCount: number | undefined;
denom_value: number;
@@ -68,15 +66,7 @@ type SplitedCoinInfo = {
usable: CalculatedCoinfInfo[];
};
-export interface Props {
- // FIXME: Pending operations don't exist anymore.
-}
-
-function hashObjectId(o: any): string {
- return JSON.stringify(o);
-}
-
-export function DeveloperPage({}: Props): VNode {
+export function DeveloperPage(): VNode {
const { i18n } = useTranslationContext();
const [downloadedDatabase, setDownloadedDatabase] = useState<
{ time: Date; content: string } | undefined
@@ -361,6 +351,7 @@ export function DeveloperPage({}: Props): VNode {
<a
href={new URL(`/keys`, e.exchangeBaseUrl).href}
target="_blank"
+ rel="noreferrer"
>
{e.exchangeBaseUrl}
</a>
diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx b/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx
index 7b80977f3..b995a44d0 100644
--- a/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx
@@ -130,7 +130,6 @@ export function ReadyView({
))}
</div>
<div style={{ border: "1px solid gray", padding: 8, borderRadius: 5 }}>
- --- {uri.value} ---
<p>
<CustomFieldByAccountType
type={accountType.value as AccountType}
diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
index 1f0293352..ca5bc3756 100644
--- a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
@@ -1416,9 +1416,11 @@ export function TransferPickupDetails({
export function WithdrawDetails({
conversion,
amount,
+ bankFee,
}: {
conversion?: AmountJson;
amount: AmountWithFee;
+ bankFee?: AmountJson;
}): VNode {
const { i18n } = useTranslationContext();
@@ -1481,6 +1483,16 @@ export function WithdrawDetails({
</tr>
</Fragment>
)}
+ {!bankFee ? undefined : (
+ <tr>
+ <td>
+ <i18n.Translate>Bank fee</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={bankFee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ )}
</PurchaseDetailsTable>
);
}
diff --git a/packages/taler-wallet-webextension/src/wxApi.ts b/packages/taler-wallet-webextension/src/wxApi.ts
index 4394a982f..47b466fcd 100644
--- a/packages/taler-wallet-webextension/src/wxApi.ts
+++ b/packages/taler-wallet-webextension/src/wxApi.ts
@@ -55,7 +55,7 @@ import { WalletActivityTrack } from "./wxBackend.js";
const logger = new Logger("wxApi");
-export const WALLET_CORE_SUPPORTED_VERSION = "4:0:0"
+export const WALLET_CORE_SUPPORTED_VERSION = "5:0:0"
export interface ExtendedPermissionsResponse {
newValue: boolean;
diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts
index 5fa255f5d..a0b9f2908 100644
--- a/packages/taler-wallet-webextension/src/wxBackend.ts
+++ b/packages/taler-wallet-webextension/src/wxBackend.ts
@@ -39,7 +39,7 @@ import {
makeErrorDetail,
openPromise,
setGlobalLogLevelFromString,
- setLogLevelFromString
+ setLogLevelFromString,
} from "@gnu-taler/taler-util";
import { HttpRequestLibrary } from "@gnu-taler/taler-util/http";
import {
@@ -92,7 +92,7 @@ async function resetDb(): Promise<void> {
export type WalletActivityTrack = {
id: number;
- events: (WalletNotification & {when: AbsoluteTime})[];
+ events: (WalletNotification & { when: AbsoluteTime })[];
start: AbsoluteTime;
type: NotificationType;
end: AbsoluteTime;
@@ -107,130 +107,138 @@ function getUniqueId(): number {
//FIXME: maybe circular buffer
const activity: WalletActivityTrack[] = [];
-function addNewWalletActivityNotification(list: WalletActivityTrack[], n: WalletNotification) {
- const start = AbsoluteTime.now();
- const ev = {...n, when:start};
- switch (n.type) {
+function convertWalletActivityNotification(
+ knownEvents: WalletActivityTrack[],
+ event: WalletNotification & {
+ when: AbsoluteTime;
+ },
+): WalletActivityTrack | undefined {
+ switch (event.type) {
case NotificationType.BalanceChange: {
- const groupId = `${n.type}:${n.hintTransactionId}`;
- const found = list.find((a)=>a.groupId === groupId)
+ const groupId = `${event.type}:${event.hintTransactionId}`;
+ const found = knownEvents.find((a) => a.groupId === groupId);
if (found) {
- found.end = start;
- found.events.unshift(ev)
- return;
+ found.end = event.when;
+ found.events.unshift(event);
+ return found;
}
- list.push({
+ return {
id: getUniqueId(),
- type: n.type,
- start,
+ type: event.type,
+ start: event.when,
end: AbsoluteTime.never(),
- events: [ev],
+ events: [event],
groupId,
- });
- return;
+ };
}
case NotificationType.BackupOperationError: {
const groupId = "";
- list.push({
+ return {
id: getUniqueId(),
- type: n.type,
- start,
+ type: event.type,
+ start: event.when,
end: AbsoluteTime.never(),
- events: [ev],
+ events: [event],
groupId,
- });
- return;
+ };
}
case NotificationType.TransactionStateTransition: {
- const groupId = `${n.type}:${n.transactionId}`;
- const found = list.find((a)=>a.groupId === groupId)
+ const groupId = `${event.type}:${event.transactionId}`;
+ const found = knownEvents.find((a) => a.groupId === groupId);
if (found) {
- found.end = start;
- found.events.unshift(ev)
- return;
+ found.end = event.when;
+ found.events.unshift(event);
+ return found;
}
- list.push({
+ return {
id: getUniqueId(),
- type: n.type,
- start,
+ type: event.type,
+ start: event.when,
end: AbsoluteTime.never(),
- events: [ev],
+ events: [event],
groupId,
- });
- return;
+ };
}
case NotificationType.WithdrawalOperationTransition: {
- return;
+ return undefined;
}
case NotificationType.ExchangeStateTransition: {
- const groupId = `${n.type}:${n.exchangeBaseUrl}`;
- const found = list.find((a)=>a.groupId === groupId)
+ const groupId = `${event.type}:${event.exchangeBaseUrl}`;
+ const found = knownEvents.find((a) => a.groupId === groupId);
if (found) {
- found.end = start;
- found.events.unshift(ev)
- return;
+ found.end = event.when;
+ found.events.unshift(event);
+ return found;
}
- list.push({
+ return {
id: getUniqueId(),
- type: n.type,
- start,
+ type: event.type,
+ start: event.when,
end: AbsoluteTime.never(),
- events: [ev],
+ events: [event],
groupId,
- });
- return;
+ };
}
case NotificationType.Idle: {
const groupId = "";
- list.push({
+ return({
id: getUniqueId(),
- type: n.type,
- start,
+ type: event.type,
+ start: event.when,
end: AbsoluteTime.never(),
- events: [ev],
+ events: [event],
groupId,
});
- return;
}
case NotificationType.TaskObservabilityEvent: {
- const groupId = `${n.type}:${n.taskId}`;
- const found = list.find((a)=>a.groupId === groupId)
+ const groupId = `${event.type}:${event.taskId}`;
+ const found = knownEvents.find((a) => a.groupId === groupId);
if (found) {
- found.end = start;
- found.events.unshift(ev)
- return;
+ found.end = event.when;
+ found.events.unshift(event);
+ return found;
}
- list.push({
+ return({
id: getUniqueId(),
- type: n.type,
- start,
+ type: event.type,
+ start: event.when,
end: AbsoluteTime.never(),
- events: [ev],
+ events: [event],
groupId,
});
- return;
}
case NotificationType.RequestObservabilityEvent: {
- const groupId = `${n.type}:${n.operation}:${n.requestId}`;
- const found = list.find((a)=>a.groupId === groupId)
+ const groupId = `${event.type}:${event.operation}:${event.requestId}`;
+ const found = knownEvents.find((a) => a.groupId === groupId);
if (found) {
- found.end = start;
- found.events.unshift(ev)
- return;
+ found.end = event.when;
+ found.events.unshift(event);
+ return found;
}
- list.push({
+ return({
id: getUniqueId(),
- type: n.type,
- start,
+ type: event.type,
+ start: event.when,
end: AbsoluteTime.never(),
- events: [ev],
+ events: [event],
groupId,
});
- return;
}
}
}
+function addNewWalletActivityNotification(
+ list: WalletActivityTrack[],
+ n: WalletNotification,
+) {
+ const start = AbsoluteTime.now();
+ const ev = { ...n, when: start };
+ const activity = convertWalletActivityNotification(list, ev);
+ if (activity) {
+ list.unshift(activity); // insert at start
+ }
+}
+
async function getNotifications({
filter,
}: {
diff --git a/packages/web-util/package.json b/packages/web-util/package.json
index 369b872b6..c6bf20160 100644
--- a/packages/web-util/package.json
+++ b/packages/web-util/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/web-util",
- "version": "0.10.7",
+ "version": "0.11.4",
"description": "Generic helper functionality for GNU Taler Web Apps",
"type": "module",
"types": "./lib/index.node.d.ts",
diff --git a/packages/web-util/src/components/CopyButton.tsx b/packages/web-util/src/components/CopyButton.tsx
index dbb38b474..4351da018 100644
--- a/packages/web-util/src/components/CopyButton.tsx
+++ b/packages/web-util/src/components/CopyButton.tsx
@@ -21,7 +21,7 @@ export function CopyButton({ class: clazz, children, getContent }: { children?:
const [copied, setCopied] = useState(false);
function copyText(): void {
if (!navigator.clipboard && !window.isSecureContext) {
- alert('clipboard is not available on insecure context (http)')
+ prompt("Clipboard is not available on insecure context (http).", getContent());
}
if (navigator.clipboard) {
navigator.clipboard.writeText(getContent() || "");
diff --git a/packages/web-util/src/index.build.ts b/packages/web-util/src/index.build.ts
index c0c5fc179..2260ecb9a 100644
--- a/packages/web-util/src/index.build.ts
+++ b/packages/web-util/src/index.build.ts
@@ -305,8 +305,10 @@ export function computeConfig(params: BuildParams): esbuild.BuildOptions {
/**
* Build sources for prod environment
*/
-export function build(config: BuildParams) {
- return esbuild.build(computeConfig(config));
+export async function build(config: BuildParams) {
+ const res = await esbuild.build(computeConfig(config));
+ fs.writeFileSync(`${config.destination}/version.txt`, `${_package.version}`);
+ return res;
}
const LIVE_RELOAD_SCRIPT =