commit 631027a807138ed9125befc7a2a7c0d8219e17c6 parent 349cd9a1f46a2ea8e8d9e502577196c7ed1847eb Author: Christian Blättler <blatc2@bfh.ch> Date: Wed, 22 May 2024 20:24:27 +0200 Merge branch 'master' into feature/tokens Diffstat:
150 files changed, 8122 insertions(+), 4820 deletions(-)
diff --git a/packages/aml-backoffice-ui/build.mjs b/packages/aml-backoffice-ui/build.mjs @@ -21,7 +21,7 @@ await build({ type: "production", source: { js: ["src/index.tsx"], - assets: [{ base: "src", files: ["src/index.html"] }], + assets: [{ base: "src", files: ["src/index.html","src/forms.json"] }], }, destination: "./dist/prod", css: "postcss", diff --git a/packages/aml-backoffice-ui/src/forms.json b/packages/aml-backoffice-ui/src/forms.json @@ -13,222 +13,199 @@ "fields": [ { "type": "choiceStacked", - "properties": { - "name": "customerType", - "id": ".customerType", - "label": "Type of customer", - "help": "Select one and complete the next form", - "required": true, - "choices": [ - { - "label": "Natural person", - "value": "natural" - }, - { - "label": "Legal entity", - "value": "legal" - } - ] - } + + "name": "customerType", + "id": ".customerType", + "label": "Type of customer", + "help": "Select one and complete the next form", + "required": true, + "choices": [ + { + "label": "Natural person", + "value": "natural" + }, + { + "label": "Legal entity", + "value": "legal" + } + ] }, { "type": "group", - "properties": { - "label": "Natural customer form", - "name": "algo", - "id": "algo", - "before": "a) Country risk (nationality)", - "after": "a) Country risk (nationality)", - "fields": [ - { - "type": "text", - "properties": { - "name": "naturalCustomer.fullName", - "id": ".naturalCustomer.fullName", - "label": "Full name", - "required": true - } - }, - { - "type": "text", - "properties": { - "name": "naturalCustomer.address", - "id": ".naturalCustomer.address", - "label": "Residential address", - "required": true - } - }, - { - "type": "integer", - "properties": { - "name": "naturalCustomer.telephone", - "id": ".naturalCustomer.telephone", - "label": "Telephone" - } - }, - { - "type": "text", - "properties": { - "name": "naturalCustomer.email", - "id": ".naturalCustomer.email", - "label": "E-mail" - } - }, - { - "type": "absoluteTime", - "properties": { - "pattern": "dd/MM/yyyy", - "name": "naturalCustomer.dateOfBirth", - "id": ".naturalCustomer.dateOfBirth", - "label": "Date of birth", - "required": true - } - }, - { - "type": "text", - "properties": { - "name": "naturalCustomer.nationality", - "id": ".naturalCustomer.nationality", - "label": "Nationality", - "required": true - } - }, - { - "type": "text", - "properties": { - "name": "naturalCustomer.document", - "id": ".naturalCustomer.document", - "label": "Identification document", - "required": true - } - }, - { - "type": "file", - "properties": { - "name": "naturalCustomer.documentAttachment", - "id": ".naturalCustomer.documentAttachment", - "label": "Document attachment", - "required": true, - "maxBites": 2097152, - "accept": ".pdf", - "help": "PDF file with max size of 2 mega bytes" - } - }, - { - "type": "text", - "properties": { - "name": "naturalCustomer.companyName", - "id": ".naturalCustomer.companyName", - "label": "Company name" - } - }, - { - "type": "text", - "properties": { - "name": "naturalCustomer.office", - "id": ".naturalCustomer.office", - "label": "Registered office" - } - }, - { - "type": "text", - "properties": { - "name": "naturalCustomer.companyDocument", - "id": ".naturalCustomer.companyDocument", - "label": "Company identification document" - } - }, - { - "type": "file", - "properties": { - "name": "naturalCustomer.companyDocumentAttachment", - "id": ".naturalCustomer.companyDocumentAttachment", - "label": "Document attachment", - "required": true, - "maxBites": 2097152, - "accept": ".png", - "help": "PNG file with max size of 2 mega bytes" - } - } - ] - } + + "label": "Natural customer form", + "name": "algo", + "id": "algo", + "before": "a) Country risk (nationality)", + "after": "a) Country risk (nationality)", + "fields": [ + { + "type": "text", + + "name": "naturalCustomer.fullName", + "id": ".naturalCustomer.fullName", + "label": "Full name", + "required": true + }, + { + "type": "text", + + "name": "naturalCustomer.address", + "id": ".naturalCustomer.address", + "label": "Residential address", + "required": true + }, + { + "type": "integer", + + "name": "naturalCustomer.telephone", + "id": ".naturalCustomer.telephone", + "label": "Telephone" + }, + { + "type": "text", + + "name": "naturalCustomer.email", + "id": ".naturalCustomer.email", + "label": "E-mail" + }, + { + "type": "absoluteTimeText", + + "pattern": "dd/MM/yyyy", + "name": "naturalCustomer.dateOfBirth", + "id": ".naturalCustomer.dateOfBirth", + "label": "Date of birth", + "required": true + }, + { + "type": "text", + + "name": "naturalCustomer.nationality", + "id": ".naturalCustomer.nationality", + "label": "Nationality", + "required": true + }, + { + "type": "text", + + "name": "naturalCustomer.document", + "id": ".naturalCustomer.document", + "label": "Identification document", + "required": true + }, + { + "type": "file", + + "name": "naturalCustomer.documentAttachment", + "id": ".naturalCustomer.documentAttachment", + "label": "Document attachment", + "required": true, + "maxBites": 2097152, + "accept": ".pdf", + "help": "PDF file with max size of 2 mega bytes" + }, + { + "type": "text", + + "name": "naturalCustomer.companyName", + "id": ".naturalCustomer.companyName", + "label": "Company name" + }, + { + "type": "text", + + "name": "naturalCustomer.office", + "id": ".naturalCustomer.office", + "label": "Registered office" + }, + { + "type": "text", + + "name": "naturalCustomer.companyDocument", + "id": ".naturalCustomer.companyDocument", + "label": "Company identification document" + }, + { + "type": "file", + + "name": "naturalCustomer.companyDocumentAttachment", + "id": ".naturalCustomer.companyDocumentAttachment", + "label": "Document attachment", + "required": true, + "maxBites": 2097152, + "accept": ".png", + "help": "PNG file with max size of 2 mega bytes" + } + ] }, - - + { "type": "group", - "properties": { - "label": "Natural customer form", - "name": "algo", - "id": "algo", - "before": "a) Country risk (nationality)", - "after": "a) Country risk (nationality)", - "fields": [ - { - "type": "text", - "properties": { - "name": "legalCustomer.companyName", - "id": ".legalCustomer.companyName", - "label": "Company name", - "required": true - } - }, - { - "type": "text", - "properties": { - "name": "legalCustomer.domicile", - "id": ".legalCustomer.domicile", - "label": "Domicile", - "required": true - } - }, - { - "type": "text", - "properties": { - "name": "legalCustomer.contactPerson", - "id": ".legalCustomer.contactPerson", - "label": "Contact person" - } - }, - { - "type": "text", - "properties": { - "name": "legalCustomer.telephone", - "id": ".legalCustomer.telephone", - "label": "Telephone" - } - }, - { - "type": "text", - "properties": { - "name": "legalCustomer.email", - "id": ".legalCustomer.email", - "label": "E-mail" - } - }, - { - "type": "text", - "properties": { - "name": "legalCustomer.document", - "id": ".legalCustomer.document", - "label": "Identification document", - "help": "Not older than 12 month" - } - }, - { - "type": "file", - "properties": { - "name": "legalCustomer.documentAttachment", - "id": ".legalCustomer.documentAttachment", - "label": "Document attachment", - "required": true, - "maxBites": 2097152, - "accept": ".png", - "help": "PNG file with max size of 2 mega bytes" - } - } - ] - } + + "label": "Natural customer form", + "name": "algo", + "id": "algo", + "before": "a) Country risk (nationality)", + "after": "a) Country risk (nationality)", + "fields": [ + { + "type": "text", + + "name": "legalCustomer.companyName", + "id": ".legalCustomer.companyName", + "label": "Company name", + "required": true + }, + { + "type": "text", + + "name": "legalCustomer.domicile", + "id": ".legalCustomer.domicile", + "label": "Domicile", + "required": true + }, + { + "type": "text", + + "name": "legalCustomer.contactPerson", + "id": ".legalCustomer.contactPerson", + "label": "Contact person" + }, + { + "type": "text", + + "name": "legalCustomer.telephone", + "id": ".legalCustomer.telephone", + "label": "Telephone" + }, + { + "type": "text", + + "name": "legalCustomer.email", + "id": ".legalCustomer.email", + "label": "E-mail" + }, + { + "type": "text", + + "name": "legalCustomer.document", + "id": ".legalCustomer.document", + "label": "Identification document", + "help": "Not older than 12 month" + }, + { + "type": "file", + + "name": "legalCustomer.documentAttachment", + "id": ".legalCustomer.documentAttachment", + "label": "Document attachment", + "required": true, + "maxBites": 2097152, + "accept": ".png", + "help": "PNG file with max size of 2 mega bytes" + } + ] } ] }, @@ -238,108 +215,99 @@ "fields": [ { "type": "array", - "properties": { - "name": "businessEstablisher", - "id": ".businessEstablisher", - "label": "Persons", - "required": true, - "labelFieldId": "fullName", - "placeholder": "this is the placeholder", - "fields": [ - { - "type": "text", - "properties": { - "name": "fullName", - "id": ".fullName", - "label": "Full name", - "required": true - } - }, - { - "type": "text", - "properties": { - "name": "address", - "id": ".address", - "label": "Residential address", - "required": true - } - }, - { - "type": "absoluteTime", - "properties": { - "pattern": "dd/MM/yyyy", - "name": "dateOfBirth", - "id": ".dateOfBirth", - "label": "Date of birth", - "required": true - } - }, - - { - "type": "text", - "properties": { - "name": "nationality", - "id": ".nationality", - "label": "Nationality", - "required": true - } - }, - { - "type": "text", - "properties": { - "name": "typeOfAuthorization", - "id": ".typeOfAuthorization", - "label": "Type of authorization (signatory of representation)", - "required": true - } - }, - { - "type": "file", - "properties": { - "name": "documentAttachment", - "id": ".documentAttachment", - "label": "Identification document attachment", - "required": true, - "maxBites": 2097152, - "accept": ".pdf", - "help": "PDF file with max size of 2 mega bytes" - } - }, - { - "type": "choiceStacked", - "properties": { - "name": "powerOfAttorneyArrangements", - "id": ".powerOfAttorneyArrangements", - "label": "Power of attorney arrangements", - "required": true, - "choices": [ - { - "label": "CR extract", - "value": "cr" - }, - { - "label": "Mandate", - "value": "mandate" - }, - { - "label": "Other", - "value": "other" - } - ] - } - }, - { - "type": "text", - "properties": { - "name": "powerOfAttorneyArrangementsOther", - "id": ".powerOfAttorneyArrangementsOther", - "label": "Power of attorney arrangements", - "required": true + + "name": "businessEstablisher", + "id": ".businessEstablisher", + "label": "Persons", + "required": true, + "labelFieldId": "fullName", + "placeholder": "this is the placeholder", + "fields": [ + { + "type": "text", + + "name": "fullName", + "id": ".fullName", + "label": "Full name", + "required": true + }, + { + "type": "text", + + "name": "address", + "id": ".address", + "label": "Residential address", + "required": true + }, + { + "type": "absoluteTimeText", + + "pattern": "dd/MM/yyyy", + "name": "dateOfBirth", + "id": ".dateOfBirth", + "label": "Date of birth", + "required": true + }, + + { + "type": "text", + + "name": "nationality", + "id": ".nationality", + "label": "Nationality", + "required": true + }, + { + "type": "text", + + "name": "typeOfAuthorization", + "id": ".typeOfAuthorization", + "label": "Type of authorization (signatory of representation)", + "required": true + }, + { + "type": "file", + + "name": "documentAttachment", + "id": ".documentAttachment", + "label": "Identification document attachment", + "required": true, + "maxBites": 2097152, + "accept": ".pdf", + "help": "PDF file with max size of 2 mega bytes" + }, + { + "type": "choiceStacked", + + "name": "powerOfAttorneyArrangements", + "id": ".powerOfAttorneyArrangements", + "label": "Power of attorney arrangements", + "required": true, + "choices": [ + { + "label": "CR extract", + "value": "cr" + }, + { + "label": "Mandate", + "value": "mandate" + }, + { + "label": "Other", + "value": "other" } - } - ], - "labelField": "fullName" - } + ] + }, + { + "type": "text", + + "name": "powerOfAttorneyArrangementsOther", + "id": ".powerOfAttorneyArrangementsOther", + "label": "Power of attorney arrangements", + "required": true + } + ], + "labelField": "fullName" } ] }, @@ -347,104 +315,97 @@ "title": "Acceptance of business relationship", "fields": [ { - "type": "absoluteTime", - "properties": { - "name": "acceptance.when", - "id": ".acceptance.when", - "pattern": "dd/MM/yyyy", - "converterId": "Taler.AbsoluteTime", - "label": "Date (conclusion of contract)" - } + "type": "absoluteTimeText", + + "name": "acceptance.when", + "id": ".acceptance.when", + "pattern": "dd/MM/yyyy", + "converterId": "Taler.AbsoluteTime", + "label": "Date (conclusion of contract)" }, { "type": "choiceStacked", - "properties": { - "name": "acceptance.acceptedBy", - "id": ".acceptance.acceptedBy", - "label": "Accepted by", - "required": true, - "choices": [ - { - "label": "Face-to-face meeting with customer", - "value": "face-to-face" - }, - { - "label": "Correspondence: authenticated copy of identification document obtained", - "value": "correspondence-document" - }, - { - "label": "Correspondence: residential address validated", - "value": "correspondence-address" - } - ] - } + + "name": "acceptance.acceptedBy", + "id": ".acceptance.acceptedBy", + "label": "Accepted by", + "required": true, + "choices": [ + { + "label": "Face-to-face meeting with customer", + "value": "face-to-face" + }, + { + "label": "Correspondence: authenticated copy of identification document obtained", + "value": "correspondence-document" + }, + { + "label": "Correspondence: residential address validated", + "value": "correspondence-address" + } + ] }, { "type": "choiceStacked", - "properties": { - "name": "acceptance.typeOfCorrespondence", - "id": ".acceptance.typeOfCorrespondence", - "label": "Type of correspondence service", - "choices": [ - { - "label": "to the customer", - "value": "customer" - }, - { - "label": "hold at bank", - "value": "bank" - }, - { - "label": "to the member", - "value": "member" - }, - { - "label": "to a third party", - "value": "third-party" - } - ] - } + + "name": "acceptance.typeOfCorrespondence", + "id": ".acceptance.typeOfCorrespondence", + "label": "Type of correspondence service", + "choices": [ + { + "label": "to the customer", + "value": "customer" + }, + { + "label": "hold at bank", + "value": "bank" + }, + { + "label": "to the member", + "value": "member" + }, + { + "label": "to a third party", + "value": "third-party" + } + ] }, { "type": "text", - "properties": { - "name": "acceptance.thirdPartyFullName", - "id": ".acceptance.thirdPartyFullName", - "label": "Third party full name", - "required": true - } + + "name": "acceptance.thirdPartyFullName", + "id": ".acceptance.thirdPartyFullName", + "label": "Third party full name", + "required": true }, { "type": "text", - "properties": { - "name": "acceptance.thirdPartyAddress", - "id": ".acceptance.thirdPartyAddress", - "label": "Third party address", - "required": true - } + + "name": "acceptance.thirdPartyAddress", + "id": ".acceptance.thirdPartyAddress", + "label": "Third party address", + "required": true }, { "type": "selectMultiple", - "properties": { - "name": "acceptance.language", - "id": ".acceptance.language", - "label": "Languages", - "choices": [ - { - "label": "Espanol", - "value": "es" - } - ], - "unique": true - } + + "name": "acceptance.language", + "id": ".acceptance.language", + "label": "Languages", + "choices": [ + { + "label": "Espanol", + "value": "es" + } + ], + "unique": true }, { "type": "textArea", - "properties": { - "name": "acceptance.furtherInformation", - "id": ".acceptance.furtherInformation", - "label": "Further information" - } + + "name": "acceptance.furtherInformation", + "id": ".acceptance.furtherInformation", + "label": "Further information" } ] }, @@ -454,34 +415,33 @@ "fields": [ { "type": "choiceStacked", - "properties": { - "name": "establishment", - "id": ".establishment", - "label": "The customer is", - "required": true, - "choices": [ - { - "label": "a natural person and there are no doubts that this person is the sole beneficial owner of the assets", - "value": "natural" - }, - { - "label": "a foundation (or a similar construct; incl. underlying companies)", - "value": "foundation" - }, - { - "label": "a trust (incl. underlying companies)", - "value": "trust" - }, - { - "label": "a life insurance policy with separately managed accounts/securities accounts", - "value": "insurance-wrapper" - }, - { - "label": "all other cases", - "value": "other" - } - ] - } + + "name": "establishment", + "id": ".establishment", + "label": "The customer is", + "required": true, + "choices": [ + { + "label": "a natural person and there are no doubts that this person is the sole beneficial owner of the assets", + "value": "natural" + }, + { + "label": "a foundation (or a similar construct; incl. underlying companies)", + "value": "foundation" + }, + { + "label": "a trust (incl. underlying companies)", + "value": "trust" + }, + { + "label": "a life insurance policy with separately managed accounts/securities accounts", + "value": "insurance-wrapper" + }, + { + "label": "all other cases", + "value": "other" + } + ] } ] }, @@ -491,12 +451,11 @@ "fields": [ { "type": "textArea", - "properties": { - "name": "embargoEvaluation", - "id": ".embargoEvaluation", - "help": "The evaluation must be made at the beginning of the business relationship and has to be repeated in the case of permanent business relationship every time the according lists are updated.", - "label": "Evaluation" - } + + "name": "embargoEvaluation", + "id": ".embargoEvaluation", + "help": "The evaluation must be made at the beginning of the business relationship and has to be repeated in the case of permanent business relationship every time the according lists are updated.", + "label": "Evaluation" } ] }, @@ -506,42 +465,59 @@ "fields": [ { "type": "choiceStacked", - "properties": { - "name": "cashTransactions.typeOfBusiness", - "id": ".cashTransactions.typeOfBusiness", - "label": "Type of business relationship", - "choices": [ - { - "label": "Money exchange", - "value": "money-exchange" - }, - { - "label": "Money and asset transfer", - "value": "money-and-asset-transfer" - }, - { - "label": "Other cash transactions. Specify below", - "value": "other" - } - ] - } + + "name": "cashTransactions.typeOfBusiness", + "id": ".cashTransactions.typeOfBusiness", + "label": "Type of business relationship", + "choices": [ + { + "label": "Money exchange", + "value": "money-exchange" + }, + { + "label": "Money and asset transfer", + "value": "money-and-asset-transfer" + }, + { + "label": "Other cash transactions. Specify below", + "value": "other" + } + ] }, { "type": "text", - "properties": { - "name": "cashTransactions.otherTypeOfBusiness", - "id": ".cashTransactions.otherTypeOfBusiness", - "required": true, - "label": "Specify other cash transactions:" - } + + "name": "cashTransactions.otherTypeOfBusiness", + "id": ".cashTransactions.otherTypeOfBusiness", + "required": true, + "label": "Specify other cash transactions:" }, { "type": "textArea", - "properties": { - "name": "cashTransactions.purpose", - "id": ".cashTransactions.purpose", - "label": "Purpose of the business relationship (purpose of service requested)" - } + "name": "cashTransactions.purpose", + "id": ".cashTransactions.purpose", + "label": "Purpose of the business relationship (purpose of service requested)" + } + ] + } + ] + } + }, + { + "label": "Example form", + "id": "example", + "version": 1, + "config": { + "type": "double-column", + "design": [ + { + "title": "Boolean inputs", + "fields": [ + { + "type": "toggle", + "name": "yes", + "id": ".yes", + "label": "Yes or no?" } ] } diff --git a/packages/aml-backoffice-ui/src/forms/simplest.ts b/packages/aml-backoffice-ui/src/forms/simplest.ts @@ -18,7 +18,7 @@ import type { DoubleColumnForm, DoubleColumnFormSection, InternationalizationAPI, - UIHandlerId + UIHandlerId, } from "@gnu-taler/web-util/browser"; export const v1 = (i18n: InternationalizationAPI): DoubleColumnForm => ({ @@ -29,11 +29,9 @@ export const v1 = (i18n: InternationalizationAPI): DoubleColumnForm => ({ fields: [ { type: "textArea", - properties: { - id: ".comment" as UIHandlerId, - name: "comment", - label: i18n.str`Comment`, - }, + id: ".comment" as UIHandlerId, + name: "comment", + label: i18n.str`Comment`, }, ], }, @@ -61,36 +59,32 @@ export function resolutionSection( fields: [ { type: "choiceHorizontal", - properties: { - id: ".state" as UIHandlerId, - name: "state", - label: i18n.str`New state`, - converterId: "TalerExchangeApi.AmlState", - choices: [ - { - value: "frozen", - label: i18n.str`Frozen`, - }, - { - value: "pending", - label: i18n.str`Pending`, - }, - { - value: "normal", - label: i18n.str`Normal`, - }, - ], - }, + id: ".state" as UIHandlerId, + name: "state", + label: i18n.str`New state`, + converterId: "TalerExchangeApi.AmlState", + choices: [ + { + value: "frozen", + label: i18n.str`Frozen`, + }, + { + value: "pending", + label: i18n.str`Pending`, + }, + { + value: "normal", + label: i18n.str`Normal`, + }, + ], }, { type: "amount", - properties: { - id: ".threshold" as UIHandlerId, - currency: "NETZBON", - name: "threshold", - converterId: "Taler.Amount", - label: i18n.str`New threshold`, - }, + id: ".threshold" as UIHandlerId, + currency: "NETZBON", + name: "threshold", + converterId: "Taler.Amount", + label: i18n.str`New threshold`, }, ], }; diff --git a/packages/aml-backoffice-ui/src/hooks/form.ts b/packages/aml-backoffice-ui/src/hooks/form.ts @@ -22,7 +22,7 @@ import { } from "@gnu-taler/taler-util"; import { UIFieldHandler, - UIFormFieldConfig, + UIFormElementConfig, UIHandlerId, } from "@gnu-taler/web-util/browser"; import { useState } from "preact/hooks"; @@ -167,21 +167,21 @@ export function setValueDeeper(object: any, names: string[], value: any): any { } export function getShapeFromFields( - fields: UIFormFieldConfig[], + fields: UIFormElementConfig[], ): Array<UIHandlerId> { const shape: Array<UIHandlerId> = []; fields.forEach((field) => { - if ("id" in field.properties) { + if ("id" in field) { // FIXME: this should be a validation when loading the form // consistency check - if (shape.indexOf(field.properties.id) !== -1) { - throw Error(`already present: ${field.properties.id}`); + if (shape.indexOf(field.id) !== -1) { + throw Error(`already present: ${field.id}`); } - shape.push(field.properties.id); + shape.push(field.id); } else if (field.type === "group") { Array.prototype.push.apply( shape, - getShapeFromFields(field.properties.fields), + getShapeFromFields(field.fields), ); } }); @@ -189,24 +189,24 @@ export function getShapeFromFields( } export function getRequiredFields( - fields: UIFormFieldConfig[], + fields: UIFormElementConfig[], ): Array<UIHandlerId> { const shape: Array<UIHandlerId> = []; fields.forEach((field) => { - if ("id" in field.properties) { + if ("id" in field) { // FIXME: this should be a validation when loading the form // consistency check - if (shape.indexOf(field.properties.id) !== -1) { - throw Error(`already present: ${field.properties.id}`); + if (shape.indexOf(field.id) !== -1) { + throw Error(`already present: ${field.id}`); } - if (!field.properties.required) { + if (!field.required) { return; } - shape.push(field.properties.id); + shape.push(field.id); } else if (field.type === "group") { Array.prototype.push.apply( shape, - getRequiredFields(field.properties.fields), + getRequiredFields(field.fields), ); } }); diff --git a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx @@ -145,7 +145,6 @@ export function CaseUpdate({ const validatedForm = state.status !== "ok" ? undefined : state.result; - console.log(state.errors); const submitHandler = validatedForm === undefined ? undefined diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx @@ -21,11 +21,10 @@ import { } from "@gnu-taler/taler-util"; import { DefaultForm, - FlexibleForm, - UIFormField, - UIFormFieldConfig, + FormConfiguration, + UIFormElementConfig, UIHandlerId, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; import { Fragment, VNode, h } from "preact"; @@ -42,7 +41,7 @@ export function ShowConsolidated({ const cons = getConsolidated(history, until); - const form: FlexibleForm = { + const form: FormConfiguration = { type: "double-column", design: [ { @@ -50,34 +49,30 @@ export function ShowConsolidated({ fields: [ { type: "amount", - properties: { - id: ".aml.threshold" as UIHandlerId, - currency: "NETZBON", - label: i18n.str`Threshold`, - name: "aml.threshold", - }, + id: ".aml.threshold" as UIHandlerId, + currency: "NETZBON", + label: i18n.str`Threshold`, + name: "aml.threshold", }, { type: "choiceHorizontal", - properties: { - label: i18n.str`State`, - name: "aml.state", - id: ".aml.state" as UIHandlerId, - choices: [ - { - label: i18n.str`Frozen`, - value: "frozen", - }, - { - label: i18n.str`Pending`, - value: "pending", - }, - { - label: i18n.str`Normal`, - value: "normal", - }, - ], - }, + label: i18n.str`State`, + name: "aml.state", + id: ".aml.state" as UIHandlerId, + choices: [ + { + label: i18n.str`Frozen`, + value: "frozen", + }, + { + label: i18n.str`Pending`, + value: "pending", + }, + { + label: i18n.str`Normal`, + value: "normal", + }, + ], }, ], }, @@ -85,18 +80,16 @@ export function ShowConsolidated({ ? { title: i18n.str`KYC`, fields: Object.entries(cons.kyc).map(([key, field]) => { - const result: UIFormFieldConfig = { + const result: UIFormElementConfig = { type: "text", - properties: { - label: key as TranslatedString, - id: `kyc.${key}.value` as UIHandlerId, - name: `kyc.${key}.value`, - help: `${field.provider} since ${ - field.since.t_ms === "never" - ? "never" - : format(field.since.t_ms, "dd/MM/yyyy") - }` as TranslatedString, - }, + label: key as TranslatedString, + id: `kyc.${key}.value` as UIHandlerId, + name: `kyc.${key}.value`, + help: `${field.provider} since ${ + field.since.t_ms === "never" + ? "never" + : format(field.since.t_ms, "dd/MM/yyyy") + }` as TranslatedString, }; return result; }), diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx @@ -19,10 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { - Amounts, - MerchantTemplateContractDetails, -} from "@gnu-taler/taler-util"; +import { Amounts, TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; @@ -36,12 +33,12 @@ import { InputCurrency } from "../../../../components/form/InputCurrency.js"; import { InputDuration } from "../../../../components/form/InputDuration.js"; import { InputNumber } from "../../../../components/form/InputNumber.js"; import { InputSearchOnList } from "../../../../components/form/InputSearchOnList.js"; +import { InputTab } from "../../../../components/form/InputTab.js"; import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; import { useBackendContext } from "../../../../context/backend.js"; import { MerchantBackend } from "../../../../declaration.js"; import { useInstanceOtpDevices } from "../../../../hooks/otp.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; -import { InputTab } from "../../../../components/form/InputTab.js"; enum Steps { BOTH_FIXED, @@ -59,8 +56,8 @@ interface Props { export function CreatePage({ onCreate, onBack }: Props): VNode { const { i18n } = useTranslationContext(); - const { url: backendURL } = useBackendContext() - const devices = useInstanceOtpDevices() + const { url: backendURL } = useBackendContext(); + const devices = useInstanceOtpDevices(); const [state, setState] = useState<Partial<Entity>>({ template_contract: { @@ -88,32 +85,37 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { template_contract: !state.template_contract ? undefined : undefinedIfEmpty({ - amount: !(state.type === Steps.FIXED_PRICE || state.type === Steps.BOTH_FIXED) - ? undefined - : !state.template_contract?.amount - ? i18n.str`required` - : !parsedPrice - ? i18n.str`not valid` - : Amounts.isZero(parsedPrice) - ? i18n.str`must be greater than 0` - : undefined, - summary: !(state.type === Steps.FIXED_SUMMARY || state.type === Steps.BOTH_FIXED) - ? undefined - : !state.template_contract?.summary - ? i18n.str`required` - : undefined, - minimum_age: - state.template_contract.minimum_age < 0 - ? i18n.str`should be greater that 0` - : undefined, - pay_duration: !state.template_contract.pay_duration - ? i18n.str`can't be empty` - : state.template_contract.pay_duration.d_us === "forever" + amount: !( + state.type === Steps.FIXED_PRICE || state.type === Steps.BOTH_FIXED + ) ? undefined - : state.template_contract.pay_duration.d_us < 1000 * 1000 //less than one second - ? i18n.str`to short` + : !state.template_contract?.amount + ? i18n.str`required` + : !parsedPrice + ? i18n.str`not valid` + : Amounts.isZero(parsedPrice) + ? i18n.str`must be greater than 0` + : undefined, + summary: !( + state.type === Steps.FIXED_SUMMARY || + state.type === Steps.BOTH_FIXED + ) + ? undefined + : !state.template_contract?.summary + ? i18n.str`required` + : undefined, + minimum_age: + state.template_contract.minimum_age < 0 + ? i18n.str`should be greater that 0` : undefined, - } as Partial<MerchantTemplateContractDetails>), + pay_duration: !state.template_contract.pay_duration + ? i18n.str`can't be empty` + : state.template_contract.pay_duration.d_us === "forever" + ? undefined + : state.template_contract.pay_duration.d_us < 1000 * 1000 //less than one second + ? i18n.str`to short` + : undefined, + } as Partial<TalerMerchantApi.TemplateContractDetails>), }; const hasErrors = Object.keys(errors).some( @@ -132,11 +134,11 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { delete state.template_contract.summary; } } - delete state.type + delete state.type; return onCreate(state as any); }; - const deviceList = !devices.ok ? [] : devices.data.otp_devices + const deviceList = !devices.ok ? [] : devices.data.otp_devices; return ( <div> @@ -166,10 +168,14 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { label={i18n.str`Type`} help={(() => { switch (state.type) { - case Steps.NON_FIXED: return i18n.str`User will be able to input price and summary before payment.` - case Steps.FIXED_PRICE: return i18n.str`User will be able to add a summary before payment.` - case Steps.FIXED_SUMMARY: return i18n.str`User will be able to set the price before payment.` - case Steps.BOTH_FIXED: return i18n.str`User will not be able to change the price or the summary.` + case Steps.NON_FIXED: + return i18n.str`User will be able to input price and summary before payment.`; + case Steps.FIXED_PRICE: + return i18n.str`User will be able to add a summary before payment.`; + case Steps.FIXED_SUMMARY: + return i18n.str`User will be able to set the price before payment.`; + case Steps.BOTH_FIXED: + return i18n.str`User will not be able to change the price or the summary.`; } })()} tooltip={i18n.str`Define what the user be allowed to modify`} @@ -181,28 +187,34 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { ]} toStr={(v: Steps): string => { switch (v) { - case Steps.NON_FIXED: return i18n.str`Simple` - case Steps.FIXED_PRICE: return i18n.str`With price` - case Steps.FIXED_SUMMARY: return i18n.str`With summary` - case Steps.BOTH_FIXED: return i18n.str`With price and summary` + case Steps.NON_FIXED: + return i18n.str`Simple`; + case Steps.FIXED_PRICE: + return i18n.str`With price`; + case Steps.FIXED_SUMMARY: + return i18n.str`With summary`; + case Steps.BOTH_FIXED: + return i18n.str`With price and summary`; } }} /> - {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_SUMMARY ? + {state.type === Steps.BOTH_FIXED || + state.type === Steps.FIXED_SUMMARY ? ( <Input name="template_contract.summary" inputType="multiline" label={i18n.str`Fixed summary`} tooltip={i18n.str`If specified, this template will create order with the same summary`} /> - : undefined} - {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_PRICE ? + ) : undefined} + {state.type === Steps.BOTH_FIXED || + state.type === Steps.FIXED_PRICE ? ( <InputCurrency name="template_contract.amount" label={i18n.str`Fixed price`} tooltip={i18n.str`If specified, this template will create order with the same price`} /> - : undefined} + ) : undefined} <InputNumber name="template_contract.minimum_age" label={i18n.str`Minimum age`} @@ -224,12 +236,11 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { <InputSearchOnList label={i18n.str`Search device`} onChange={(p) => setState((v) => ({ ...v, otp_id: p?.id }))} - list={deviceList.map(e => ({ + list={deviceList.map((e) => ({ description: e.device_description, - id: e.otp_device_id + id: e.otp_device_id, }))} /> - </FormProvider> <div class="buttons is-right mt-5"> diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx @@ -77,7 +77,7 @@ export function QrPage({ contract, id: templateId, onBack }: Props): VNode { const payTemplateUri = stringifyPayTemplateUri({ merchantBaseUrl, templateId, - templateParams + //templateParams }) const issuer = encodeURIComponent( diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx @@ -19,10 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { - Amounts, - MerchantTemplateContractDetails, -} from "@gnu-taler/taler-util"; +import { Amounts, TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; @@ -35,11 +32,11 @@ import { Input } from "../../../../components/form/Input.js"; import { InputCurrency } from "../../../../components/form/InputCurrency.js"; import { InputDuration } from "../../../../components/form/InputDuration.js"; import { InputNumber } from "../../../../components/form/InputNumber.js"; +import { InputTab } from "../../../../components/form/InputTab.js"; import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; import { useBackendContext } from "../../../../context/backend.js"; import { MerchantBackend, WithId } from "../../../../declaration.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; -import { InputTab } from "../../../../components/form/InputTab.js"; enum Steps { BOTH_FIXED, @@ -58,10 +55,11 @@ interface Props { export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { const { i18n } = useTranslationContext(); - const { url: backendURL } = useBackendContext() + const { url: backendURL } = useBackendContext(); const intialStep = - template.template_contract?.amount === undefined && template.template_contract?.summary === undefined + template.template_contract?.amount === undefined && + template.template_contract?.summary === undefined ? Steps.NON_FIXED : template.template_contract?.summary === undefined ? Steps.FIXED_PRICE @@ -69,7 +67,10 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { ? Steps.FIXED_SUMMARY : Steps.BOTH_FIXED; - const [state, setState] = useState<Partial<Entity & { type: Steps }>>({ ...template, type: intialStep }); + const [state, setState] = useState<Partial<Entity & { type: Steps }>>({ + ...template, + type: intialStep, + }); const parsedPrice = !state.template_contract?.amount ? undefined @@ -82,32 +83,37 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { template_contract: !state.template_contract ? undefined : undefinedIfEmpty({ - amount: !(state.type === Steps.FIXED_PRICE || state.type === Steps.BOTH_FIXED) - ? undefined - : !state.template_contract?.amount - ? i18n.str`required` - : !parsedPrice - ? i18n.str`not valid` - : Amounts.isZero(parsedPrice) - ? i18n.str`must be greater than 0` - : undefined, - summary: !(state.type === Steps.FIXED_SUMMARY || state.type === Steps.BOTH_FIXED) - ? undefined - : !state.template_contract?.summary - ? i18n.str`required` - : undefined, - minimum_age: - state.template_contract.minimum_age < 0 - ? i18n.str`should be greater that 0` - : undefined, - pay_duration: !state.template_contract.pay_duration - ? i18n.str`can't be empty` - : state.template_contract.pay_duration.d_us === "forever" + amount: !( + state.type === Steps.FIXED_PRICE || state.type === Steps.BOTH_FIXED + ) ? undefined - : state.template_contract.pay_duration.d_us < 1000 * 1000 // less than one second - ? i18n.str`to short` + : !state.template_contract?.amount + ? i18n.str`required` + : !parsedPrice + ? i18n.str`not valid` + : Amounts.isZero(parsedPrice) + ? i18n.str`must be greater than 0` + : undefined, + summary: !( + state.type === Steps.FIXED_SUMMARY || + state.type === Steps.BOTH_FIXED + ) + ? undefined + : !state.template_contract?.summary + ? i18n.str`required` + : undefined, + minimum_age: + state.template_contract.minimum_age < 0 + ? i18n.str`should be greater that 0` : undefined, - } as Partial<MerchantTemplateContractDetails>), + pay_duration: !state.template_contract.pay_duration + ? i18n.str`can't be empty` + : state.template_contract.pay_duration.d_us === "forever" + ? undefined + : state.template_contract.pay_duration.d_us < 1000 * 1000 // less than one second + ? i18n.str`to short` + : undefined, + } as Partial<TalerMerchantApi.TemplateContractDetails>), }; const hasErrors = Object.keys(errors).some( @@ -126,11 +132,10 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { delete state.template_contract.summary; } } - delete state.type + delete state.type; return onUpdate(state as any); }; - return ( <div> <section class="section"> @@ -176,10 +181,14 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { label={i18n.str`Type`} help={(() => { switch (state.type) { - case Steps.NON_FIXED: return i18n.str`User will be able to input price and summary before payment.` - case Steps.FIXED_PRICE: return i18n.str`User will be able to add a summary before payment.` - case Steps.FIXED_SUMMARY: return i18n.str`User will be able to set the price before payment.` - case Steps.BOTH_FIXED: return i18n.str`User will not be able to change the price or the summary.` + case Steps.NON_FIXED: + return i18n.str`User will be able to input price and summary before payment.`; + case Steps.FIXED_PRICE: + return i18n.str`User will be able to add a summary before payment.`; + case Steps.FIXED_SUMMARY: + return i18n.str`User will be able to set the price before payment.`; + case Steps.BOTH_FIXED: + return i18n.str`User will not be able to change the price or the summary.`; } })()} tooltip={i18n.str`Define what the user be allowed to modify`} @@ -191,28 +200,34 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { ]} toStr={(v: Steps): string => { switch (v) { - case Steps.NON_FIXED: return i18n.str`Simple` - case Steps.FIXED_PRICE: return i18n.str`With price` - case Steps.FIXED_SUMMARY: return i18n.str`With summary` - case Steps.BOTH_FIXED: return i18n.str`With price and summary` + case Steps.NON_FIXED: + return i18n.str`Simple`; + case Steps.FIXED_PRICE: + return i18n.str`With price`; + case Steps.FIXED_SUMMARY: + return i18n.str`With summary`; + case Steps.BOTH_FIXED: + return i18n.str`With price and summary`; } }} /> - {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_SUMMARY ? + {state.type === Steps.BOTH_FIXED || + state.type === Steps.FIXED_SUMMARY ? ( <Input name="template_contract.summary" inputType="multiline" label={i18n.str`Fixed summary`} tooltip={i18n.str`If specified, this template will create order with the same summary`} /> - : undefined} - {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_PRICE ? + ) : undefined} + {state.type === Steps.BOTH_FIXED || + state.type === Steps.FIXED_PRICE ? ( <InputCurrency name="template_contract.amount" label={i18n.str`Fixed price`} tooltip={i18n.str`If specified, this template will create order with the same price`} /> - : undefined} + ) : undefined} <InputNumber name="template_contract.minimum_age" label={i18n.str`Minimum age`} diff --git a/packages/bank-ui/src/i18n/bank.pot b/packages/bank-ui/src/i18n/bank.pot @@ -263,11 +263,13 @@ msgstr "" #: src/pages/PaytoWireTransferForm.tsx:457 #, c-format +msgctxt "wire_transfer" msgid "Cancel" msgstr "" #: src/pages/PaytoWireTransferForm.tsx:471 #, c-format +msgctxt "wire_transfer" msgid "Send" msgstr "" diff --git a/packages/bank-ui/src/i18n/de.po b/packages/bank-ui/src/i18n/de.po @@ -14,7 +14,7 @@ msgstr "" "Project-Id-Version: Taler Wallet\n" "Report-Msgid-Bugs-To: taler@gnu.org\n" "POT-Creation-Date: 2016-11-23 00:00+0100\n" -"PO-Revision-Date: 2024-03-21 21:39+0000\n" +"PO-Revision-Date: 2024-05-05 09:32+0000\n" "Last-Translator: Stefan Kügel <skuegel@web.de>\n" "Language-Team: German <https://weblate.taler.net/projects/gnu-taler/" "taler-bank-spa/de/>\n" @@ -23,7 +23,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.2.1\n" +"X-Generator: Weblate 5.4.3\n" #: src/utils.ts:137 #, c-format @@ -268,7 +268,7 @@ msgstr "" #: src/pages/PaytoWireTransferForm.tsx:457 #, c-format msgid "Cancel" -msgstr "" +msgstr "Zurück" #: src/pages/PaytoWireTransferForm.tsx:471 #, c-format @@ -490,7 +490,7 @@ msgstr "" #: src/pages/OperationState/views.tsx:319 #, c-format msgid "Close" -msgstr "" +msgstr "Schließen" #: src/pages/OperationState/views.tsx:399 #, c-format @@ -558,7 +558,7 @@ msgstr "" #: src/pages/WalletWithdrawForm.tsx:253 #, c-format msgid "Continue" -msgstr "" +msgstr "Weiter" #: src/pages/WalletWithdrawForm.tsx:282 #, c-format diff --git a/packages/bank-ui/src/i18n/en.po b/packages/bank-ui/src/i18n/en.po @@ -1,1784 +0,0 @@ -# This file is part of GNU Taler -# (C) 2021 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/> -# -msgid "" -msgstr "" -"Project-Id-Version: Taler Wallet\n" -"Report-Msgid-Bugs-To: taler@gnu.org\n" -"POT-Creation-Date: 2016-11-23 00:00+0100\n" -"PO-Revision-Date: 2022-01-08 09:57+0100\n" -"Last-Translator: <translate@taler.net>\n" -"Language-Team: English\n" -"Language: en\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: src/utils.ts:137 -#, c-format -msgid "Operation failed, please report" -msgstr "" - -#: src/utils.ts:156 -#, c-format -msgid "Request timeout" -msgstr "" - -#: src/utils.ts:165 -#, c-format -msgid "Request throttled" -msgstr "" - -#: src/utils.ts:174 -#, c-format -msgid "Malformed response" -msgstr "" - -#: src/utils.ts:183 -#, c-format -msgid "Network error" -msgstr "" - -#: src/utils.ts:192 -#, c-format -msgid "Unexpected request error" -msgstr "" - -#: src/utils.ts:201 -#, c-format -msgid "Unexpected error" -msgstr "" - -#: src/utils.ts:377 -#, c-format -msgid "IBAN numbers usually have more that 4 digits" -msgstr "" - -#: src/utils.ts:379 -#, c-format -msgid "IBAN numbers usually have less that 34 digits" -msgstr "" - -#: src/utils.ts:387 -#, c-format -msgid "IBAN country code not found" -msgstr "" - -#: src/utils.ts:401 -#, c-format -msgid "IBAN number is not valid, checksum is wrong" -msgstr "" - -#: src/context/config.ts:136 -#, c-format -msgid "" -"the bank backend is not supported. supported version \"%1$s\", server " -"version \"%2$s\"" -msgstr "" - -#: src/hooks/preferences.ts:55 -#, fuzzy, c-format -msgid "Max withdrawal amount" -msgstr "" - -#: src/hooks/preferences.ts:57 -#, c-format -msgid "Show withdrawal confirmation" -msgstr "" - -#: src/hooks/preferences.ts:59 -#, c-format -msgid "Show demo description" -msgstr "" - -#: src/hooks/preferences.ts:61 -#, c-format -msgid "Show install wallet first" -msgstr "" - -#: src/hooks/preferences.ts:63 -#, fuzzy, c-format -msgid "Use fast withdrawal form" -msgstr "" - -#: src/hooks/preferences.ts:65 -#, c-format -msgid "Show debug info" -msgstr "" - -#: src/pages/PaytoWireTransferForm.tsx:90 -#, c-format -msgid "required" -msgstr "" - -#: src/pages/PaytoWireTransferForm.tsx:92 -#, c-format -msgid "IBAN should have just uppercased letters and numbers" -msgstr "" - -#: src/pages/PaytoWireTransferForm.tsx:98 -#, c-format -msgid "not valid" -msgstr "" - -#: src/pages/PaytoWireTransferForm.tsx:100 -#, c-format -msgid "should be greater than 0" -msgstr "" - -#: src/pages/PaytoWireTransferForm.tsx:102 -#, c-format -msgid "balance is not enough" -msgstr "" - -#: src/pages/PaytoWireTransferForm.tsx:112 -#, c-format -msgid "does not follow the pattern" -msgstr "" - -#: src/pages/PaytoWireTransferForm.tsx:114 -#, c-format -msgid "only \"IBAN\" target are supported" -msgstr "" - -#: src/pages/PaytoWireTransferForm.tsx:116 -#, c-format -msgid "use the \"amount\" parameter to specify the amount to be transferred" -msgstr "" - -#: src/pages/PaytoWireTransferForm.tsx:118 -#, c-format -msgid "the amount is not valid" -msgstr "" - -#: src/pages/PaytoWireTransferForm.tsx:120 -#, c-format -msgid "" -"use the \"message\" parameter to specify a reference text for the transfer" -msgstr "" - -#: src/pages/PaytoWireTransferForm.tsx:160 -#, c-format -msgid "The request was invalid or the payto://-URI used unacceptable features." -msgstr "" - -#: src/pages/PaytoWireTransferForm.tsx:167 -#, c-format -msgid "Not enough permission to complete the operation." -msgstr "" - -#: src/pages/PaytoWireTransferForm.tsx:174 -#, c-format -msgid "The destination account \"%1$s\" was not found." -msgstr "" - -#: src/pages/PaytoWireTransferForm.tsx:181 -#, c-format -msgid "The origin and the destination of the transfer can't be the same." -msgstr "" - -#: src/pages/PaytoWireTransferForm.tsx:188 -#, c-format -msgid "Your balance is not enough." -msgstr "" - -#: src/pages/PaytoWireTransferForm.tsx:195 -#, c-format -msgid "The origin account \"%1$s\" was not found." -msgstr "" - -#: src/pages/PaytoWireTransferForm.tsx:212 -#, c-format -msgid "Wire transfer created!" -msgstr "" - -#: src/pages/PaytoWireTransferForm.tsx:270 -#, c-format -msgid "Using a form" -msgstr "" - -#: src/pages/PaytoWireTransferForm.tsx:310 -#, c-format -msgid "Import payto:// URI" -msgstr "" - -#: src/pages/PaytoWireTransferForm.tsx:335 -#, c-format -msgid "Recipient" -msgstr "" - -#: src/pages/PaytoWireTransferForm.tsx:359 -#, c-format -msgid "IBAN of the recipient's account" -msgstr "" - -#: src/pages/PaytoWireTransferForm.tsx:369 -#, c-format -msgid "Transfer subject" -msgstr "" - -#: src/pages/PaytoWireTransferForm.tsx:377 -#, c-format -msgid "subject" -msgstr "" - -#: src/pages/PaytoWireTransferForm.tsx:390 -#, c-format -msgid "some text to identify the transfer" -msgstr "" - -#: src/pages/PaytoWireTransferForm.tsx:400 -#, c-format -msgid "Amount" -msgstr "" - -#: src/pages/PaytoWireTransferForm.tsx:415 -#, fuzzy, c-format -msgid "amount to transfer" -msgstr "" - -#: src/pages/PaytoWireTransferForm.tsx:425 -#, c-format -msgid "payto URI:" -msgstr "" - -#: src/pages/PaytoWireTransferForm.tsx:436 -#, c-format -msgid "uniform resource identifier of the target account" -msgstr "" - -#: src/pages/PaytoWireTransferForm.tsx:437 -#, c-format -msgid "payto://iban/[receiver-iban]?message=[subject]&amount=[%1$s:X.Y]" -msgstr "" - -#: src/pages/PaytoWireTransferForm.tsx:457 -#, c-format -msgid "Cancel" -msgstr "" - -#: src/pages/PaytoWireTransferForm.tsx:471 -#, c-format -msgid "Send" -msgstr "" - -#: src/pages/LoginForm.tsx:71 -#, c-format -msgid "Missing username" -msgstr "" - -#: src/pages/LoginForm.tsx:75 -#, c-format -msgid "Missing password" -msgstr "" - -#: src/pages/LoginForm.tsx:104 -#, c-format -msgid "Wrong credentials for \"%1$s\"" -msgstr "" - -#: src/pages/LoginForm.tsx:111 -#, c-format -msgid "Account not found" -msgstr "" - -#: src/pages/LoginForm.tsx:142 -#, c-format -msgid "Username" -msgstr "" - -#: src/pages/LoginForm.tsx:156 -#, c-format -msgid "username of the account" -msgstr "" - -#: src/pages/LoginForm.tsx:175 -#, c-format -msgid "Password" -msgstr "" - -#: src/pages/LoginForm.tsx:188 -#, c-format -msgid "password of the account" -msgstr "" - -#: src/pages/LoginForm.tsx:223 -#, c-format -msgid "Check" -msgstr "" - -#: src/pages/LoginForm.tsx:237 -#, c-format -msgid "Log in" -msgstr "" - -#: src/pages/LoginForm.tsx:249 -#, c-format -msgid "Register" -msgstr "" - -#: src/components/Transactions/views.tsx:52 -#, c-format -msgid "Latest transactions" -msgstr "" - -#: src/components/Transactions/views.tsx:63 -#, c-format -msgid "Date" -msgstr "" - -#: src/components/Transactions/views.tsx:71 -#, c-format -msgid "Counterpart" -msgstr "" - -#: src/components/Transactions/views.tsx:75 -#, c-format -msgid "Subject" -msgstr "" - -#: src/components/Transactions/views.tsx:111 -#, c-format -msgid "sent" -msgstr "" - -#: src/components/Transactions/views.tsx:112 -#, c-format -msgid "received" -msgstr "" - -#: src/components/Transactions/views.tsx:127 -#, c-format -msgid "invalid value" -msgstr "" - -#: src/components/Transactions/views.tsx:136 -#, c-format -msgid "to" -msgstr "" - -#: src/components/Transactions/views.tsx:136 -#, c-format -msgid "from" -msgstr "" - -#: src/components/Transactions/views.tsx:202 -#, c-format -msgid "First page" -msgstr "" - -#: src/components/Transactions/views.tsx:209 -#, c-format -msgid "Next" -msgstr "" - -#: src/pages/WithdrawalConfirmationQuestion.tsx:86 -#, c-format -msgid "Wire transfer completed!" -msgstr "" - -#: src/pages/WithdrawalConfirmationQuestion.tsx:93 -#, c-format -msgid "The withdrawal has been aborted previously and can't be confirmed" -msgstr "" - -#: src/pages/WithdrawalConfirmationQuestion.tsx:100 -#, c-format -msgid "" -"The withdrawal operation can't be confirmed before a wallet accepted the " -"transaction." -msgstr "" - -#: src/pages/WithdrawalConfirmationQuestion.tsx:107 -#, c-format -msgid "The operation id is invalid." -msgstr "" - -#: src/pages/WithdrawalConfirmationQuestion.tsx:114 -#, c-format -msgid "The operation was not found." -msgstr "" - -#: src/pages/WithdrawalConfirmationQuestion.tsx:121 -#, c-format -msgid "Your balance is not enough for the operation." -msgstr "" - -#: src/pages/WithdrawalConfirmationQuestion.tsx:155 -#, c-format -msgid "" -"The reserve operation has been confirmed previously and can't be aborted" -msgstr "" - -#: src/pages/WithdrawalConfirmationQuestion.tsx:186 -#, fuzzy, c-format -msgid "Confirm the withdrawal operation" -msgstr "" - -#: src/pages/WithdrawalConfirmationQuestion.tsx:203 -#, c-format -msgid "Wire transfer details" -msgstr "" - -#: src/pages/WithdrawalConfirmationQuestion.tsx:217 -#, c-format -msgid "Taler Exchange operator's account" -msgstr "" - -#: src/pages/WithdrawalConfirmationQuestion.tsx:228 -#, c-format -msgid "Taler Exchange operator's name" -msgstr "" - -#: src/pages/WithdrawalConfirmationQuestion.tsx:317 -#, c-format -msgid "Transfer" -msgstr "" - -#: src/pages/WithdrawalConfirmationQuestion.tsx:342 -#, c-format -msgid "Authentication required" -msgstr "" - -#: src/pages/WithdrawalConfirmationQuestion.tsx:352 -#, c-format -msgid "This operation was created with other username" -msgstr "" - -#: src/pages/OperationState/views.tsx:209 -#, c-format -msgid "" -"Unauthorized to make the operation, maybe the session has expired or the " -"password changed." -msgstr "" - -#: src/pages/OperationState/views.tsx:218 -#, c-format -msgid "The operation was rejected due to insufficient funds." -msgstr "" - -#: src/pages/OperationState/views.tsx:268 -#, c-format -msgid "Withdrawal confirmed" -msgstr "" - -#: src/pages/OperationState/views.tsx:272 -#, c-format -msgid "" -"The wire transfer to the Taler operator has been initiated. You will soon " -"receive the requested amount in your Taler wallet." -msgstr "" - -#: src/pages/OperationState/views.tsx:287 -#, c-format -msgid "Do not show this again" -msgstr "" - -#: src/pages/OperationState/views.tsx:319 -#, c-format -msgid "Close" -msgstr "" - -#: src/pages/OperationState/views.tsx:399 -#, c-format -msgid "On this device" -msgstr "" - -#: src/pages/OperationState/views.tsx:404 -#, c-format -msgid "" -"If you are using a web browser on desktop you should access your wallet with " -"the GNU Taler WebExtension now or click the link if your WebExtension have " -"the \"Inject Taler support\" option enabled." -msgstr "" - -#: src/pages/OperationState/views.tsx:417 -#, c-format -msgid "Start" -msgstr "" - -#: src/pages/OperationState/views.tsx:426 -#, c-format -msgid "On a mobile phone" -msgstr "" - -#: src/pages/OperationState/views.tsx:431 -#, c-format -msgid "Scan the QR code with your mobile device." -msgstr "" - -#: src/pages/WalletWithdrawForm.tsx:73 -#, c-format -msgid "There is an operation already" -msgstr "" - -#: src/pages/WalletWithdrawForm.tsx:75 -#, fuzzy, c-format -msgid "Complete or cancel the operation in" -msgstr "" - -#: src/pages/WalletWithdrawForm.tsx:84 -#, c-format -msgid "this page" -msgstr "" - -#: src/pages/WalletWithdrawForm.tsx:101 -#, c-format -msgid "invalid" -msgstr "" - -#: src/pages/WalletWithdrawForm.tsx:116 -#, c-format -msgid "Server responded with an invalid withdraw URI" -msgstr "" - -#: src/pages/WalletWithdrawForm.tsx:117 -#, fuzzy, c-format -msgid "Withdraw URI: %1$s" -msgstr "" - -#: src/pages/WalletWithdrawForm.tsx:132 -#, c-format -msgid "The operation was rejected due to insufficient funds" -msgstr "" - -#: src/pages/WalletWithdrawForm.tsx:253 -#, c-format -msgid "Continue" -msgstr "" - -#: src/pages/WalletWithdrawForm.tsx:282 -#, c-format -msgid "Prepare your wallet" -msgstr "" - -#: src/pages/WalletWithdrawForm.tsx:285 -#, c-format -msgid "" -"After using your wallet you will need to confirm or cancel the operation on " -"this site." -msgstr "" - -#: src/pages/WalletWithdrawForm.tsx:295 -#, fuzzy, c-format -msgid "You need a GNU Taler Wallet" -msgstr "" - -#: src/pages/WalletWithdrawForm.tsx:300 -#, c-format -msgid "If you don't have one yet you can follow the instruction in" -msgstr "" - -#: src/pages/PaymentOptions.tsx:55 -#, c-format -msgid "Send money" -msgstr "" - -#: src/pages/PaymentOptions.tsx:73 -#, c-format -msgid "to a %1$s wallet" -msgstr "" - -#: src/pages/PaymentOptions.tsx:95 -#, c-format -msgid "Withdraw digital money into your mobile wallet or browser extension" -msgstr "" - -#: src/pages/PaymentOptions.tsx:109 -#, c-format -msgid "operation ready" -msgstr "" - -#: src/pages/PaymentOptions.tsx:129 -#, c-format -msgid "to another bank account" -msgstr "" - -#: src/pages/PaymentOptions.tsx:149 -#, c-format -msgid "Make a wire transfer to an account with known bank account number." -msgstr "" - -#: src/pages/PaymentOptions.tsx:171 -#, fuzzy, c-format -msgid "Transfer details" -msgstr "" - -#: src/pages/AccountPage/views.tsx:41 -#, c-format -msgid "This is a demo bank" -msgstr "" - -#: src/pages/AccountPage/views.tsx:46 -#, c-format -msgid "" -"This part of the demo shows how a bank that supports Taler directly would " -"work. In addition to using your own bank account, you can also see the " -"transaction history of some %1$s." -msgstr "" - -#: src/pages/AccountPage/views.tsx:53 -#, c-format -msgid "" -"This part of the demo shows how a bank that supports Taler directly would " -"work." -msgstr "" - -#: src/pages/AccountPage/views.tsx:70 -#, c-format -msgid "Pending account delete operation" -msgstr "" - -#: src/pages/AccountPage/views.tsx:72 -#, c-format -msgid "Pending account update operation" -msgstr "" - -#: src/pages/AccountPage/views.tsx:74 -#, c-format -msgid "Pending password update operation" -msgstr "" - -#: src/pages/AccountPage/views.tsx:76 -#, c-format -msgid "Pending transaction operation" -msgstr "" - -#: src/pages/AccountPage/views.tsx:78 -#, c-format -msgid "Pending withdrawal operation" -msgstr "" - -#: src/pages/AccountPage/views.tsx:80 -#, c-format -msgid "Pending cashout operation" -msgstr "" - -#: src/pages/AccountPage/views.tsx:91 -#, c-format -msgid "You can complete or cancel the operation in" -msgstr "" - -#: src/pages/BankFrame.tsx:64 -#, c-format -msgid "Internal error, please report." -msgstr "" - -#: src/pages/BankFrame.tsx:100 -#, c-format -msgid "Preferences" -msgstr "" - -#: src/pages/BankFrame.tsx:184 -#, c-format -msgid "Welcome, %1$s" -msgstr "" - -#: src/pages/WireTransfer.tsx:79 -#, c-format -msgid "Make a wire transfer" -msgstr "" - -#: src/pages/admin/AccountList.tsx:72 -#, c-format -msgid "Accounts" -msgstr "" - -#: src/pages/admin/AccountList.tsx:75 -#, c-format -msgid "A list of all business account in the bank." -msgstr "" - -#: src/pages/admin/AccountList.tsx:86 -#, c-format -msgid "Create account" -msgstr "" - -#: src/pages/admin/AccountList.tsx:106 -#, c-format -msgid "Name" -msgstr "" - -#: src/pages/admin/AccountList.tsx:110 -#, c-format -msgid "Balance" -msgstr "" - -#: src/pages/admin/AccountList.tsx:112 -#, c-format -msgid "Actions" -msgstr "" - -#: src/pages/admin/AccountList.tsx:151 -#, c-format -msgid "unknown" -msgstr "" - -#: src/pages/admin/AccountList.tsx:170 -#, c-format -msgid "change password" -msgstr "" - -#: src/pages/admin/AccountList.tsx:179 -#, c-format -msgid "cashouts" -msgstr "" - -#: src/pages/admin/AccountList.tsx:189 -#, c-format -msgid "remove" -msgstr "" - -#: src/pages/admin/AdminHome.tsx:168 -#, c-format -msgid "Cashout not implemented" -msgstr "" - -#: src/pages/admin/AdminHome.tsx:184 -#, c-format -msgid "Select a section" -msgstr "" - -#: src/pages/admin/AdminHome.tsx:202 -#, c-format -msgid "Last hour" -msgstr "" - -#: src/pages/admin/AdminHome.tsx:208 -#, c-format -msgid "Last day" -msgstr "" - -#: src/pages/admin/AdminHome.tsx:216 -#, c-format -msgid "Last month" -msgstr "" - -#: src/pages/admin/AdminHome.tsx:222 -#, c-format -msgid "Last year" -msgstr "" - -#: src/pages/admin/AdminHome.tsx:310 -#, c-format -msgid "Last Year" -msgstr "" - -#: src/pages/admin/AdminHome.tsx:325 -#, c-format -msgid "Trading volume on %1$s compared to %2$s" -msgstr "" - -#: src/pages/admin/AdminHome.tsx:342 -#, c-format -msgid "Cashin" -msgstr "" - -#: src/pages/admin/AdminHome.tsx:352 -#, c-format -msgid "Cashout" -msgstr "" - -#: src/pages/admin/AdminHome.tsx:364 -#, c-format -msgid "Payin" -msgstr "" - -#: src/pages/admin/AdminHome.tsx:374 -#, c-format -msgid "Payout" -msgstr "" - -#: src/pages/admin/AdminHome.tsx:388 -#, c-format -msgid "download stats as CSV" -msgstr "" - -#: src/pages/admin/AdminHome.tsx:494 -#, c-format -msgid "Decreased by" -msgstr "" - -#: src/pages/admin/AdminHome.tsx:498 -#, c-format -msgid "Increased by" -msgstr "" - -#: src/pages/DownloadStats.tsx:89 -#, c-format -msgid "Download bank stats" -msgstr "" - -#: src/pages/DownloadStats.tsx:110 -#, c-format -msgid "Include hour metric" -msgstr "" - -#: src/pages/DownloadStats.tsx:143 -#, c-format -msgid "Include day metric" -msgstr "" - -#: src/pages/DownloadStats.tsx:173 -#, c-format -msgid "Include month metric" -msgstr "" - -#: src/pages/DownloadStats.tsx:206 -#, c-format -msgid "Include year metric" -msgstr "" - -#: src/pages/DownloadStats.tsx:239 -#, c-format -msgid "Include table header" -msgstr "" - -#: src/pages/DownloadStats.tsx:272 -#, c-format -msgid "Add previous metric for compare" -msgstr "" - -#: src/pages/DownloadStats.tsx:307 -#, c-format -msgid "Fail on first error" -msgstr "" - -#: src/pages/DownloadStats.tsx:364 -#, c-format -msgid "Download" -msgstr "" - -#: src/pages/DownloadStats.tsx:381 -#, c-format -msgid "downloading... %1$s" -msgstr "" - -#: src/pages/DownloadStats.tsx:399 -#, c-format -msgid "Download completed" -msgstr "" - -#: src/pages/DownloadStats.tsx:400 -#, c-format -msgid "click here to save the file in your computer" -msgstr "" - -#: src/pages/PublicHistoriesPage.tsx:78 -#, c-format -msgid "History of public accounts" -msgstr "" - -#: src/pages/RegistrationPage.tsx:48 -#, c-format -msgid "Currently, the bank is not accepting new registrations!" -msgstr "" - -#: src/pages/RegistrationPage.tsx:87 -#, c-format -msgid "Missing name" -msgstr "" - -#: src/pages/RegistrationPage.tsx:91 -#, c-format -msgid "Use letters and numbers only, and start with a lowercase letter" -msgstr "" - -#: src/pages/RegistrationPage.tsx:107 -#, c-format -msgid "Passwords don't match" -msgstr "" - -#: src/pages/RegistrationPage.tsx:130 -#, c-format -msgid "Server replied with invalid phone or email." -msgstr "" - -#: src/pages/RegistrationPage.tsx:137 -#, c-format -msgid "Registration is disabled because the bank ran out of bonus credit." -msgstr "" - -#: src/pages/RegistrationPage.tsx:144 -#, c-format -msgid "No enough permission to create that account." -msgstr "" - -#: src/pages/RegistrationPage.tsx:151 -#, c-format -msgid "That account id is already taken." -msgstr "" - -#: src/pages/RegistrationPage.tsx:158 -#, c-format -msgid "That username is already taken." -msgstr "" - -#: src/pages/RegistrationPage.tsx:165 -#, c-format -msgid "That username can't be used because is reserved." -msgstr "" - -#: src/pages/RegistrationPage.tsx:172 -#, c-format -msgid "Only admin is allow to set debt limit." -msgstr "" - -#: src/pages/RegistrationPage.tsx:179 -#, c-format -msgid "No information for the selected authentication channel." -msgstr "" - -#: src/pages/RegistrationPage.tsx:186 -#, c-format -msgid "Authentication channel is not supported." -msgstr "" - -#: src/pages/RegistrationPage.tsx:193 -#, c-format -msgid "Only admin can create accounts with second factor authentication." -msgstr "" - -#: src/pages/RegistrationPage.tsx:233 -#, c-format -msgid "Account registration" -msgstr "" - -#: src/pages/RegistrationPage.tsx:315 -#, c-format -msgid "Repeat password" -msgstr "" - -#: src/pages/RegistrationPage.tsx:457 -#, c-format -msgid "Create a random temporary user" -msgstr "" - -#: src/pages/QrCodeSection.tsx:110 -#, c-format -msgid "If you have a Taler wallet installed in this device" -msgstr "" - -#: src/pages/QrCodeSection.tsx:116 -#, c-format -msgid "" -"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" -msgstr "" - -#: src/pages/QrCodeSection.tsx:143 -#, fuzzy, c-format -msgid "Withdraw" -msgstr "" - -#: src/pages/QrCodeSection.tsx:152 -#, c-format -msgid "Or if you have the wallet in another device" -msgstr "" - -#: src/pages/QrCodeSection.tsx:157 -#, fuzzy, c-format -msgid "Scan the QR below to start the withdrawal." -msgstr "" - -#: src/pages/WithdrawalQRCode.tsx:79 -#, c-format -msgid "Operation aborted" -msgstr "" - -#: src/pages/WithdrawalQRCode.tsx:82 -#, c-format -msgid "" -"The wire transfer to the Taler Exchange operator's account was aborted, your " -"balance was not affected." -msgstr "" - -#: src/pages/WithdrawalQRCode.tsx:88 -#, c-format -msgid "You can close this page now or continue to the account page." -msgstr "" - -#: src/pages/WithdrawalQRCode.tsx:147 -#, c-format -msgid "Done" -msgstr "" - -#: src/pages/WithdrawalQRCode.tsx:158 -#, c-format -msgid "Operation canceled" -msgstr "" - -#: src/pages/WithdrawalQRCode.tsx:173 -#, c-format -msgid "" -"The operation is marked as 'selected' but some step in the withdrawal failed" -msgstr "" - -#: src/pages/WithdrawalQRCode.tsx:175 -#, c-format -msgid "The account is selected but no withdrawal identification found." -msgstr "" - -#: src/pages/WithdrawalQRCode.tsx:188 -#, c-format -msgid "" -"There is a withdrawal identification but no account has been selected or the " -"selected account is invalid." -msgstr "" - -#: src/pages/WithdrawalQRCode.tsx:202 -#, c-format -msgid "" -"No withdrawal ID found and no account has been selected or the selected " -"account is invalid." -msgstr "" - -#: src/pages/WithdrawalQRCode.tsx:259 -#, c-format -msgid "Operation not found" -msgstr "" - -#: src/pages/WithdrawalQRCode.tsx:263 -#, c-format -msgid "" -"This operation is not known by the server. The operation id is wrong or the " -"server deleted the operation information before reaching here." -msgstr "" - -#: src/pages/WithdrawalQRCode.tsx:278 -#, c-format -msgid "Cotinue to dashboard" -msgstr "" - -#: src/pages/SolveChallengePage.tsx:98 -#, c-format -msgid "Cashout not found. It may be also mean that it was already aborted." -msgstr "" - -#: src/pages/SolveChallengePage.tsx:136 -#, c-format -msgid "Challenge not found." -msgstr "" - -#: src/pages/SolveChallengePage.tsx:143 -#, c-format -msgid "This user is not authorized to complete this challenge." -msgstr "" - -#: src/pages/SolveChallengePage.tsx:150 -#, c-format -msgid "Too many attempts, try another code." -msgstr "" - -#: src/pages/SolveChallengePage.tsx:157 -#, c-format -msgid "The confirmation code is wrong, try again." -msgstr "" - -#: src/pages/SolveChallengePage.tsx:164 -#, c-format -msgid "The operation expired." -msgstr "" - -#: src/pages/SolveChallengePage.tsx:197 -#, c-format -msgid "The operation failed." -msgstr "" - -#: src/pages/SolveChallengePage.tsx:212 -#, c-format -msgid "The operation needs another confirmation to complete." -msgstr "" - -#: src/pages/SolveChallengePage.tsx:224 -#, c-format -msgid "Account delete" -msgstr "" - -#: src/pages/SolveChallengePage.tsx:226 -#, c-format -msgid "Account update" -msgstr "" - -#: src/pages/SolveChallengePage.tsx:228 -#, c-format -msgid "Password update" -msgstr "" - -#: src/pages/SolveChallengePage.tsx:230 -#, c-format -msgid "Wire transfer" -msgstr "" - -#: src/pages/SolveChallengePage.tsx:232 -#, fuzzy, c-format -msgid "Withdrawal" -msgstr "" - -#: src/pages/SolveChallengePage.tsx:248 -#, fuzzy, c-format -msgid "Confirm the operation" -msgstr "" - -#: src/pages/SolveChallengePage.tsx:271 -#, c-format -msgid "Enter the confirmation code" -msgstr "" - -#: src/pages/SolveChallengePage.tsx:313 -#, c-format -msgid "Confirm" -msgstr "" - -#: src/pages/SolveChallengePage.tsx:348 -#, c-format -msgid "Send again" -msgstr "" - -#: src/pages/SolveChallengePage.tsx:359 -#, c-format -msgid "Send code" -msgstr "" - -#: src/pages/SolveChallengePage.tsx:369 -#, c-format -msgid "Operation details" -msgstr "" - -#: src/pages/SolveChallengePage.tsx:529 -#, c-format -msgid "Challenge details" -msgstr "" - -#: src/pages/SolveChallengePage.tsx:536 -#, c-format -msgid "Sent at" -msgstr "" - -#: src/pages/SolveChallengePage.tsx:551 -#, c-format -msgid "To phone" -msgstr "" - -#: src/pages/SolveChallengePage.tsx:553 -#, c-format -msgid "To email" -msgstr "" - -#: src/pages/WithdrawalOperationPage.tsx:49 -#, c-format -msgid "The Withdrawal URI is not valid" -msgstr "" - -#: src/components/Cashouts/views.tsx:100 -#, c-format -msgid "Latest cashouts" -msgstr "" - -#: src/components/Cashouts/views.tsx:111 -#, c-format -msgid "Created" -msgstr "" - -#: src/components/Cashouts/views.tsx:115 -#, c-format -msgid "Total debit" -msgstr "" - -#: src/components/Cashouts/views.tsx:119 -#, c-format -msgid "Total credit" -msgstr "" - -#: src/pages/ProfileNavigation.tsx:70 -#, c-format -msgid "Details" -msgstr "" - -#: src/pages/ProfileNavigation.tsx:74 -#, c-format -msgid "Delete" -msgstr "" - -#: src/pages/ProfileNavigation.tsx:78 -#, c-format -msgid "Credentials" -msgstr "" - -#: src/pages/ProfileNavigation.tsx:82 -#, c-format -msgid "Cashouts" -msgstr "" - -#: src/pages/business/CreateCashout.tsx:95 -#, c-format -msgid "Unable to create a cashout" -msgstr "" - -#: src/pages/business/CreateCashout.tsx:96 -#, c-format -msgid "The bank configuration does not support cashout operations." -msgstr "" - -#: src/pages/business/CreateCashout.tsx:223 -#, c-format -msgid "need to be higher due to fees" -msgstr "" - -#: src/pages/business/CreateCashout.tsx:225 -#, c-format -msgid "the total transfer at destination will be zero" -msgstr "" - -#: src/pages/business/CreateCashout.tsx:250 -#, c-format -msgid "Cashout created" -msgstr "" - -#: src/pages/business/CreateCashout.tsx:272 -#, c-format -msgid "" -"Duplicated request detected, check if the operation succeeded or try again." -msgstr "" - -#: src/pages/business/CreateCashout.tsx:279 -#, c-format -msgid "The conversion rate was incorrectly applied" -msgstr "" - -#: src/pages/business/CreateCashout.tsx:286 -#, c-format -msgid "The account does not have sufficient funds" -msgstr "" - -#: src/pages/business/CreateCashout.tsx:293 -#, c-format -msgid "Cashouts are not supported" -msgstr "" - -#: src/pages/business/CreateCashout.tsx:300 -#, c-format -msgid "Missing cashout URI in the profile" -msgstr "" - -#: src/pages/business/CreateCashout.tsx:307 -#, c-format -msgid "" -"Sending the confirmation message failed, retry later or contact the " -"administrator." -msgstr "" - -#: src/pages/business/CreateCashout.tsx:339 -#, c-format -msgid "Conversion rate" -msgstr "" - -#: src/pages/business/CreateCashout.tsx:360 -#, c-format -msgid "Fee" -msgstr "" - -#: src/pages/business/CreateCashout.tsx:374 -#, c-format -msgid "To account" -msgstr "" - -#: src/pages/business/CreateCashout.tsx:381 -#, c-format -msgid "No cashout account" -msgstr "" - -#: src/pages/business/CreateCashout.tsx:382 -#, c-format -msgid "Before doing a cashout you need to complete your profile" -msgstr "" - -#: src/pages/business/CreateCashout.tsx:440 -#, fuzzy, c-format -msgid "Amount to send" -msgstr "" - -#: src/pages/business/CreateCashout.tsx:441 -#, fuzzy, c-format -msgid "Amount to receive" -msgstr "" - -#: src/pages/business/CreateCashout.tsx:490 -#, c-format -msgid "Total cost" -msgstr "" - -#: src/pages/business/CreateCashout.tsx:505 -#, c-format -msgid "Balance left" -msgstr "" - -#: src/pages/business/CreateCashout.tsx:520 -#, c-format -msgid "Before fee" -msgstr "" - -#: src/pages/business/CreateCashout.tsx:533 -#, c-format -msgid "Total cashout transfer" -msgstr "" - -#: src/pages/business/CreateCashout.tsx:553 -#, c-format -msgid "No cashout channel available" -msgstr "" - -#: src/pages/business/CreateCashout.tsx:555 -#, c-format -msgid "" -"Before doing a cashout the server need to provide an second channel to " -"confirm the operation" -msgstr "" - -#: src/pages/business/CreateCashout.tsx:567 -#, c-format -msgid "Second factor authentication" -msgstr "" - -#: src/pages/business/CreateCashout.tsx:598 -#, c-format -msgid "Email" -msgstr "" - -#: src/pages/business/CreateCashout.tsx:600 -#, c-format -msgid "add a email in your profile to enable this option" -msgstr "" - -#: src/pages/business/CreateCashout.tsx:646 -#, c-format -msgid "SMS" -msgstr "" - -#: src/pages/business/CreateCashout.tsx:648 -#, c-format -msgid "add a phone number in your profile to enable this option" -msgstr "" - -#: src/pages/account/CashoutListForAccount.tsx:52 -#, c-format -msgid "Cashout for account %1$s" -msgstr "" - -#: src/pages/admin/AccountForm.tsx:165 -#, c-format -msgid "it doesn't have the pattern of an IBAN number" -msgstr "" - -#: src/pages/admin/AccountForm.tsx:185 -#, c-format -msgid "it doesn't have the pattern of an email" -msgstr "" - -#: src/pages/admin/AccountForm.tsx:190 -#, c-format -msgid "should start with +" -msgstr "" - -#: src/pages/admin/AccountForm.tsx:192 -#, c-format -msgid "phone number can't have other than numbers" -msgstr "" - -#: src/pages/admin/AccountForm.tsx:329 -#, c-format -msgid "account identification in the bank" -msgstr "" - -#: src/pages/admin/AccountForm.tsx:365 -#, c-format -msgid "name of the person owner the account" -msgstr "" - -#: src/pages/admin/AccountForm.tsx:374 -#, c-format -msgid "Internal IBAN" -msgstr "" - -#: src/pages/admin/AccountForm.tsx:377 -#, c-format -msgid "if empty a random account number will be assigned" -msgstr "" - -#: src/pages/admin/AccountForm.tsx:378 -#, c-format -msgid "account identification for bank transfer" -msgstr "" - -#: src/pages/admin/AccountForm.tsx:423 -#, c-format -msgid "Phone" -msgstr "" - -#: src/pages/admin/AccountForm.tsx:451 -#, c-format -msgid "Cashout IBAN" -msgstr "" - -#: src/pages/admin/AccountForm.tsx:452 -#, c-format -msgid "account number where the money is going to be sent when doing cashouts" -msgstr "" - -#: src/pages/admin/AccountForm.tsx:470 -#, c-format -msgid "Max debt" -msgstr "" - -#: src/pages/admin/AccountForm.tsx:494 -#, c-format -msgid "how much is user able to transfer after zero balance" -msgstr "" - -#: src/pages/admin/AccountForm.tsx:508 -#, c-format -msgid "Is this a Taler Exchange?" -msgstr "" - -#: src/pages/admin/AccountForm.tsx:549 -#, c-format -msgid "This server doesn't support second factor authentication." -msgstr "" - -#: src/pages/admin/AccountForm.tsx:560 -#, c-format -msgid "Enable second factor authentication" -msgstr "" - -#: src/pages/admin/AccountForm.tsx:596 -#, c-format -msgid "Using email" -msgstr "" - -#: src/pages/admin/AccountForm.tsx:654 -#, c-format -msgid "Using SMS" -msgstr "" - -#: src/pages/admin/AccountForm.tsx:691 -#, c-format -msgid "Is this account public?" -msgstr "" - -#: src/pages/admin/AccountForm.tsx:719 -#, c-format -msgid "public accounts have their balance publicly accessible" -msgstr "" - -#: src/pages/account/ShowAccountDetails.tsx:100 -#, c-format -msgid "Account updated" -msgstr "" - -#: src/pages/account/ShowAccountDetails.tsx:107 -#, c-format -msgid "The rights to change the account are not sufficient" -msgstr "" - -#: src/pages/account/ShowAccountDetails.tsx:114 -#, c-format -msgid "The username was not found" -msgstr "" - -#: src/pages/account/ShowAccountDetails.tsx:121 -#, c-format -msgid "" -"You can't change the legal name, please contact the your account " -"administrator." -msgstr "" - -#: src/pages/account/ShowAccountDetails.tsx:128 -#, c-format -msgid "" -"You can't change the debt limit, please contact the your account " -"administrator." -msgstr "" - -#: src/pages/account/ShowAccountDetails.tsx:135 -#, c-format -msgid "" -"You can't change the cashout address, please contact the your account " -"administrator." -msgstr "" - -#: src/pages/account/ShowAccountDetails.tsx:177 -#, c-format -msgid "Account \"%1$s\"" -msgstr "" - -#: src/pages/account/ShowAccountDetails.tsx:190 -#, c-format -msgid "Change details" -msgstr "" - -#: src/pages/account/ShowAccountDetails.tsx:235 -#, c-format -msgid "Update" -msgstr "" - -#: src/pages/account/UpdateAccountPassword.tsx:78 -#, c-format -msgid "password doesn't match" -msgstr "" - -#: src/pages/account/UpdateAccountPassword.tsx:95 -#, c-format -msgid "Password changed" -msgstr "" - -#: src/pages/account/UpdateAccountPassword.tsx:102 -#, c-format -msgid "Not authorized to change the password, maybe the session is invalid." -msgstr "" - -#: src/pages/account/UpdateAccountPassword.tsx:112 -#, c-format -msgid "" -"You need to provide the old password. If you don't have it contact your " -"account administrator." -msgstr "" - -#: src/pages/account/UpdateAccountPassword.tsx:117 -#, c-format -msgid "Your current password doesn't match, can't change to a new password." -msgstr "" - -#: src/pages/account/UpdateAccountPassword.tsx:149 -#, c-format -msgid "Update password" -msgstr "" - -#: src/pages/account/UpdateAccountPassword.tsx:167 -#, c-format -msgid "New password" -msgstr "" - -#: src/pages/account/UpdateAccountPassword.tsx:195 -#, c-format -msgid "Type it again" -msgstr "" - -#: src/pages/account/UpdateAccountPassword.tsx:217 -#, c-format -msgid "repeat the same password" -msgstr "" - -#: src/pages/account/UpdateAccountPassword.tsx:227 -#, c-format -msgid "Current password" -msgstr "" - -#: src/pages/account/UpdateAccountPassword.tsx:248 -#, c-format -msgid "your current password, for security" -msgstr "" - -#: src/pages/account/UpdateAccountPassword.tsx:272 -#, c-format -msgid "Change" -msgstr "" - -#: src/pages/admin/CreateNewAccount.tsx:74 -#, c-format -msgid "" -"Account created with password \"%1$s\". The user must change the password on " -"the next login." -msgstr "" - -#: src/pages/admin/CreateNewAccount.tsx:83 -#, c-format -msgid "Server replied that phone or email is invalid" -msgstr "" - -#: src/pages/admin/CreateNewAccount.tsx:90 -#, c-format -msgid "The rights to perform the operation are not sufficient" -msgstr "" - -#: src/pages/admin/CreateNewAccount.tsx:97 -#, c-format -msgid "Account username is already taken" -msgstr "" - -#: src/pages/admin/CreateNewAccount.tsx:104 -#, c-format -msgid "Account id is already taken" -msgstr "" - -#: src/pages/admin/CreateNewAccount.tsx:111 -#, c-format -msgid "Bank ran out of bonus credit." -msgstr "" - -#: src/pages/admin/CreateNewAccount.tsx:118 -#, c-format -msgid "Account username can't be used because is reserved" -msgstr "" - -#: src/pages/admin/CreateNewAccount.tsx:160 -#, c-format -msgid "Can't create accounts" -msgstr "" - -#: src/pages/admin/CreateNewAccount.tsx:161 -#, c-format -msgid "Only system admin can create accounts." -msgstr "" - -#: src/pages/admin/CreateNewAccount.tsx:183 -#, c-format -msgid "New business account" -msgstr "" - -#: src/pages/admin/CreateNewAccount.tsx:209 -#, c-format -msgid "Create" -msgstr "" - -#: src/pages/admin/RemoveAccount.tsx:94 -#, c-format -msgid "Can't delete the account" -msgstr "" - -#: src/pages/admin/RemoveAccount.tsx:95 -#, c-format -msgid "" -"The account can't be delete while still holding some balance. First make " -"sure that the owner make a complete cashout." -msgstr "" - -#: src/pages/admin/RemoveAccount.tsx:117 -#, c-format -msgid "Account removed" -msgstr "" - -#: src/pages/admin/RemoveAccount.tsx:124 -#, c-format -msgid "No enough permission to delete the account." -msgstr "" - -#: src/pages/admin/RemoveAccount.tsx:131 -#, c-format -msgid "The username was not found." -msgstr "" - -#: src/pages/admin/RemoveAccount.tsx:138 -#, c-format -msgid "Can't delete a reserved username." -msgstr "" - -#: src/pages/admin/RemoveAccount.tsx:145 -#, c-format -msgid "Can't delete an account with balance different than zero." -msgstr "" - -#: src/pages/admin/RemoveAccount.tsx:170 -#, c-format -msgid "name doesn't match" -msgstr "" - -#: src/pages/admin/RemoveAccount.tsx:180 -#, c-format -msgid "You are going to remove the account" -msgstr "" - -#: src/pages/admin/RemoveAccount.tsx:182 -#, c-format -msgid "This step can't be undone." -msgstr "" - -#: src/pages/admin/RemoveAccount.tsx:188 -#, c-format -msgid "Deleting account \"%1$s\"" -msgstr "" - -#: src/pages/admin/RemoveAccount.tsx:206 -#, c-format -msgid "Verification" -msgstr "" - -#: src/pages/admin/RemoveAccount.tsx:231 -#, c-format -msgid "enter the account name that is going to be deleted" -msgstr "" - -#: src/pages/business/ShowCashoutDetails.tsx:49 -#, c-format -msgid "cashout id should be a number" -msgstr "" - -#: src/pages/business/ShowCashoutDetails.tsx:65 -#, c-format -msgid "This cashout not found. Maybe already aborted." -msgstr "" - -#: src/pages/business/ShowCashoutDetails.tsx:106 -#, c-format -msgid "Cashout detail" -msgstr "" - -#: src/pages/business/ShowCashoutDetails.tsx:139 -#, c-format -msgid "Debited" -msgstr "" - -#: src/pages/business/ShowCashoutDetails.tsx:154 -#, c-format -msgid "Credited" -msgstr "" - -#: src/Routing.tsx:140 -#, c-format -msgid "Welcome to %1$s!" -msgstr "" - -#, c-format -#~ msgid "days" -#~ msgstr "" - -#, c-format -#~ msgid "hours" -#~ msgstr "" - -#, c-format -#~ msgid "minutes" -#~ msgstr "" - -#, c-format -#~ msgid "seconds" -#~ msgstr "" - -#~ msgid "Go back" -#~ msgstr "" - -#, fuzzy -#~ msgid "Withdraw Money into a Taler wallet" -#~ msgstr "" - -#~ msgid "Page has a problem: logged in but backend state is lost." -#~ msgstr "" - -#, fuzzy -#~ msgid "Welcome to the euFin bank!" -#~ msgstr "" - -#~ msgid "Page has a problem:" -#~ msgstr "" - -#~ msgid "Sign in" -#~ msgstr "" diff --git a/packages/bank-ui/src/i18n/es.po b/packages/bank-ui/src/i18n/es.po @@ -271,11 +271,13 @@ msgstr "payto://iban/[iban-destinatario]?message=[asunto]&amount=[%1$s:X.Y]" #: src/pages/PaytoWireTransferForm.tsx:457 #, c-format +msgctxt "wire_transfer" msgid "Cancel" msgstr "Cancelar" #: src/pages/PaytoWireTransferForm.tsx:471 #, c-format +msgctxt "wire_transfer" msgid "Send" msgstr "Envíar" diff --git a/packages/bank-ui/src/i18n/ru.po b/packages/bank-ui/src/i18n/ru.po @@ -0,0 +1,1794 @@ +# This file is part of GNU Taler +# (C) 2022-2024 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/> +# +msgid "" +msgstr "" +"Project-Id-Version: Taler Bank\n" +"Report-Msgid-Bugs-To: taler@gnu.org\n" +"PO-Revision-Date: 2024-05-10 00:13+0000\n" +"Last-Translator: Lily Ponomareva <lilyponomareva2017@gmail.com>\n" +"Language-Team: Russian <https://weblate.taler.net/projects/gnu-taler/" +"taler-bank-spa/ru/>\n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"X-Generator: Weblate 5.4.3\n" + +#: src/utils.ts:137 +#, c-format +msgid "Operation failed, please report" +msgstr "Не удалось выполнить операцию, сообщите об этом" + +#: src/utils.ts:156 +#, c-format +msgid "Request timeout" +msgstr "Тайм-аут запроса" + +#: src/utils.ts:165 +#, c-format +msgid "Request throttled" +msgstr "Запрос замедлен" + +#: src/utils.ts:174 +#, c-format +msgid "Malformed response" +msgstr "Неправильный ответ" + +#: src/utils.ts:183 +#, c-format +msgid "Network error" +msgstr "Ошибка сети" + +#: src/utils.ts:192 +#, c-format +msgid "Unexpected request error" +msgstr "Неожиданная ошибка запроса" + +#: src/utils.ts:201 +#, c-format +msgid "Unexpected error" +msgstr "Непредвиденная ошибка" + +#: src/utils.ts:377 +#, c-format +msgid "IBAN numbers usually have more that 4 digits" +msgstr "Номера IBAN обычно содержат более 4 цифр" + +#: src/utils.ts:379 +#, c-format +msgid "IBAN numbers usually have less that 34 digits" +msgstr "Номера IBAN обычно содержат менее 34 цифр" + +#: src/utils.ts:387 +#, c-format +msgid "IBAN country code not found" +msgstr "Код страны IBAN не найден" + +#: src/utils.ts:401 +#, c-format +msgid "IBAN number is not valid, checksum is wrong" +msgstr "Номер IBAN недействителен, контрольная сумма неверна" + +#: src/context/config.ts:136 +#, c-format +msgid "" +"the bank backend is not supported. supported version \"%1$s\", server version " +"\"%2$s\"" +msgstr "" +"Бэкенд банка не поддерживается. Поддерживаемая версия \"%1$s\" а версия " +"сервера \"%2$s\"" + +#: src/hooks/preferences.ts:55 +#, c-format +msgid "Max withdrawal amount" +msgstr "Максимальная сумма вывода" + +#: src/hooks/preferences.ts:57 +#, c-format +msgid "Show withdrawal confirmation" +msgstr "Показать подтверждение вывода средств" + +#: src/hooks/preferences.ts:59 +#, c-format +msgid "Show demo description" +msgstr "Показать описание демо" + +#: src/hooks/preferences.ts:61 +#, c-format +msgid "Show install wallet first" +msgstr "Сначала показать как установить кошелёк" + +#: src/hooks/preferences.ts:63 +#, c-format +msgid "Use fast withdrawal form" +msgstr "Используйте форму быстрого вывода средств" + +#: src/hooks/preferences.ts:65 +#, c-format +msgid "Show debug info" +msgstr "Показать информацию для отладки" + +#: src/pages/PaytoWireTransferForm.tsx:90 +#, c-format +msgid "required" +msgstr "обязательно" + +#: src/pages/PaytoWireTransferForm.tsx:92 +#, c-format +msgid "IBAN should have just uppercased letters and numbers" +msgstr "IBAN должен состоять только из прописных букв и цифр" + +#: src/pages/PaytoWireTransferForm.tsx:98 +#, c-format +msgid "not valid" +msgstr "недопустимый" + +#: src/pages/PaytoWireTransferForm.tsx:100 +#, c-format +msgid "should be greater than 0" +msgstr "должно быть больше 0" + +#: src/pages/PaytoWireTransferForm.tsx:102 +#, c-format +msgid "balance is not enough" +msgstr "Недостаточно средств на балансе" + +#: src/pages/PaytoWireTransferForm.tsx:112 +#, c-format +msgid "does not follow the pattern" +msgstr "не следует шаблону" + +#: src/pages/PaytoWireTransferForm.tsx:114 +#, c-format +msgid "only \"IBAN\" target are supported" +msgstr "поддерживаются только \"IBAN\"" + +#: src/pages/PaytoWireTransferForm.tsx:116 +#, c-format +msgid "use the \"amount\" parameter to specify the amount to be transferred" +msgstr "Используйте параметр \"Сумма\" для указания суммы перевода" + +#: src/pages/PaytoWireTransferForm.tsx:118 +#, c-format +msgid "the amount is not valid" +msgstr "сумма не является действительной" + +#: src/pages/PaytoWireTransferForm.tsx:120 +#, c-format +msgid "use the \"message\" parameter to specify a reference text for the transfer" +msgstr "используйте параметр \"message\" для текста причины перевода" + +#: src/pages/PaytoWireTransferForm.tsx:160 +#, c-format +msgid "The request was invalid or the payto://-URI used unacceptable features." +msgstr "" +"Запрос был неверным или payto://-URI использовал недопустимую " +"функциональность." + +#: src/pages/PaytoWireTransferForm.tsx:167 +#, c-format +msgid "Not enough permission to complete the operation." +msgstr "Не хватает разрешения для завершения операции." + +#: src/pages/PaytoWireTransferForm.tsx:174 +#, c-format +msgid "The destination account \"%1$s\" was not found." +msgstr "Целевой счет \"%1$s\" не найден." + +#: src/pages/PaytoWireTransferForm.tsx:181 +#, c-format +msgid "The origin and the destination of the transfer can't be the same." +msgstr "Пункт отправления и пункт назначения перевода не могут совпадать." + +#: src/pages/PaytoWireTransferForm.tsx:188 +#, c-format +msgid "Your balance is not enough." +msgstr "Вашего баланса недостаточно." + +#: src/pages/PaytoWireTransferForm.tsx:195 +#, c-format +msgid "The origin account \"%1$s\" was not found." +msgstr "Исходный аккаунт \"%1$s\" не найден." + +#: src/pages/PaytoWireTransferForm.tsx:212 +#, c-format +msgid "Wire transfer created!" +msgstr "Банковский перевод создан!" + +#: src/pages/PaytoWireTransferForm.tsx:270 +#, c-format +msgid "Using a form" +msgstr "Используя форму" + +#: src/pages/PaytoWireTransferForm.tsx:310 +#, c-format +msgid "Import payto:// URI" +msgstr "Импорт payto:// URI" + +#: src/pages/PaytoWireTransferForm.tsx:335 +#, c-format +msgid "Recipient" +msgstr "Получатель" + +#: src/pages/PaytoWireTransferForm.tsx:359 +#, c-format +msgid "IBAN of the recipient's account" +msgstr "IBAN счета получателя" + +#: src/pages/PaytoWireTransferForm.tsx:369 +#, c-format +msgid "Transfer subject" +msgstr "Причина перевода" + +#: src/pages/PaytoWireTransferForm.tsx:377 +#, c-format +msgid "subject" +msgstr "причина" + +#: src/pages/PaytoWireTransferForm.tsx:390 +#, c-format +msgid "some text to identify the transfer" +msgstr "какой-то текст для идентификации перевода" + +#: src/pages/PaytoWireTransferForm.tsx:400 +#, c-format +msgid "Amount" +msgstr "Сумма" + +#: src/pages/PaytoWireTransferForm.tsx:415 +#, c-format +msgid "amount to transfer" +msgstr "сумма для перевода" + +#: src/pages/PaytoWireTransferForm.tsx:425 +#, c-format +msgid "payto URI:" +msgstr "payto URI:" + +#: src/pages/PaytoWireTransferForm.tsx:436 +#, c-format +msgid "uniform resource identifier of the target account" +msgstr "унифицированный идентификатор ресурса целевой учетной записи" + +#: src/pages/PaytoWireTransferForm.tsx:437 +#, c-format +msgid "payto://iban/[receiver-iban]?message=[subject]&amount=[%1$s:X.Y]" +msgstr "" +"payto://iban/[iban_получателя]?message=[причина_платежа]&amount=[%1$s:X.Y]" + +#: src/pages/PaytoWireTransferForm.tsx:457 +#, c-format +msgctxt "wire_transfer" +msgid "Cancel" +msgstr "Отмена" + +#: src/pages/PaytoWireTransferForm.tsx:471 +#, c-format +msgctxt "wire_transfer" +msgid "Send" +msgstr "Отправить" + +#: src/pages/LoginForm.tsx:71 +#, c-format +msgid "Missing username" +msgstr "Отсутствует имя пользователя" + +#: src/pages/LoginForm.tsx:75 +#, c-format +msgid "Missing password" +msgstr "Отсутствует пароль" + +#: src/pages/LoginForm.tsx:104 +#, c-format +msgid "Wrong credentials for \"%1$s\"" +msgstr "Неверные учетные данные для «%1$s» " + +#: src/pages/LoginForm.tsx:111 +#, c-format +msgid "Account not found" +msgstr "Учётная запись не найдена" + +#: src/pages/LoginForm.tsx:142 +#, c-format +msgid "Username" +msgstr "Имя пользователя" + +#: src/pages/LoginForm.tsx:156 +#, c-format +msgid "username of the account" +msgstr "имя пользователя счёта" + +#: src/pages/LoginForm.tsx:175 +#, c-format +msgid "Password" +msgstr "Пароль" + +#: src/pages/LoginForm.tsx:188 +#, c-format +msgid "password of the account" +msgstr "пароль от счёта" + +#: src/pages/LoginForm.tsx:223 +#, c-format +msgid "Check" +msgstr "Проверить" + +#: src/pages/LoginForm.tsx:237 +#, c-format +msgid "Log in" +msgstr "Войти" + +#: src/pages/LoginForm.tsx:249 +#, c-format +msgid "Register" +msgstr "Регистрация" + +#: src/components/Transactions/views.tsx:52 +#, c-format +msgid "Latest transactions" +msgstr "Последние транзакции" + +#: src/components/Transactions/views.tsx:63 +#, c-format +msgid "Date" +msgstr "Дата" + +#: src/components/Transactions/views.tsx:71 +#, c-format +msgid "Counterpart" +msgstr "Контрагент" + +#: src/components/Transactions/views.tsx:75 +#, c-format +msgid "Subject" +msgstr "Причина" + +#: src/components/Transactions/views.tsx:111 +#, c-format +msgid "sent" +msgstr "отправлено" + +#: src/components/Transactions/views.tsx:112 +#, c-format +msgid "received" +msgstr "получено" + +#: src/components/Transactions/views.tsx:127 +#, c-format +msgid "invalid value" +msgstr "Недопустимое значение" + +#: src/components/Transactions/views.tsx:136 +#, c-format +msgid "to" +msgstr "к" + +#: src/components/Transactions/views.tsx:136 +#, c-format +msgid "from" +msgstr "от" + +#: src/components/Transactions/views.tsx:202 +#, c-format +msgid "First page" +msgstr "Первая страница" + +#: src/components/Transactions/views.tsx:209 +#, c-format +msgid "Next" +msgstr "Далее" + +#: src/pages/WithdrawalConfirmationQuestion.tsx:86 +#, c-format +msgid "Wire transfer completed!" +msgstr "Отправка перевода завершена!" + +#: src/pages/WithdrawalConfirmationQuestion.tsx:93 +#, c-format +msgid "The withdrawal has been aborted previously and can't be confirmed" +msgstr "Вывод средств был прерван ранее и не может быть подтвержден" + +#: src/pages/WithdrawalConfirmationQuestion.tsx:100 +#, c-format +msgid "" +"The withdrawal operation can't be confirmed before a wallet accepted the " +"transaction." +msgstr "" +"Операция по выводу средств не может быть подтверждена до того как кошёлек " +"примет транзакцию." + +#: src/pages/WithdrawalConfirmationQuestion.tsx:107 +#, c-format +msgid "The operation id is invalid." +msgstr "Идентификатор операции недействителен." + +#: src/pages/WithdrawalConfirmationQuestion.tsx:114 +#, c-format +msgid "The operation was not found." +msgstr "Операция не найдена." + +#: src/pages/WithdrawalConfirmationQuestion.tsx:121 +#, c-format +msgid "Your balance is not enough for the operation." +msgstr "Вашего баланса недостаточно для проведения операции." + +#: src/pages/WithdrawalConfirmationQuestion.tsx:155 +#, c-format +msgid "The reserve operation has been confirmed previously and can't be aborted" +msgstr "Резервная операция была подтверждена ранее и не может быть прервана" + +#: src/pages/WithdrawalConfirmationQuestion.tsx:186 +#, c-format +msgid "Confirm the withdrawal operation" +msgstr "Подтвердите операцию вывода" + +#: src/pages/WithdrawalConfirmationQuestion.tsx:203 +#, c-format +msgid "Wire transfer details" +msgstr "Детали банковского перевода" + +#: src/pages/WithdrawalConfirmationQuestion.tsx:217 +#, c-format +msgid "Taler Exchange operator's account" +msgstr "Счет оператора Обменника Taler" + +#: src/pages/WithdrawalConfirmationQuestion.tsx:228 +#, c-format +msgid "Taler Exchange operator's name" +msgstr "Название оператора Обменника Taler" + +#: src/pages/WithdrawalConfirmationQuestion.tsx:317 +#, c-format +msgid "Transfer" +msgstr "Перевести" + +#: src/pages/WithdrawalConfirmationQuestion.tsx:342 +#, c-format +msgid "Authentication required" +msgstr "Требуется аутентификация" + +#: src/pages/WithdrawalConfirmationQuestion.tsx:352 +#, c-format +msgid "This operation was created with other username" +msgstr "Эта операция была создана с другим именем пользователя" + +#: src/pages/OperationState/views.tsx:209 +#, c-format +msgid "" +"Unauthorized to make the operation, maybe the session has expired or the " +"password changed." +msgstr "" +"Неавторизированное выполнение операции, возможно истек сеанс или изменён " +"пароль." + +#: src/pages/OperationState/views.tsx:218 +#, c-format +msgid "The operation was rejected due to insufficient funds." +msgstr "Операция отклонена из-за нехватки средств." + +#: src/pages/OperationState/views.tsx:268 +#, c-format +msgid "Withdrawal confirmed" +msgstr "Вывод подтверждён" + +#: src/pages/OperationState/views.tsx:272 +#, c-format +msgid "" +"The wire transfer to the Taler operator has been initiated. You will soon " +"receive the requested amount in your Taler wallet." +msgstr "" +"Инициирован банковский перевод оператору Taler. Вскоре вы получите " +"запрошенную сумму на свой кошелёк Taler." + +#: src/pages/OperationState/views.tsx:287 +#, c-format +msgid "Do not show this again" +msgstr "Не показывать снова" + +#: src/pages/OperationState/views.tsx:319 +#, c-format +msgid "Close" +msgstr "Закрыть" + +#: src/pages/OperationState/views.tsx:399 +#, c-format +msgid "On this device" +msgstr "На этом устройстве" + +#: src/pages/OperationState/views.tsx:404 +#, c-format +msgid "" +"If you are using a web browser on desktop you should access your wallet with the " +"GNU Taler WebExtension now or click the link if your WebExtension have the " +"\"Inject Taler support\" option enabled." +msgstr "" +"Если вы используете веб-браузер на рабочем столе, вы можете получить доступ " +"к своему кошельку с помощью расширения браузера GNU Taler прямо сейчас или " +"нажать на ссылку если в вашем расширении браузера включена опция «Встронная " +"поддержка Taler»." + +#: src/pages/OperationState/views.tsx:417 +#, c-format +msgid "Start" +msgstr "Старт" + +#: src/pages/OperationState/views.tsx:426 +#, c-format +msgid "On a mobile phone" +msgstr "На мобильном телефоне" + +#: src/pages/OperationState/views.tsx:431 +#, c-format +msgid "Scan the QR code with your mobile device." +msgstr "Отсканируйте QR-код с помощью мобильного устройства." + +#: src/pages/WalletWithdrawForm.tsx:73 +#, c-format +msgid "There is an operation already" +msgstr "Операция уже идет" + +#: src/pages/WalletWithdrawForm.tsx:75 +#, c-format +msgid "Complete or cancel the operation in" +msgstr "Завершите или отмените операцию в" + +#: src/pages/WalletWithdrawForm.tsx:84 +#, c-format +msgid "this page" +msgstr "этой странице" + +#: src/pages/WalletWithdrawForm.tsx:101 +#, c-format +msgid "invalid" +msgstr "недействительно" + +#: src/pages/WalletWithdrawForm.tsx:116 +#, c-format +msgid "Server responded with an invalid withdraw URI" +msgstr "Сервер ответил с недопустимым URI вывода" + +#: src/pages/WalletWithdrawForm.tsx:117 +#, c-format +msgid "Withdraw URI: %1$s" +msgstr "URI вывода: %1$s" + +#: src/pages/WalletWithdrawForm.tsx:132 +#, c-format +msgid "The operation was rejected due to insufficient funds" +msgstr "Операция отклонена из-за нехватки средств." + +#: src/pages/WalletWithdrawForm.tsx:253 +#, c-format +msgid "Continue" +msgstr "Продолжить" + +#: src/pages/WalletWithdrawForm.tsx:282 +#, c-format +msgid "Prepare your wallet" +msgstr "Подготовьте свой кошелёк" + +#: src/pages/WalletWithdrawForm.tsx:285 +#, c-format +msgid "" +"After using your wallet you will need to confirm or cancel the operation on this " +"site." +msgstr "" +"После использования кошелька вам нужно будет подтвердить или отменить " +"операцию на этом сайте." + +#: src/pages/WalletWithdrawForm.tsx:295 +#, c-format +msgid "You need a GNU Taler Wallet" +msgstr "Вам нужен кошелёк Taler" + +#: src/pages/WalletWithdrawForm.tsx:300 +#, c-format +msgid "If you don't have one yet you can follow the instruction in" +msgstr "Если у вас его еще нет, вы можете следовать инструкциям на" + +#: src/pages/PaymentOptions.tsx:55 +#, c-format +msgid "Send money" +msgstr "Отправить деньги" + +#: src/pages/PaymentOptions.tsx:73 +#, c-format +msgid "to a %1$s wallet" +msgstr "на кошелёк %1$s" + +#: src/pages/PaymentOptions.tsx:95 +#, c-format +msgid "Withdraw digital money into your mobile wallet or browser extension" +msgstr "" +"Выводите цифровые деньги на свой мобильный кошелёк или расширение для " +"браузера" + +#: src/pages/PaymentOptions.tsx:109 +#, c-format +msgid "operation ready" +msgstr "операция готова" + +#: src/pages/PaymentOptions.tsx:129 +#, c-format +msgid "to another bank account" +msgstr "на другой банковский счет" + +#: src/pages/PaymentOptions.tsx:149 +#, c-format +msgid "Make a wire transfer to an account with known bank account number." +msgstr "" +"Сделайте банковский перевод на счет с известным номером банковского счета." + +#: src/pages/PaymentOptions.tsx:171 +#, c-format +msgid "Transfer details" +msgstr "Подробности перевода" + +#: src/pages/AccountPage/views.tsx:41 +#, c-format +msgid "This is a demo bank" +msgstr "Это демо-банк" + +#: src/pages/AccountPage/views.tsx:46 +#, c-format +msgid "" +"This part of the demo shows how a bank that supports Taler directly would work. " +"In addition to using your own bank account, you can also see the transaction " +"history of some %1$s." +msgstr "" +"В этой части демонстрации показано как будет работать банк поддерживающий " +"Taler напрямую. Помимо использования собственного банковского счёта, вы " +"также можете просмотреть историю транзакций некоторых %1$s." + +#: src/pages/AccountPage/views.tsx:53 +#, c-format +msgid "This part of the demo shows how a bank that supports Taler directly would work." +msgstr "" +"В этой части демонстрации показано как будет работать банк поддерживающий " +"Taler напрямую." + +#: src/pages/AccountPage/views.tsx:70 +#, c-format +msgid "Pending account delete operation" +msgstr "Ожидание операции удаления счёта" + +#: src/pages/AccountPage/views.tsx:72 +#, c-format +msgid "Pending account update operation" +msgstr "Ожидание операции обновления счёта" + +#: src/pages/AccountPage/views.tsx:74 +#, c-format +msgid "Pending password update operation" +msgstr "Ожидание операции обновления пароля" + +#: src/pages/AccountPage/views.tsx:76 +#, c-format +msgid "Pending transaction operation" +msgstr "Ожидание операции транзакции" + +#: src/pages/AccountPage/views.tsx:78 +#, c-format +msgid "Pending withdrawal operation" +msgstr "Ожидание операции вывода средств" + +#: src/pages/AccountPage/views.tsx:80 +#, c-format +msgid "Pending cashout operation" +msgstr "Ожидание операции обналички" + +#: src/pages/AccountPage/views.tsx:91 +#, c-format +msgid "You can complete or cancel the operation in" +msgstr "Завершить или отменить операцию можно в" + +#: src/pages/BankFrame.tsx:64 +#, c-format +msgid "Internal error, please report." +msgstr "Внутренняя ошибка, пожалуйста, сообщите." + +#: src/pages/BankFrame.tsx:100 +#, c-format +msgid "Preferences" +msgstr "Настройки" + +#: src/pages/BankFrame.tsx:184 +#, c-format +msgid "Welcome, %1$s" +msgstr "Добро пожаловать, %1$s" + +#: src/pages/WireTransfer.tsx:79 +#, c-format +msgid "Make a wire transfer" +msgstr "Сделать банковский перевод" + +#: src/pages/admin/AccountList.tsx:72 +#, c-format +msgid "Accounts" +msgstr "Счета" + +#: src/pages/admin/AccountList.tsx:75 +#, c-format +msgid "A list of all business account in the bank." +msgstr "Список всех бизнес-счетов в банке." + +#: src/pages/admin/AccountList.tsx:86 +#, c-format +msgid "Create account" +msgstr "Создать учётную запись" + +#: src/pages/admin/AccountList.tsx:106 +#, c-format +msgid "Name" +msgstr "Название" + +#: src/pages/admin/AccountList.tsx:110 +#, c-format +msgid "Balance" +msgstr "Баланс" + +#: src/pages/admin/AccountList.tsx:112 +#, c-format +msgid "Actions" +msgstr "Действия" + +#: src/pages/admin/AccountList.tsx:151 +#, c-format +msgid "unknown" +msgstr "неизвестно" + +#: src/pages/admin/AccountList.tsx:170 +#, c-format +msgid "change password" +msgstr "изменить пароль" + +#: src/pages/admin/AccountList.tsx:179 +#, c-format +msgid "cashouts" +msgstr "выплаты" + +#: src/pages/admin/AccountList.tsx:189 +#, c-format +msgid "remove" +msgstr "удалить" + +#: src/pages/admin/AdminHome.tsx:168 +#, c-format +msgid "Cashout not implemented" +msgstr "Обналичка не реализована" + +#: src/pages/admin/AdminHome.tsx:184 +#, c-format +msgid "Select a section" +msgstr "Выберите раздел" + +#: src/pages/admin/AdminHome.tsx:202 +#, c-format +msgid "Last hour" +msgstr "Последний час" + +#: src/pages/admin/AdminHome.tsx:208 +#, c-format +msgid "Last day" +msgstr "Последний день" + +#: src/pages/admin/AdminHome.tsx:216 +#, c-format +msgid "Last month" +msgstr "Последний месяц" + +#: src/pages/admin/AdminHome.tsx:222 +#, c-format +msgid "Last year" +msgstr "Последний год" + +#: src/pages/admin/AdminHome.tsx:310 +#, c-format +msgid "Last Year" +msgstr "Прошлый год" + +#: src/pages/admin/AdminHome.tsx:325 +#, c-format +msgid "Trading volume on %1$s compared to %2$s" +msgstr "Объем торгов на %1$s по сравнению с %2$s" + +#: src/pages/admin/AdminHome.tsx:342 +#, c-format +msgid "Cashin" +msgstr "Внесения" + +#: src/pages/admin/AdminHome.tsx:352 +#, c-format +msgid "Cashout" +msgstr "Выплата" + +#: src/pages/admin/AdminHome.tsx:364 +#, c-format +msgid "Payin" +msgstr "Отплата" + +#: src/pages/admin/AdminHome.tsx:374 +#, c-format +msgid "Payout" +msgstr "Выплата" + +#: src/pages/admin/AdminHome.tsx:388 +#, c-format +msgid "download stats as CSV" +msgstr "скачать статистику в формате CSV" + +#: src/pages/admin/AdminHome.tsx:494 +#, c-format +msgid "Decreased by" +msgstr "Уменьшилось на" + +#: src/pages/admin/AdminHome.tsx:498 +#, c-format +msgid "Increased by" +msgstr "Увеличение на" + +#: src/pages/DownloadStats.tsx:89 +#, c-format +msgid "Download bank stats" +msgstr "Скачивать статистику банка" + +#: src/pages/DownloadStats.tsx:110 +#, c-format +msgid "Include hour metric" +msgstr "Включить часовую метрику" + +#: src/pages/DownloadStats.tsx:143 +#, c-format +msgid "Include day metric" +msgstr "Включить дневную метрику" + +#: src/pages/DownloadStats.tsx:173 +#, c-format +msgid "Include month metric" +msgstr "Включить месячную метрику" + +#: src/pages/DownloadStats.tsx:206 +#, c-format +msgid "Include year metric" +msgstr "Включить годовую метрику" + +#: src/pages/DownloadStats.tsx:239 +#, c-format +msgid "Include table header" +msgstr "Включить заголовок таблицы" + +#: src/pages/DownloadStats.tsx:272 +#, c-format +msgid "Add previous metric for compare" +msgstr "Добавить предыдущую метрику для сравнения" + +#: src/pages/DownloadStats.tsx:307 +#, c-format +msgid "Fail on first error" +msgstr "Сбой при первой ошибке" + +#: src/pages/DownloadStats.tsx:364 +#, c-format +msgid "Download" +msgstr "Скачивать" + +#: src/pages/DownloadStats.tsx:381 +#, c-format +msgid "downloading... %1$s" +msgstr "скачивание... %1$s" + +#: src/pages/DownloadStats.tsx:399 +#, c-format +msgid "Download completed" +msgstr "Скачивание завершено" + +#: src/pages/DownloadStats.tsx:400 +#, c-format +msgid "click here to save the file in your computer" +msgstr "Нажмите здесь, чтобы сохранить файл на своем компьютере" + +#: src/pages/PublicHistoriesPage.tsx:78 +#, c-format +msgid "History of public accounts" +msgstr "История публичных счетов" + +#: src/pages/RegistrationPage.tsx:48 +#, c-format +msgid "Currently, the bank is not accepting new registrations!" +msgstr "В настоящее время банк не принимает новые регистрации!" + +#: src/pages/RegistrationPage.tsx:87 +#, c-format +msgid "Missing name" +msgstr "Отсутствует имя" + +#: src/pages/RegistrationPage.tsx:91 +#, c-format +msgid "Use letters and numbers only, and start with a lowercase letter" +msgstr "Используйте только буквы и цифры и начинайте со строчной буквы" + +#: src/pages/RegistrationPage.tsx:107 +#, c-format +msgid "Passwords don't match" +msgstr "Пароли не совпадают" + +#: src/pages/RegistrationPage.tsx:130 +#, c-format +msgid "Server replied with invalid phone or email." +msgstr "Сервер ответил что телефон или электронной почта недействительны." + +#: src/pages/RegistrationPage.tsx:137 +#, c-format +msgid "Registration is disabled because the bank ran out of bonus credit." +msgstr "Регистрация отключена, так как в банке закончился бонусный кредит." + +#: src/pages/RegistrationPage.tsx:144 +#, c-format +msgid "No enough permission to create that account." +msgstr "Недостаточно разрешений для создания этого счёта." + +#: src/pages/RegistrationPage.tsx:151 +#, c-format +msgid "That account id is already taken." +msgstr "Этот идентификатор счёта уже занят." + +#: src/pages/RegistrationPage.tsx:158 +#, c-format +msgid "That username is already taken." +msgstr "Это имя пользователя уже используется." + +#: src/pages/RegistrationPage.tsx:165 +#, c-format +msgid "That username can't be used because is reserved." +msgstr "" +"Это имя пользователя не может быть использовано, так как оно зарезервировано." + +#: src/pages/RegistrationPage.tsx:172 +#, c-format +msgid "Only admin is allow to set debt limit." +msgstr "Только администратор может установить лимит задолженности." + +#: src/pages/RegistrationPage.tsx:179 +#, c-format +msgid "No information for the selected authentication channel." +msgstr "Нет информации о выбранном канале аутентификации." + +#: src/pages/RegistrationPage.tsx:186 +#, c-format +msgid "Authentication channel is not supported." +msgstr "Канал аутентификации не поддерживается." + +#: src/pages/RegistrationPage.tsx:193 +#, c-format +msgid "Only admin can create accounts with second factor authentication." +msgstr "" +"Только администратор может создавать учетные записи со второй " +"аутентификацией." + +#: src/pages/RegistrationPage.tsx:233 +#, c-format +msgid "Account registration" +msgstr "Регистрация счёта" + +#: src/pages/RegistrationPage.tsx:315 +#, c-format +msgid "Repeat password" +msgstr "Повторите Пароль" + +#: src/pages/RegistrationPage.tsx:457 +#, c-format +msgid "Create a random temporary user" +msgstr "Создать случайного временного пользователя" + +#: src/pages/QrCodeSection.tsx:110 +#, c-format +msgid "If you have a Taler wallet installed in this device" +msgstr "Если в этом устройстве установлен кошелёк Taler" + +#: src/pages/QrCodeSection.tsx:116 +#, c-format +msgid "" +"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" +msgstr "" +"Вы увидите подробности операции в своем кошельке, включая комиссию (если " +"применимо). Если у вас его еще нет, вы можете установить его следуя " +"инструкциям на" + +#: src/pages/QrCodeSection.tsx:143 +#, c-format +msgid "Withdraw" +msgstr "Снять средства" + +#: src/pages/QrCodeSection.tsx:152 +#, c-format +msgid "Or if you have the wallet in another device" +msgstr "Или если у вас есть кошелёк в другом устройстве" + +#: src/pages/QrCodeSection.tsx:157 +#, c-format +msgid "Scan the QR below to start the withdrawal." +msgstr "Отсканируйте QR-код ниже чтобы начать вывод средств." + +#: src/pages/WithdrawalQRCode.tsx:79 +#, c-format +msgid "Operation aborted" +msgstr "Операция прервана" + +#: src/pages/WithdrawalQRCode.tsx:82 +#, c-format +msgid "" +"The wire transfer to the Taler Exchange operator's account was aborted, your " +"balance was not affected." +msgstr "" +"Банковский перевод на счет оператора Обменника Taler был прерван, ваш баланс " +"не пострадал." + +#: src/pages/WithdrawalQRCode.tsx:88 +#, c-format +msgid "You can close this page now or continue to the account page." +msgstr "Теперь вы можете закрыть эту страницу или перейти на страницу счёта." + +#: src/pages/WithdrawalQRCode.tsx:147 +#, c-format +msgid "Done" +msgstr "Готово" + +#: src/pages/WithdrawalQRCode.tsx:158 +#, c-format +msgid "Operation canceled" +msgstr "Операция отменена" + +#: src/pages/WithdrawalQRCode.tsx:173 +#, c-format +msgid "The operation is marked as 'selected' but some step in the withdrawal failed" +msgstr "" +"Операция помечена как «выбранная», но какой-то шаг в выводе средств не удался" + +#: src/pages/WithdrawalQRCode.tsx:175 +#, c-format +msgid "The account is selected but no withdrawal identification found." +msgstr "Счёт выбран, но идентификатор вывода средств не найден." + +#: src/pages/WithdrawalQRCode.tsx:188 +#, c-format +msgid "" +"There is a withdrawal identification but no account has been selected or the " +"selected account is invalid." +msgstr "" +"Есть идентификатор вывода средств, но счёт не был выбран или выбранный счёт " +"недействителен." + +#: src/pages/WithdrawalQRCode.tsx:202 +#, c-format +msgid "" +"No withdrawal ID found and no account has been selected or the selected account " +"is invalid." +msgstr "" + +#: src/pages/WithdrawalQRCode.tsx:259 +#, c-format +msgid "Operation not found" +msgstr "" + +#: src/pages/WithdrawalQRCode.tsx:263 +#, c-format +msgid "" +"This operation is not known by the server. The operation id is wrong or the " +"server deleted the operation information before reaching here." +msgstr "" + +#: src/pages/WithdrawalQRCode.tsx:278 +#, c-format +msgid "Cotinue to dashboard" +msgstr "" + +#: src/pages/SolveChallengePage.tsx:98 +#, c-format +msgid "Cashout not found. It may be also mean that it was already aborted." +msgstr "" + +#: src/pages/SolveChallengePage.tsx:136 +#, c-format +msgid "Challenge not found." +msgstr "" + +#: src/pages/SolveChallengePage.tsx:143 +#, c-format +msgid "This user is not authorized to complete this challenge." +msgstr "" + +#: src/pages/SolveChallengePage.tsx:150 +#, c-format +msgid "Too many attempts, try another code." +msgstr "" + +#: src/pages/SolveChallengePage.tsx:157 +#, c-format +msgid "The confirmation code is wrong, try again." +msgstr "" + +#: src/pages/SolveChallengePage.tsx:164 +#, c-format +msgid "The operation expired." +msgstr "" + +#: src/pages/SolveChallengePage.tsx:197 +#, c-format +msgid "The operation failed." +msgstr "" + +#: src/pages/SolveChallengePage.tsx:212 +#, c-format +msgid "The operation needs another confirmation to complete." +msgstr "" + +#: src/pages/SolveChallengePage.tsx:224 +#, c-format +msgid "Account delete" +msgstr "Удаление счёта" + +#: src/pages/SolveChallengePage.tsx:226 +#, c-format +msgid "Account update" +msgstr "Обновление счёта" + +#: src/pages/SolveChallengePage.tsx:228 +#, c-format +msgid "Password update" +msgstr "" + +#: src/pages/SolveChallengePage.tsx:230 +#, c-format +msgid "Wire transfer" +msgstr "Перевод" + +#: src/pages/SolveChallengePage.tsx:232 +#, c-format +msgid "Withdrawal" +msgstr "Вывод" + +#: src/pages/SolveChallengePage.tsx:248 +#, c-format +msgid "Confirm the operation" +msgstr "Подтвердить операцию" + +#: src/pages/SolveChallengePage.tsx:271 +#, c-format +msgid "Enter the confirmation code" +msgstr "Введите код подтверждения" + +#: src/pages/SolveChallengePage.tsx:313 +#, c-format +msgid "Confirm" +msgstr "Подтвердить" + +#: src/pages/SolveChallengePage.tsx:348 +#, c-format +msgid "Send again" +msgstr "Отправить ещё раз" + +#: src/pages/SolveChallengePage.tsx:359 +#, c-format +msgid "Send code" +msgstr "Отправить код" + +#: src/pages/SolveChallengePage.tsx:369 +#, c-format +msgid "Operation details" +msgstr "Сведения об операции" + +#: src/pages/SolveChallengePage.tsx:529 +#, c-format +msgid "Challenge details" +msgstr "Детали подтверждения" + +#: src/pages/SolveChallengePage.tsx:536 +#, c-format +msgid "Sent at" +msgstr "Время отправления" + +#: src/pages/SolveChallengePage.tsx:551 +#, c-format +msgid "To phone" +msgstr "На телефон" + +#: src/pages/SolveChallengePage.tsx:553 +#, c-format +msgid "To email" +msgstr "На email" + +#: src/pages/WithdrawalOperationPage.tsx:49 +#, c-format +msgid "The Withdrawal URI is not valid" +msgstr "URI вывода недействителен" + +#: src/components/Cashouts/views.tsx:100 +#, c-format +msgid "Latest cashouts" +msgstr "Последние обналички" + +#: src/components/Cashouts/views.tsx:111 +#, c-format +msgid "Created" +msgstr "Создано" + +#: src/components/Cashouts/views.tsx:115 +#, c-format +msgid "Total debit" +msgstr "Всего дебет" + +#: src/components/Cashouts/views.tsx:119 +#, c-format +msgid "Total credit" +msgstr "Итого кредит" + +#: src/pages/ProfileNavigation.tsx:70 +#, c-format +msgid "Details" +msgstr "Подробности" + +#: src/pages/ProfileNavigation.tsx:74 +#, c-format +msgid "Delete" +msgstr "Удалить" + +#: src/pages/ProfileNavigation.tsx:78 +#, c-format +msgid "Credentials" +msgstr "Учетные данные" + +#: src/pages/ProfileNavigation.tsx:82 +#, c-format +msgid "Cashouts" +msgstr "Выплаты" + +#: src/pages/business/CreateCashout.tsx:95 +#, c-format +msgid "Unable to create a cashout" +msgstr "Не удается создать выплату" + +#: src/pages/business/CreateCashout.tsx:96 +#, c-format +msgid "The bank configuration does not support cashout operations." +msgstr "Конфигурация банка не поддерживает операции выплаты." + +#: src/pages/business/CreateCashout.tsx:223 +#, c-format +msgid "need to be higher due to fees" +msgstr "должна быть выше из-за комиссий" + +#: src/pages/business/CreateCashout.tsx:225 +#, c-format +msgid "the total transfer at destination will be zero" +msgstr "общая сумма перевода в назначенее будет равна нулю" + +#: src/pages/business/CreateCashout.tsx:250 +#, c-format +msgid "Cashout created" +msgstr "Выплата создана" + +#: src/pages/business/CreateCashout.tsx:272 +#, c-format +msgid "Duplicated request detected, check if the operation succeeded or try again." +msgstr "" +"Обнаружен дубликат запроса, проверьте, успешно ли выполнена операция, или " +"повторите попытку." + +#: src/pages/business/CreateCashout.tsx:279 +#, c-format +msgid "The conversion rate was incorrectly applied" +msgstr "Неправильно применен курс конвертации" + +#: src/pages/business/CreateCashout.tsx:286 +#, c-format +msgid "The account does not have sufficient funds" +msgstr "На счете недостаточно средств" + +#: src/pages/business/CreateCashout.tsx:293 +#, c-format +msgid "Cashouts are not supported" +msgstr "Выплаты не поддерживаются" + +#: src/pages/business/CreateCashout.tsx:300 +#, c-format +msgid "Missing cashout URI in the profile" +msgstr "Отсутствующий URI вылат в профиле" + +#: src/pages/business/CreateCashout.tsx:307 +#, c-format +msgid "" +"Sending the confirmation message failed, retry later or contact the " +"administrator." +msgstr "" +"Не удалось отправить сообщение с подтверждением, повторите попытку позже или " +"обратитесь к администратору." + +#: src/pages/business/CreateCashout.tsx:339 +#, c-format +msgid "Conversion rate" +msgstr "Обменный курс" + +#: src/pages/business/CreateCashout.tsx:360 +#, c-format +msgid "Fee" +msgstr "Комиссия" + +#: src/pages/business/CreateCashout.tsx:374 +#, c-format +msgid "To account" +msgstr "На счёт" + +#: src/pages/business/CreateCashout.tsx:381 +#, c-format +msgid "No cashout account" +msgstr "Нет счёта для выплат" + +#: src/pages/business/CreateCashout.tsx:382 +#, c-format +msgid "Before doing a cashout you need to complete your profile" +msgstr "Перед тем, как сделать выплату, вам необходимо заполнить свой профиль" + +#: src/pages/business/CreateCashout.tsx:440 +#, c-format +msgid "Amount to send" +msgstr "Сумма к отправке" + +#: src/pages/business/CreateCashout.tsx:441 +#, c-format +msgid "Amount to receive" +msgstr "Сумма к получению" + +#: src/pages/business/CreateCashout.tsx:490 +#, c-format +msgid "Total cost" +msgstr "Общая стоимость" + +#: src/pages/business/CreateCashout.tsx:505 +#, c-format +msgid "Balance left" +msgstr "Остаток баланса" + +#: src/pages/business/CreateCashout.tsx:520 +#, c-format +msgid "Before fee" +msgstr "Комиссия до" + +#: src/pages/business/CreateCashout.tsx:533 +#, c-format +msgid "Total cashout transfer" +msgstr "Общий сумма перевода выплаты" + +#: src/pages/business/CreateCashout.tsx:553 +#, c-format +msgid "No cashout channel available" +msgstr "Канал вывода средств недоступен" + +#: src/pages/business/CreateCashout.tsx:555 +#, c-format +msgid "" +"Before doing a cashout the server need to provide an second channel to confirm " +"the operation" +msgstr "" +"Перед тем, как сделать кэшаут, серверу необходимо предоставить второй канал " +"для подтверждения операции" + +#: src/pages/business/CreateCashout.tsx:567 +#, c-format +msgid "Second factor authentication" +msgstr "Двухфакторная аутентификация" + +#: src/pages/business/CreateCashout.tsx:598 +#, c-format +msgid "Email" +msgstr "Email" + +#: src/pages/business/CreateCashout.tsx:600 +#, c-format +msgid "add a email in your profile to enable this option" +msgstr "" +"Добавьте адрес электронной почты в свой профиль, чтобы включить эту опцию" + +#: src/pages/business/CreateCashout.tsx:646 +#, c-format +msgid "SMS" +msgstr "SMS" + +#: src/pages/business/CreateCashout.tsx:648 +#, c-format +msgid "add a phone number in your profile to enable this option" +msgstr "Добавьте номер телефона в свой профиль, чтобы включить эту опцию" + +#: src/pages/account/CashoutListForAccount.tsx:52 +#, c-format +msgid "Cashout for account %1$s" +msgstr "Выплата для аккаунта %1$s" + +#: src/pages/admin/AccountForm.tsx:165 +#, c-format +msgid "it doesn't have the pattern of an IBAN number" +msgstr "у него нет шаблона номера IBAN" + +#: src/pages/admin/AccountForm.tsx:185 +#, c-format +msgid "it doesn't have the pattern of an email" +msgstr "У него нет шаблона электронного письма" + +#: src/pages/admin/AccountForm.tsx:190 +#, c-format +msgid "should start with +" +msgstr "должен начинаться с +" + +#: src/pages/admin/AccountForm.tsx:192 +#, c-format +msgid "phone number can't have other than numbers" +msgstr "Номер телефона не может иметь ничего, кроме цифр" + +#: src/pages/admin/AccountForm.tsx:329 +#, c-format +msgid "account identification in the bank" +msgstr "Идентификация счета в банке" + +#: src/pages/admin/AccountForm.tsx:365 +#, c-format +msgid "name of the person owner the account" +msgstr "имя владельца счёта" + +#: src/pages/admin/AccountForm.tsx:374 +#, c-format +msgid "Internal IBAN" +msgstr "Внутренний IBAN" + +#: src/pages/admin/AccountForm.tsx:377 +#, c-format +msgid "if empty a random account number will be assigned" +msgstr "Если пусто, будет присвоен случайный номер счета" + +#: src/pages/admin/AccountForm.tsx:378 +#, c-format +msgid "account identification for bank transfer" +msgstr "Идентификация счета для банковского перевода" + +#: src/pages/admin/AccountForm.tsx:423 +#, c-format +msgid "Phone" +msgstr "Телефон" + +#: src/pages/admin/AccountForm.tsx:451 +#, c-format +msgid "Cashout IBAN" +msgstr "IBAN выплаты" + +#: src/pages/admin/AccountForm.tsx:452 +#, c-format +msgid "account number where the money is going to be sent when doing cashouts" +msgstr "номер счета, на который будут отправлены деньги при выводе средств" + +#: src/pages/admin/AccountForm.tsx:470 +#, c-format +msgid "Max debt" +msgstr "Максимальная задолженность" + +#: src/pages/admin/AccountForm.tsx:494 +#, c-format +msgid "how much is user able to transfer after zero balance" +msgstr "Какую сумму пользователь может перевести после нулевого баланса" + +#: src/pages/admin/AccountForm.tsx:508 +#, c-format +msgid "Is this a Taler Exchange?" +msgstr "Это Обменник Taler?" + +#: src/pages/admin/AccountForm.tsx:549 +#, c-format +msgid "This server doesn't support second factor authentication." +msgstr "Этот сервер не поддерживает двухфакторную аутентификацию." + +#: src/pages/admin/AccountForm.tsx:560 +#, c-format +msgid "Enable second factor authentication" +msgstr "Включите двухфакторную аутентификацию" + +#: src/pages/admin/AccountForm.tsx:596 +#, c-format +msgid "Using email" +msgstr "Используя email" + +#: src/pages/admin/AccountForm.tsx:654 +#, c-format +msgid "Using SMS" +msgstr "Используя SMS" + +#: src/pages/admin/AccountForm.tsx:691 +#, c-format +msgid "Is this account public?" +msgstr "Является ли этот счёт общедоступным?" + +#: src/pages/admin/AccountForm.tsx:719 +#, c-format +msgid "public accounts have their balance publicly accessible" +msgstr "Баланс публичных счётов находится в открытом доступе" + +#: src/pages/account/ShowAccountDetails.tsx:100 +#, c-format +msgid "Account updated" +msgstr "Счёт обновлён" + +#: src/pages/account/ShowAccountDetails.tsx:107 +#, c-format +msgid "The rights to change the account are not sufficient" +msgstr "Недостаточно прав на изменение счёта" + +#: src/pages/account/ShowAccountDetails.tsx:114 +#, c-format +msgid "The username was not found" +msgstr "Имя пользователя не найдено" + +#: src/pages/account/ShowAccountDetails.tsx:121 +#, c-format +msgid "You can't change the legal name, please contact the your account administrator." +msgstr "" +"Вы не можете изменить официальное имя, обратитесь к администратору вашей " +"учетной записи." + +#: src/pages/account/ShowAccountDetails.tsx:128 +#, c-format +msgid "You can't change the debt limit, please contact the your account administrator." +msgstr "" +"Вы не можете изменить лимит задолженности, обратитесь к администратору " +"аккаунта." + +#: src/pages/account/ShowAccountDetails.tsx:135 +#, c-format +msgid "" +"You can't change the cashout address, please contact the your account " +"administrator." +msgstr "" +"Вы не можете изменить адрес для вывода средств, пожалуйста, свяжитесь с " +"администратором вашего аккаунта." + +#: src/pages/account/ShowAccountDetails.tsx:177 +#, c-format +msgid "Account \"%1$s\"" +msgstr "Счет \"%1$s\"" + +#: src/pages/account/ShowAccountDetails.tsx:190 +#, c-format +msgid "Change details" +msgstr "Изменение реквизитов" + +#: src/pages/account/ShowAccountDetails.tsx:235 +#, c-format +msgid "Update" +msgstr "Обновить" + +#: src/pages/account/UpdateAccountPassword.tsx:78 +#, c-format +msgid "password doesn't match" +msgstr "пароль не совпадает" + +#: src/pages/account/UpdateAccountPassword.tsx:95 +#, c-format +msgid "Password changed" +msgstr "Пароль изменен" + +#: src/pages/account/UpdateAccountPassword.tsx:102 +#, c-format +msgid "Not authorized to change the password, maybe the session is invalid." +msgstr "" + +#: src/pages/account/UpdateAccountPassword.tsx:112 +#, c-format +msgid "" +"You need to provide the old password. If you don't have it contact your account " +"administrator." +msgstr "" + +#: src/pages/account/UpdateAccountPassword.tsx:117 +#, c-format +msgid "Your current password doesn't match, can't change to a new password." +msgstr "" + +#: src/pages/account/UpdateAccountPassword.tsx:149 +#, c-format +msgid "Update password" +msgstr "Обновить пароль" + +#: src/pages/account/UpdateAccountPassword.tsx:167 +#, c-format +msgid "New password" +msgstr "Новый пароль" + +#: src/pages/account/UpdateAccountPassword.tsx:195 +#, c-format +msgid "Type it again" +msgstr "Введите его ещё раз" + +#: src/pages/account/UpdateAccountPassword.tsx:217 +#, c-format +msgid "repeat the same password" +msgstr "повторите этот же пароль" + +#: src/pages/account/UpdateAccountPassword.tsx:227 +#, c-format +msgid "Current password" +msgstr "Текущий пароль" + +#: src/pages/account/UpdateAccountPassword.tsx:248 +#, c-format +msgid "your current password, for security" +msgstr "" + +#: src/pages/account/UpdateAccountPassword.tsx:272 +#, c-format +msgid "Change" +msgstr "Изменить" + +#: src/pages/admin/CreateNewAccount.tsx:74 +#, c-format +msgid "" +"Account created with password \"%1$s\". The user must change the password on the " +"next login." +msgstr "" + +#: src/pages/admin/CreateNewAccount.tsx:83 +#, c-format +msgid "Server replied that phone or email is invalid" +msgstr "" + +#: src/pages/admin/CreateNewAccount.tsx:90 +#, c-format +msgid "The rights to perform the operation are not sufficient" +msgstr "" + +#: src/pages/admin/CreateNewAccount.tsx:97 +#, c-format +msgid "Account username is already taken" +msgstr "" + +#: src/pages/admin/CreateNewAccount.tsx:104 +#, c-format +msgid "Account id is already taken" +msgstr "" + +#: src/pages/admin/CreateNewAccount.tsx:111 +#, c-format +msgid "Bank ran out of bonus credit." +msgstr "" + +#: src/pages/admin/CreateNewAccount.tsx:118 +#, c-format +msgid "Account username can't be used because is reserved" +msgstr "" + +#: src/pages/admin/CreateNewAccount.tsx:160 +#, c-format +msgid "Can't create accounts" +msgstr "" + +#: src/pages/admin/CreateNewAccount.tsx:161 +#, c-format +msgid "Only system admin can create accounts." +msgstr "" + +#: src/pages/admin/CreateNewAccount.tsx:183 +#, c-format +msgid "New business account" +msgstr "Новый бизнес счёт" + +#: src/pages/admin/CreateNewAccount.tsx:209 +#, c-format +msgid "Create" +msgstr "Создать" + +#: src/pages/admin/RemoveAccount.tsx:94 +#, c-format +msgid "Can't delete the account" +msgstr "" + +#: src/pages/admin/RemoveAccount.tsx:95 +#, c-format +msgid "" +"The account can't be delete while still holding some balance. First make sure " +"that the owner make a complete cashout." +msgstr "" + +#: src/pages/admin/RemoveAccount.tsx:117 +#, c-format +msgid "Account removed" +msgstr "" + +#: src/pages/admin/RemoveAccount.tsx:124 +#, c-format +msgid "No enough permission to delete the account." +msgstr "" + +#: src/pages/admin/RemoveAccount.tsx:131 +#, c-format +msgid "The username was not found." +msgstr "" + +#: src/pages/admin/RemoveAccount.tsx:138 +#, c-format +msgid "Can't delete a reserved username." +msgstr "" + +#: src/pages/admin/RemoveAccount.tsx:145 +#, c-format +msgid "Can't delete an account with balance different than zero." +msgstr "" + +#: src/pages/admin/RemoveAccount.tsx:170 +#, c-format +msgid "name doesn't match" +msgstr "" + +#: src/pages/admin/RemoveAccount.tsx:180 +#, c-format +msgid "You are going to remove the account" +msgstr "" + +#: src/pages/admin/RemoveAccount.tsx:182 +#, c-format +msgid "This step can't be undone." +msgstr "" + +#: src/pages/admin/RemoveAccount.tsx:188 +#, c-format +msgid "Deleting account \"%1$s\"" +msgstr "Удаление счёта \"%1$s\"" + +#: src/pages/admin/RemoveAccount.tsx:206 +#, c-format +msgid "Verification" +msgstr "Проверка" + +#: src/pages/admin/RemoveAccount.tsx:231 +#, c-format +msgid "enter the account name that is going to be deleted" +msgstr "" + +#: src/pages/business/ShowCashoutDetails.tsx:49 +#, c-format +msgid "cashout id should be a number" +msgstr "" + +#: src/pages/business/ShowCashoutDetails.tsx:65 +#, c-format +msgid "This cashout not found. Maybe already aborted." +msgstr "" + +#: src/pages/business/ShowCashoutDetails.tsx:106 +#, c-format +msgid "Cashout detail" +msgstr "Подробности обналичивания" + +#: src/pages/business/ShowCashoutDetails.tsx:139 +#, c-format +msgid "Debited" +msgstr "Дебетировано" + +#: src/pages/business/ShowCashoutDetails.tsx:154 +#, c-format +msgid "Credited" +msgstr "Кредитировано" + +#: src/Routing.tsx:140 +#, c-format +msgid "Welcome to %1$s!" +msgstr "Добро пожаловать в %1$s!" diff --git a/packages/merchant-backoffice-ui/src/Application.tsx b/packages/merchant-backoffice-ui/src/Application.tsx @@ -279,7 +279,10 @@ const swrCacheEvictor = new (class return; } case TalerMerchantInstanceCacheEviction.UPDATE_PRODUCT: { - await Promise.all([revalidateProductDetails()]); + await Promise.all([ + revalidateProductDetails(), + revalidateInstanceProducts(), + ]); return; } case TalerMerchantInstanceCacheEviction.DELETE_PRODUCT: { diff --git a/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx @@ -147,13 +147,13 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) { name="minimum_age" label={i18n.str`Age restriction`} tooltip={i18n.str`is this product restricted for customer below certain age?`} - help={i18n.str`minimum age of the buyer`} + help={i18n.str`minimum age of the customer`} /> <Input<Entity> name="unit" label={i18n.str`Unit name`} tooltip={i18n.str`unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers`} - help={i18n.str`exajmple: kg, items or liters`} + help={i18n.str`example: kg, items or liters`} /> <InputCurrency<Entity> name="price" diff --git a/packages/merchant-backoffice-ui/src/hooks/templates.ts b/packages/merchant-backoffice-ui/src/hooks/templates.ts @@ -57,7 +57,8 @@ export function useInstanceTemplates() { if (data === undefined) return undefined; if (data.type !== "ok") return data; - return buildPaginatedResult(data.body.templates, offset, setOffset, (d) => d.template_id) + // return buildPaginatedResult(data.body.templates, offset, setOffset, (d) => d.template_id) + return data; } diff --git a/packages/merchant-backoffice-ui/src/i18n/de.po b/packages/merchant-backoffice-ui/src/i18n/de.po @@ -17,7 +17,7 @@ msgstr "" "Project-Id-Version: Taler Wallet\n" "Report-Msgid-Bugs-To: taler@gnu.org\n" "POT-Creation-Date: 2016-11-23 00:00+0100\n" -"PO-Revision-Date: 2024-03-21 21:39+0000\n" +"PO-Revision-Date: 2024-05-07 14:32+0000\n" "Last-Translator: Stefan Kügel <skuegel@web.de>\n" "Language-Team: German <https://weblate.taler.net/projects/gnu-taler/" "merchant-backoffice/de/>\n" @@ -26,27 +26,27 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.2.1\n" +"X-Generator: Weblate 5.4.3\n" #: src/components/modal/index.tsx:71 #, c-format msgid "Cancel" -msgstr "" +msgstr "Zurück" #: src/components/modal/index.tsx:79 #, c-format msgid "%1$s" -msgstr "" +msgstr "%1$s" #: src/components/modal/index.tsx:84 #, c-format msgid "Close" -msgstr "" +msgstr "Schließen" #: src/components/modal/index.tsx:124 #, c-format msgid "Continue" -msgstr "" +msgstr "Weiter" #: src/components/modal/index.tsx:178 #, c-format @@ -66,12 +66,12 @@ msgstr "" #: src/components/modal/index.tsx:299 #, c-format msgid "cannot be empty" -msgstr "" +msgstr "darf nicht leer sein" #: src/components/modal/index.tsx:301 #, c-format msgid "cannot be the same as the old token" -msgstr "" +msgstr "muss sich vom alten Token unterscheiden" #: src/components/modal/index.tsx:305 #, c-format @@ -563,57 +563,59 @@ msgstr "" #: src/paths/instance/orders/create/CreatePage.tsx:164 #, c-format msgid "not a valid json" -msgstr "" +msgstr "kein gültiges JSON-Format" #: src/paths/instance/orders/create/CreatePage.tsx:170 #, c-format msgid "should be in the future" -msgstr "" +msgstr "sollte in der Zukunft liegen" #: src/paths/instance/orders/create/CreatePage.tsx:173 #, c-format msgid "refund deadline cannot be before pay deadline" -msgstr "" +msgstr "Die Rückerstattungsfrist kann nicht vor der Zahlungsfrist liegen" #: src/paths/instance/orders/create/CreatePage.tsx:179 #, c-format msgid "wire transfer deadline cannot be before refund deadline" -msgstr "" +msgstr "Die Überweisungsfrist kann nicht vor der Rückerstattungsfrist liegen" #: src/paths/instance/orders/create/CreatePage.tsx:190 #, c-format msgid "wire transfer deadline cannot be before pay deadline" -msgstr "" +msgstr "Die Überweisungsfrist kann nicht vor der Zahlungsfrist liegen" #: src/paths/instance/orders/create/CreatePage.tsx:197 #, c-format msgid "should have a refund deadline" -msgstr "" +msgstr "sollte eine Rückerstattungsfrist haben" #: src/paths/instance/orders/create/CreatePage.tsx:202 #, c-format msgid "auto refund cannot be after refund deadline" msgstr "" +"Die automatische Rückerstattung kann nicht nach der Rückerstattungsfrist " +"erfolgen" #: src/paths/instance/orders/create/CreatePage.tsx:360 #, c-format msgid "Manage products in order" -msgstr "" +msgstr "Artikel in der Bestellung verwalten" #: src/paths/instance/orders/create/CreatePage.tsx:369 #, c-format msgid "Manage list of products in the order." -msgstr "" +msgstr "Liste der Artikel in der Bestellung verwalten." #: src/paths/instance/orders/create/CreatePage.tsx:391 #, c-format msgid "Remove this product from the order." -msgstr "" +msgstr "Diesen Artikel aus der Bestellung entfernen." #: src/paths/instance/orders/create/CreatePage.tsx:415 #, c-format msgid "Total price" -msgstr "" +msgstr "Gesamtpreis" #: src/paths/instance/orders/create/CreatePage.tsx:417 #, c-format @@ -623,12 +625,12 @@ msgstr "" #: src/paths/instance/orders/create/CreatePage.tsx:430 #, c-format msgid "Amount to be paid by the customer" -msgstr "" +msgstr "Zu zahlender Betrag" #: src/paths/instance/orders/create/CreatePage.tsx:436 #, c-format msgid "Order price" -msgstr "" +msgstr "Bestellsumme" #: src/paths/instance/orders/create/CreatePage.tsx:437 #, c-format @@ -638,12 +640,12 @@ msgstr "" #: src/paths/instance/orders/create/CreatePage.tsx:444 #, c-format msgid "Summary" -msgstr "" +msgstr "Zusammenfassung" #: src/paths/instance/orders/create/CreatePage.tsx:445 #, c-format msgid "Title of the order to be shown to the customer" -msgstr "" +msgstr "Bezeichnung der Bestellung, die den Kunden angezeigt wird" #: src/paths/instance/orders/create/CreatePage.tsx:450 #, c-format @@ -653,12 +655,12 @@ msgstr "" #: src/paths/instance/orders/create/CreatePage.tsx:455 #, c-format msgid "Delivery date" -msgstr "" +msgstr "Lieferdatum" #: src/paths/instance/orders/create/CreatePage.tsx:456 #, c-format msgid "Deadline for physical delivery assured by the merchant." -msgstr "" +msgstr "Vom Händler zugesicherte Zustellfrist." #: src/paths/instance/orders/create/CreatePage.tsx:461 #, c-format @@ -668,22 +670,22 @@ msgstr "" #: src/paths/instance/orders/create/CreatePage.tsx:462 #, c-format msgid "address where the products will be delivered" -msgstr "" +msgstr "Zustelladresse der Artikel" #: src/paths/instance/orders/create/CreatePage.tsx:469 #, c-format msgid "Fulfillment URL" -msgstr "" +msgstr "Adresse digitaler Dienstleistung (Fulfillment-URL)" #: src/paths/instance/orders/create/CreatePage.tsx:470 #, c-format msgid "URL to which the user will be redirected after successful payment." -msgstr "" +msgstr "URL der von Kunden zu besuchenden Adresse nach erfolgter Bezahlung." #: src/paths/instance/orders/create/CreatePage.tsx:476 #, c-format msgid "Taler payment options" -msgstr "" +msgstr "Taler-Zahlungsoptionen" #: src/paths/instance/orders/create/CreatePage.tsx:477 #, c-format @@ -693,7 +695,7 @@ msgstr "" #: src/paths/instance/orders/create/CreatePage.tsx:481 #, c-format msgid "Payment deadline" -msgstr "" +msgstr "Zahlungsfrist" #: src/paths/instance/orders/create/CreatePage.tsx:482 #, c-format diff --git a/packages/merchant-backoffice-ui/src/index.html b/packages/merchant-backoffice-ui/src/index.html @@ -26,6 +26,7 @@ <meta name="viewport" content="width=device-width,initial-scale=1" /> <meta name="mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" /> + <meta name="taler-support" content="uri,api" /> <link rel="icon" diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx @@ -506,6 +506,7 @@ function difference(price: string, tax: number) { ps[1] = `${p - tax}`; return ps.join(":"); } -function sum(taxes: TalerMerchantApi.Tax[]) { +function sum(taxes: TalerMerchantApi.Tax[] | undefined) { + if (taxes === undefined) return 0; return taxes.reduce((p, c) => p + parseInt(c.tax.split(":")[1], 10), 0); } 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 @@ -70,7 +70,7 @@ interface Props { export function CreatePage({ onCreate, onBack }: Props): VNode { const { i18n } = useTranslationContext(); const { config } = useSessionContext(); - const {state:session} = useSessionContext(); + const { state: session } = useSessionContext(); const devices = useInstanceOtpDevices(); const [state, setState] = useState<Partial<Entity>>({ @@ -100,11 +100,11 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { : undefined, description: !state.description ? i18n.str`should not be empty` : undefined, amount: !state.amount - ? undefined + ? state.amount_editable ? undefined : i18n.str`required` : !parsedPrice ? i18n.str`not valid` : Amounts.isZero(parsedPrice) - ? i18n.str`must be greater than 0` + ? state.amount_editable ? undefined : i18n.str`must be greater than 0` : undefined, minimum_age: state.minimum_age && state.minimum_age < 0 @@ -125,24 +125,30 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { (k) => (errors as Record<string, unknown>)[k] !== undefined, ); + const zero = Amounts.stringify(Amounts.zeroOfCurrency(config.currency)) + const submitForm = () => { if (hasErrors) return Promise.reject(); + const contract_amount = state.amount_editable ? undefined : state.amount as AmountString + const contract_summary = state.summary_editable ? undefined : state.summary + const template_contract: TalerMerchantApi.TemplateContractDetails = { + minimum_age: state.minimum_age!, + pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!), + amount: contract_amount, + summary: contract_summary, + currency: + cList.length > 1 && state.currency_editable + ? undefined + : config.currency, + } return onCreate({ template_id: state.id!, template_description: state.description!, - template_contract: { - minimum_age: state.minimum_age!, - pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!), - amount: state.amount_editable ? undefined : state.amount, - summary: state.summary_editable ? undefined : state.summary, - currency: - cList.length > 1 && state.currency_editable - ? undefined - : config.currency, - }, + template_contract, + required_currency: contract_amount !== undefined ? undefined : config.currency, editable_defaults: { - amount: !state.amount_editable ? undefined : state.amount, - summary: !state.summary_editable ? undefined : state.summary, + amount: !state.amount_editable ? undefined : (state.amount ?? zero), + summary: !state.summary_editable ? undefined : (state.summary ?? ""), currency: cList.length === 1 || !state.currency_editable ? undefined diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx @@ -93,11 +93,16 @@ export default function ListTemplates({ /> <ListPage - templates={result.body} - onLoadMoreBefore={ - result.isFirstPage ? undefined: result.loadFirst - } - onLoadMoreAfter={result.isLastPage ? undefined : result.loadNext} + // templates={result.body} + // onLoadMoreBefore={ + // result.isFirstPage ? undefined: result.loadFirst + // } + // onLoadMoreAfter={result.isLastPage ? undefined : result.loadNext} + + templates={result.body.templates} + onLoadMoreBefore={undefined} + onLoadMoreAfter={undefined} + onCreate={onCreate} onSelect={(e) => { onSelect(e.template_id); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx @@ -70,7 +70,8 @@ export function QrPage({ id: templateId, onBack }: Props): VNode { const payTemplateUri = stringifyPayTemplateUri({ merchantBaseUrl, templateId, - templateParams: {}, + // FIXME! + //templateParams: {}, }); return ( @@ -78,7 +79,7 @@ export function QrPage({ id: templateId, onBack }: Props): VNode { <section id="printThis"> <QR text={payTemplateUri} /> <pre style={{ textAlign: "center" }}> - <a href={payTemplateUri}>{payTemplateUri}</a> + <a target="_blank" rel="noreferrer" href={payTemplateUri}>{payTemplateUri}</a> </pre> </section> 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 @@ -51,7 +51,7 @@ type Entity = { description?: string; otpId?: string | null; summary?: string; - amount?: AmountString; + amount?: string; minimum_age?: number; pay_duration?: Duration; summary_editable?: boolean; @@ -68,7 +68,7 @@ interface Props { export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { const { i18n } = useTranslationContext(); const { config } = useSessionContext(); - const {state:session} = useSessionContext(); + const { state: session } = useSessionContext(); const [state, setState] = useState<Partial<Entity>>({ description: template.template_description, @@ -76,8 +76,8 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { otpId: template.otp_id, pay_duration: template.template_contract.pay_duration ? Duration.fromTalerProtocolDuration( - template.template_contract.pay_duration, - ) + template.template_contract.pay_duration, + ) : undefined, summary: template.editable_defaults?.summary ?? template.template_contract.summary, @@ -85,8 +85,8 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { template.editable_defaults?.amount ?? (template.template_contract.amount as AmountString | undefined), currency_editable: !!template.editable_defaults?.currency, - summary_editable: !!template.editable_defaults?.summary, - amount_editable: !!template.editable_defaults?.amount, + summary_editable: template.editable_defaults?.summary !== undefined, + amount_editable: template.editable_defaults?.amount !== undefined, }); function updateState(up: (s: Partial<Entity>) => Partial<Entity>) { @@ -117,11 +117,11 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { const errors: FormErrors<Entity> = { description: !state.description ? i18n.str`should not be empty` : undefined, amount: !state.amount - ? undefined + ? state.amount_editable ? undefined : i18n.str`required` : !parsedPrice ? i18n.str`not valid` : Amounts.isZero(parsedPrice) - ? i18n.str`must be greater than 0` + ? state.amount_editable ? undefined : i18n.str`must be greater than 0` : undefined, minimum_age: state.minimum_age && state.minimum_age < 0 @@ -142,23 +142,29 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { (k) => (errors as Record<string, unknown>)[k] !== undefined, ); + const zero = Amounts.stringify(Amounts.zeroOfCurrency(config.currency)) + const submitForm = () => { if (hasErrors) return Promise.reject(); + const contract_amount = state.amount_editable ? undefined : state.amount as AmountString + const contract_summary = state.summary_editable ? undefined : state.summary + const template_contract: TalerMerchantApi.TemplateContractDetails = { + minimum_age: state.minimum_age!, + pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!), + amount: contract_amount, + summary: contract_summary, + currency: + cList.length > 1 && state.currency_editable + ? undefined + : config.currency, + } return onUpdate({ template_description: state.description!, - template_contract: { - minimum_age: state.minimum_age!, - pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!), - amount: state.amount_editable ? undefined : state.amount, - summary: state.summary_editable ? undefined : state.summary, - currency: - cList.length > 1 && state.currency_editable - ? undefined - : config.currency, - }, + template_contract, + required_currency: contract_amount !== undefined ? undefined : config.currency, editable_defaults: { - amount: !state.amount_editable ? undefined : state.amount, - summary: !state.summary_editable ? undefined : state.summary, + amount: !state.amount_editable ? undefined : (state.amount ?? zero), + summary: !state.summary_editable ? undefined : (state.summary ?? ""), currency: cList.length === 1 || !state.currency_editable ? undefined diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx @@ -19,9 +19,9 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { TalerMerchantApi } from "@gnu-taler/taler-util"; +import { AmountString, TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; +import { VNode, h } from "preact"; import { useState } from "preact/hooks"; import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { @@ -44,20 +44,19 @@ export function UsePage({ id, template, onCreateOrder, onBack }: Props): VNode { const { i18n } = useTranslationContext(); const [state, setState] = useState<Partial<Entity>>({ - currency: template.editable_defaults?.currency ?? template.template_contract.currency, - amount: template.editable_defaults?.amount ?? template.template_contract.amount, - summary: template.editable_defaults?.summary ?? template.template_contract.summary, + currency: + template.editable_defaults?.currency ?? + template.template_contract.currency, + // FIXME: Add additional check here, editable default might be a plain string! + amount: (template.editable_defaults?.amount ?? + template.template_contract.amount) as AmountString, + summary: + template.editable_defaults?.summary ?? template.template_contract.summary, }); const errors: FormErrors<Entity> = { - amount: - !state.amount - ? i18n.str`Amount is required` - : undefined, - summary: - !state.summary - ? i18n.str`Order summary is required` - : undefined, + amount: !state.amount ? i18n.str`Amount is required` : undefined, + summary: !state.summary ? i18n.str`Order summary is required` : undefined, }; const hasErrors = Object.keys(errors).some( diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts @@ -25,7 +25,6 @@ * Imports */ import { - AccountAddDetails, AccountRestriction, AmountJson, Amounts, @@ -36,8 +35,10 @@ import { Logger, MerchantInstanceConfig, PartialMerchantInstanceConfig, + PaytoString, TalerCorebankApiClient, TalerError, + TalerMerchantApi, WalletNotification, createEddsaKeyPair, eddsaGetPublic, @@ -779,6 +780,17 @@ export class LibeufinBankService config.setString("libeufin-bank", "currency", bc.currency); config.setString("libeufin-bank", "port", `${bc.httpPort}`); config.setString("libeufin-bank", "serve", "tcp"); + config.setString("libeufin-bank", "wire_type", "x-taler-bank"); + config.setString( + "libeufin-bank", + "x_taler_bank_payto_hostname", + "localhost", + ); + config.setString( + "libeufin-bank", + "default_debt_limit", + bc.maxDebt ?? `${bc.currency}:100`, + ); config.setString( "libeufin-bank", "DEFAULT_DEBT_LIMIT", @@ -887,15 +899,21 @@ export class LibeufinBankService } // Use libeufin bank instead of pybank. -const useLibeufinBank = false; +export const useLibeufinBank = process.env["WITH_LIBEUFIN"] === "1"; export interface BankServiceHandle { readonly corebankApiBaseUrl: string; readonly http: HttpRequestLibrary; + + setSuggestedExchange(exchange: ExchangeService, exchangePayto: string): void; + start(): Promise<void>; + pingUntilAvailable(): Promise<void>; } export type BankService = BankServiceHandle; -export const BankService = FakebankService; +export const BankService = useLibeufinBank + ? LibeufinBankService + : FakebankService; export interface ExchangeConfig { name: string; @@ -1770,8 +1788,8 @@ export class MerchantService implements MerchantServiceInterface { const accountCreateUrl = `http://localhost:${this.merchantConfig.httpPort}/instances/${instanceConfig.id}/private/accounts`; for (const paytoUri of instanceConfig.paytoUris) { - const accountReq: AccountAddDetails = { - payto_uri: paytoUri, + const accountReq: TalerMerchantApi.AccountAddDetails = { + payto_uri: paytoUri as PaytoString, }; const acctResp = await harnessHttpLib.fetch(accountCreateUrl, { method: "POST", @@ -2234,7 +2252,6 @@ export function generateRandomTestIban(salt: string | null = null): string { } export function getWireMethodForTest(): string { - if (useLibeufinBank) return "iban"; return "x-taler-bank"; } @@ -2243,10 +2260,6 @@ export function getWireMethodForTest(): string { * on whether the banking is served by euFin or Pybank. */ export function generateRandomPayto(label: string): string { - if (useLibeufinBank) - return `payto://iban/SANDBOXX/${generateRandomTestIban( - label, - )}?receiver-name=${label}`; return `payto://x-taler-bank/localhost/${label}?receiver-name=${label}`; } diff --git a/packages/taler-harness/src/harness/helpers.ts b/packages/taler-harness/src/harness/helpers.ts @@ -29,11 +29,11 @@ import { Duration, Logger, MerchantApiClient, - MerchantContractTerms, NotificationType, PartialWalletRunConfig, PreparePayResultType, TalerCorebankApiClient, + TalerMerchantApi, TransactionMajorState, WalletNotification, } from "@gnu-taler/taler-util"; @@ -51,6 +51,7 @@ import { FakebankService, GlobalTestState, HarnessExchangeBankAccount, + LibeufinBankService, MerchantService, MerchantServiceInterface, WalletCli, @@ -60,6 +61,7 @@ import { generateRandomPayto, setupDb, setupSharedDb, + useLibeufinBank, } from "./harness.js"; import * as fs from "fs"; @@ -84,7 +86,21 @@ export interface SimpleTestEnvironment { */ export interface SimpleTestEnvironmentNg { commonDb: DbInfo; - bank: BankService; + bank: FakebankService; + exchange: ExchangeService; + exchangeBankAccount: HarnessExchangeBankAccount; + merchant: MerchantService; + walletClient: WalletClient; + walletService: WalletService; +} + +/** + * Improved version of the simple test environment, + * passing bankClient instead of bank service. + */ +export interface SimpleTestEnvironmentNg3 { + commonDb: DbInfo; + bankClient: TalerCorebankApiClient; exchange: ExchangeService; exchangeBankAccount: HarnessExchangeBankAccount; merchant: MerchantService; @@ -130,12 +146,12 @@ export async function useSharedTestkudosEnvironment(t: GlobalTestState) { if (fs.existsSync(sharedDir + "/bank.conf")) { logger.info("reusing existing bank"); - bank = BankService.fromExistingConfig(t, { + bank = FakebankService.fromExistingConfig(t, { overridePath: sharedDir, }); } else { logger.info("creating new bank config"); - bank = await BankService.create(t, { + bank = await FakebankService.create(t, { allowRegistrations: true, currency: "TESTKUDOS", database: db.connStr, @@ -294,7 +310,7 @@ export async function createSimpleTestkudosEnvironmentV2( ): Promise<SimpleTestEnvironmentNg> { const db = await setupDb(t); - const bank = await BankService.create(t, { + const bank = await FakebankService.create(t, { allowRegistrations: true, currency: "TESTKUDOS", database: db.connStr, @@ -402,6 +418,159 @@ export async function createSimpleTestkudosEnvironmentV2( }; } +/** + * Run a test case with a simple TESTKUDOS Taler environment, consisting + * of one exchange, one bank and one merchant. + * + * V3 uses the unified Corebank API and allows to choose between + * Fakebank and Libeufin-bank. + */ +export async function createSimpleTestkudosEnvironmentV3( + t: GlobalTestState, + coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")), + opts: EnvOptions = {}, +): Promise<SimpleTestEnvironmentNg3> { + const db = await setupDb(t); + + const bc = { + allowRegistrations: true, + currency: "TESTKUDOS", + database: db.connStr, + httpPort: 8082, + }; + + const bank: BankService = useLibeufinBank + ? await LibeufinBankService.create(t, bc) + : await FakebankService.create(t, bc); + + 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 receiverName = "Exchange"; + const exchangeBankUsername = "exchange"; + const exchangeBankPassword = "mypw"; + const exchangePaytoUri = generateRandomPayto(exchangeBankUsername); + const wireGatewayApiBaseUrl = new URL( + "accounts/exchange/taler-wire-gateway/", + bank.corebankApiBaseUrl, + ).href; + + const exchangeBankAccount = { + wireGatewayApiBaseUrl, + accountName: exchangeBankUsername, + accountPassword: exchangeBankPassword, + accountPaytoUri: exchangePaytoUri, + }; + + await exchange.addBankAccount("1", exchangeBankAccount); + + bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); + + if (opts.additionalBankConfig) { + opts.additionalBankConfig(bank); + } + 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, + }); + + const ageMaskSpec = opts.ageMaskSpec; + + if (ageMaskSpec) { + exchange.enableAgeRestrictions(ageMaskSpec); + // Enable age restriction for all coins. + exchange.addCoinConfigList( + coinConfig.map((x) => ({ + ...x, + name: `${x.name}-age`, + ageRestricted: true, + })), + ); + // For mixed age restrictions, we also offer coins without age restrictions + if (opts.mixedAgeRestriction) { + exchange.addCoinConfigList( + coinConfig.map((x) => ({ ...x, ageRestricted: false })), + ); + } + } else { + exchange.addCoinConfigList(coinConfig); + } + + if (opts.additionalExchangeConfig) { + opts.additionalExchangeConfig(exchange); + } + await exchange.start(); + await exchange.pingUntilAvailable(); + + merchant.addExchange(exchange); + + if (opts.additionalMerchantConfig) { + opts.additionalMerchantConfig(merchant); + } + await merchant.start(); + await merchant.pingUntilAvailable(); + + await merchant.addInstanceWithWireAccount({ + id: "default", + name: "Default Instance", + paytoUris: [generateRandomPayto("merchant-default")], + defaultWireTransferDelay: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 1 }), + ), + }); + + await merchant.addInstanceWithWireAccount({ + id: "minst1", + name: "minst1", + paytoUris: [generateRandomPayto("minst1")], + defaultWireTransferDelay: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 1 }), + ), + }); + + const { walletClient, walletService } = await createWalletDaemonWithClient( + t, + { name: "wallet", persistent: true }, + ); + + console.log("setup done!"); + + return { + commonDb: db, + exchange, + merchant, + walletClient, + walletService, + bankClient, + exchangeBankAccount, + }; +} + export interface CreateWalletArgs { handleNotification?(wn: WalletNotification): void; name: string; @@ -457,7 +626,7 @@ export async function createWalletDaemonWithClient( export interface FaultyMerchantTestEnvironment { commonDb: DbInfo; - bank: BankService; + bank: FakebankService; exchange: ExchangeService; faultyExchange: FaultInjectedExchangeService; exchangeBankAccount: HarnessExchangeBankAccount; @@ -466,6 +635,16 @@ export interface FaultyMerchantTestEnvironment { walletClient: WalletClient; } +export interface FaultyMerchantTestEnvironmentNg { + commonDb: DbInfo; + bankClient: TalerCorebankApiClient; + exchange: ExchangeService; + faultyExchange: FaultInjectedExchangeService; + merchant: MerchantService; + faultyMerchant: FaultInjectedMerchantService; + walletClient: WalletClient; +} + /** * Run a test case with a simple TESTKUDOS Taler environment, consisting * of one exchange, one bank and one merchant. @@ -475,7 +654,7 @@ export async function createFaultInjectedMerchantTestkudosEnvironment( ): Promise<FaultyMerchantTestEnvironment> { const db = await setupDb(t); - const bank = await BankService.create(t, { + const bank = await FakebankService.create(t, { allowRegistrations: true, currency: "TESTKUDOS", database: db.connStr, @@ -621,6 +800,70 @@ export async function withdrawViaBankV2( }; } +/** + * Withdraw via a bank with the testing API enabled. + * Uses the new Corebank API. + */ +export async function withdrawViaBankV3( + t: GlobalTestState, + p: { + walletClient: WalletClient; + bankClient: TalerCorebankApiClient; + exchange: ExchangeServiceInterface; + amount: AmountString | string; + restrictAge?: number; + }, +): Promise<WithdrawViaBankResult> { + const { walletClient: wallet, bankClient, exchange, amount } = p; + + const user = await bankClient.createRandomBankUser(); + const bankClient2 = new TalerCorebankApiClient(bankClient.baseUrl); + bankClient2.setAuth({ + username: user.username, + password: user.password, + }); + + const wop = await bankClient2.createWithdrawalOperation( + user.username, + amount, + ); + + // Hand it to the wallet + + await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, { + talerWithdrawUri: wop.taler_withdraw_uri, + restrictAge: p.restrictAge, + }); + + // Withdraw (AKA select) + + const acceptRes = await wallet.client.call( + WalletApiOperation.AcceptBankIntegratedWithdrawal, + { + exchangeBaseUrl: exchange.baseUrl, + talerWithdrawUri: wop.taler_withdraw_uri, + restrictAge: p.restrictAge, + }, + ); + + const withdrawalFinishedCond = wallet.waitForNotificationCond( + (x) => + x.type === NotificationType.TransactionStateTransition && + x.newTxState.major === TransactionMajorState.Done && + x.transactionId === acceptRes.transactionId, + ); + + // Confirm it + + await bankClient2.confirmWithdrawalOperation(user.username, { + withdrawalOperationId: wop.withdrawal_id, + }); + + return { + withdrawalFinishedCond, + }; +} + export async function applyTimeTravelV2( timetravelOffsetMs: number, s: { @@ -658,7 +901,7 @@ export async function makeTestPaymentV2( args: { merchant: MerchantServiceInterface; walletClient: WalletClient; - order: Partial<MerchantContractTerms>; + order: TalerMerchantApi.Order; instance?: string; }, auth: WithAuthorization = {}, diff --git a/packages/taler-harness/src/index.ts b/packages/taler-harness/src/index.ts @@ -900,6 +900,9 @@ deploymentCli currency, summary: "Pay me!", }, + editable_defaults: { + amount: currency, + }, }, ); if (resp.type === "fail") { @@ -915,9 +918,6 @@ deploymentCli templateURI = stringifyPayTemplateUri({ merchantBaseUrl: instanceURL, templateId: "default", - templateParams: { - amount: currency, - }, }); } diff --git a/packages/taler-harness/src/integrationtests/test-age-restrictions-deposit.ts b/packages/taler-harness/src/integrationtests/test-age-restrictions-deposit.ts @@ -26,8 +26,8 @@ import { import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState, generateRandomPayto } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, - withdrawViaBankV2, + createSimpleTestkudosEnvironmentV3, + withdrawViaBankV3, } from "../harness/helpers.js"; import { defaultCoinConfig } from "../harness/denomStructures.js"; @@ -37,8 +37,8 @@ import { defaultCoinConfig } from "../harness/denomStructures.js"; export async function runAgeRestrictionsDepositTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bank, exchange } = - await createSimpleTestkudosEnvironmentV2( + const { walletClient, bankClient, exchange } = + await createSimpleTestkudosEnvironmentV3( t, defaultCoinConfig.map((x) => x("TESTKUDOS")), { @@ -48,9 +48,9 @@ export async function runAgeRestrictionsDepositTest(t: GlobalTestState) { // Withdraw digital cash into the wallet. - const withdrawalResult = await withdrawViaBankV2(t, { + const withdrawalResult = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); diff --git a/packages/taler-harness/src/integrationtests/test-age-restrictions-merchant.ts b/packages/taler-harness/src/integrationtests/test-age-restrictions-merchant.ts @@ -17,15 +17,15 @@ /** * Imports. */ -import { AmountString, MerchantApiClient } from "@gnu-taler/taler-util"; +import { AmountString, TalerMerchantApi } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { defaultCoinConfig } from "../harness/denomStructures.js"; import { GlobalTestState } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, + createSimpleTestkudosEnvironmentV3, createWalletDaemonWithClient, makeTestPaymentV2, - withdrawViaBankV2, + withdrawViaBankV3, } from "../harness/helpers.js"; /** @@ -36,11 +36,11 @@ export async function runAgeRestrictionsMerchantTest(t: GlobalTestState) { const { walletClient: walletClientOne, - bank, + bankClient, exchange, merchant, exchangeBankAccount, - } = await createSimpleTestkudosEnvironmentV2( + } = await createSimpleTestkudosEnvironmentV3( t, defaultCoinConfig.map((x) => x("TESTKUDOS")), { @@ -66,16 +66,16 @@ export async function runAgeRestrictionsMerchantTest(t: GlobalTestState) { name: "w0", }); - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient: walletClientZero, - bank, + bankClient, exchange, amount: "TESTKUDOS:20" as AmountString, restrictAge: 13, }); await wres.withdrawalFinishedCond; - const order = { + const order: TalerMerchantApi.Order = { summary: "Buy me!", amount: "TESTKUDOS:5", fulfillment_url: "taler://fulfillment-success/thx", @@ -96,16 +96,16 @@ export async function runAgeRestrictionsMerchantTest(t: GlobalTestState) { { const walletClient = walletClientOne; - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, - amount: "TESTKUDOS:20" as AmountString, + amount: "TESTKUDOS:20", restrictAge: 13, }); await wres.withdrawalFinishedCond; - const order = { + const order: TalerMerchantApi.Order = { summary: "Buy me!", amount: "TESTKUDOS:5", fulfillment_url: "taler://fulfillment-success/thx", @@ -122,16 +122,16 @@ export async function runAgeRestrictionsMerchantTest(t: GlobalTestState) { { const walletClient = walletClientTwo; - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:20" as AmountString, restrictAge: 13, }); await wres.withdrawalFinishedCond; - const order = { + const order: TalerMerchantApi.Order = { summary: "Buy me!", amount: "TESTKUDOS:5", fulfillment_url: "taler://fulfillment-success/thx", @@ -147,15 +147,15 @@ export async function runAgeRestrictionsMerchantTest(t: GlobalTestState) { { const walletClient = walletClientThree; - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:20" as AmountString, }); await wres.withdrawalFinishedCond; - const order = { + const order: TalerMerchantApi.Order = { summary: "Buy me!", amount: "TESTKUDOS:5", fulfillment_url: "taler://fulfillment-success/thx", diff --git a/packages/taler-harness/src/integrationtests/test-age-restrictions-mixed-merchant.ts b/packages/taler-harness/src/integrationtests/test-age-restrictions-mixed-merchant.ts @@ -17,16 +17,16 @@ /** * Imports. */ +import { AmountString, TalerMerchantApi } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { defaultCoinConfig } from "../harness/denomStructures.js"; import { GlobalTestState } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, + createSimpleTestkudosEnvironmentV3, createWalletDaemonWithClient, makeTestPaymentV2, - withdrawViaBankV2, + withdrawViaBankV3, } from "../harness/helpers.js"; -import { AmountString } from "@gnu-taler/taler-util"; /** * Run test for basic, bank-integrated withdrawal and payment. @@ -36,10 +36,10 @@ export async function runAgeRestrictionsMixedMerchantTest(t: GlobalTestState) { const { walletClient: walletOne, - bank, + bankClient, exchange, merchant, - } = await createSimpleTestkudosEnvironmentV2( + } = await createSimpleTestkudosEnvironmentV3( t, defaultCoinConfig.map((x) => x("TESTKUDOS")), { @@ -59,9 +59,9 @@ export async function runAgeRestrictionsMixedMerchantTest(t: GlobalTestState) { { const walletClient = walletOne; - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:20" as AmountString, restrictAge: 13, @@ -84,15 +84,14 @@ export async function runAgeRestrictionsMixedMerchantTest(t: GlobalTestState) { } { - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient: walletTwo, - bank, + bankClient, exchange, amount: "TESTKUDOS:20" as AmountString, restrictAge: 13, }); - await wres.withdrawalFinishedCond; const order = { @@ -106,17 +105,16 @@ export async function runAgeRestrictionsMixedMerchantTest(t: GlobalTestState) { } { - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient: walletThree, - bank, + bankClient, exchange, - amount: "TESTKUDOS:20" as AmountString, + amount: "TESTKUDOS:20", }); - await wres.withdrawalFinishedCond; - const order = { + const order: TalerMerchantApi.Order = { summary: "Buy me!", amount: "TESTKUDOS:5", fulfillment_url: "taler://fulfillment-success/thx", diff --git a/packages/taler-harness/src/integrationtests/test-age-restrictions-peer.ts b/packages/taler-harness/src/integrationtests/test-age-restrictions-peer.ts @@ -22,7 +22,6 @@ import { AmountString, Duration, NotificationType, - TalerUriAction, TransactionMajorState, TransactionMinorState, TransactionType, @@ -31,9 +30,9 @@ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { defaultCoinConfig } from "../harness/denomStructures.js"; import { GlobalTestState } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, + createSimpleTestkudosEnvironmentV3, createWalletDaemonWithClient, - withdrawViaBankV2, + withdrawViaBankV3, } from "../harness/helpers.js"; /** @@ -42,7 +41,7 @@ import { export async function runAgeRestrictionsPeerTest(t: GlobalTestState) { // Set up test environment - const { bank, exchange } = await createSimpleTestkudosEnvironmentV2( + const { bankClient, exchange } = await createSimpleTestkudosEnvironmentV3( t, defaultCoinConfig.map((x) => x("TESTKUDOS")), { @@ -63,9 +62,9 @@ export async function runAgeRestrictionsPeerTest(t: GlobalTestState) { const wallet2 = w2.walletClient; { - const withdrawalRes = await withdrawViaBankV2(t, { + const withdrawalRes = await withdrawViaBankV3(t, { walletClient: wallet1, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", restrictAge: 13, diff --git a/packages/taler-harness/src/integrationtests/test-bank-api.ts b/packages/taler-harness/src/integrationtests/test-bank-api.ts @@ -63,13 +63,20 @@ export async function runBankApiTest(t: GlobalTestState) { database: db.connStr, }); - const exchangeBankAccount = await bank.createExchangeAccount( - "myexchange", - "x", - ); - exchange.addBankAccount("1", exchangeBankAccount); + let receiverName = "Exchange"; + let exchangeBankUsername = "exchange"; + let exchangeBankPassword = "mypw"; + let exchangePaytoUri = generateRandomPayto(exchangeBankUsername); + + let wireGatewayApiBaseUrl = new URL("accounts/exchange/taler-wire-gateway/", bank.baseUrl).href; + await exchange.addBankAccount("1", { + accountName: exchangeBankUsername, + accountPassword: exchangeBankPassword, + wireGatewayApiBaseUrl, + accountPaytoUri: exchangePaytoUri, + }); - bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); + bank.setSuggestedExchange(exchange, exchangePaytoUri); await bank.start(); @@ -99,7 +106,20 @@ export async function runBankApiTest(t: GlobalTestState) { console.log("setup done!"); - const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl); + 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, + }); const bankUser = await bankClient.registerAccount("user1", "pw1"); @@ -124,11 +144,11 @@ export async function runBankApiTest(t: GlobalTestState) { const res = createEddsaKeyPair(); const wireGatewayApiClient = new WireGatewayApiClient( - exchangeBankAccount.wireGatewayApiBaseUrl, + wireGatewayApiBaseUrl, { auth: { - username: exchangeBankAccount.accountName, - password: exchangeBankAccount.accountPassword, + username: "admin", + password: "adminpw", }, }, ); @@ -146,4 +166,4 @@ export async function runBankApiTest(t: GlobalTestState) { ); } -runBankApiTest.suites = ["fakebank"] -\ No newline at end of file +runBankApiTest.suites = ["fakebank"] diff --git a/packages/taler-harness/src/integrationtests/test-claim-loop.ts b/packages/taler-harness/src/integrationtests/test-claim-loop.ts @@ -21,8 +21,8 @@ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { URL } from "url"; import { GlobalTestState } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, - withdrawViaBankV2, + createSimpleTestkudosEnvironmentV3, + withdrawViaBankV3, } from "../harness/helpers.js"; import { MerchantApiClient } from "@gnu-taler/taler-util"; @@ -35,12 +35,12 @@ import { MerchantApiClient } from "@gnu-taler/taler-util"; export async function runClaimLoopTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bank, exchange, merchant } = - await createSimpleTestkudosEnvironmentV2(t); + const { walletClient, bankClient, exchange, merchant } = + await createSimpleTestkudosEnvironmentV3(t); - await withdrawViaBankV2(t, { + await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); @@ -79,4 +79,4 @@ export async function runClaimLoopTest(t: GlobalTestState) { await t.shutdown(); } -runClaimLoopTest.suites = ["merchant"]; -\ No newline at end of file +runClaimLoopTest.suites = ["merchant"]; diff --git a/packages/taler-harness/src/integrationtests/test-clause-schnorr.ts b/packages/taler-harness/src/integrationtests/test-clause-schnorr.ts @@ -17,13 +17,14 @@ /** * Imports. */ +import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; import { GlobalTestState } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, + createSimpleTestkudosEnvironmentV3, makeTestPaymentV2, - withdrawViaBankV2, + withdrawViaBankV3, } from "../harness/helpers.js"; /** @@ -54,20 +55,20 @@ export async function runClauseSchnorrTest(t: GlobalTestState) { name: "rsa_dummy", }); - const { walletClient, bank, exchange, merchant } = - await createSimpleTestkudosEnvironmentV2(t, coinConfig); + const { walletClient, bankClient, exchange, merchant } = + await createSimpleTestkudosEnvironmentV3(t, coinConfig); // Withdraw digital cash into the wallet. - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); await wres.withdrawalFinishedCond; - const order = { + const order: TalerMerchantApi.Order = { summary: "Buy me!", amount: "TESTKUDOS:5", fulfillment_url: "taler://fulfillment-success/thx", @@ -78,7 +79,7 @@ export async function runClauseSchnorrTest(t: GlobalTestState) { // Test JSON normalization of contract terms: Does the wallet // agree with the merchant? - const order2 = { + const order2: TalerMerchantApi.Order = { summary: "Testing “unicode” characters", amount: "TESTKUDOS:5", fulfillment_url: "taler://fulfillment-success/thx", @@ -89,7 +90,7 @@ export async function runClauseSchnorrTest(t: GlobalTestState) { // Test JSON normalization of contract terms: Does the wallet // agree with the merchant? - const order3 = { + const order3: TalerMerchantApi.Order = { summary: "Testing\nNewlines\rAnd\tStuff\nHere\b", amount: "TESTKUDOS:5", fulfillment_url: "taler://fulfillment-success/thx", diff --git a/packages/taler-harness/src/integrationtests/test-currency-scope.ts b/packages/taler-harness/src/integrationtests/test-currency-scope.ts @@ -21,8 +21,8 @@ import { Duration, 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, MerchantService, generateRandomPayto, @@ -44,7 +44,7 @@ export async function runCurrencyScopeTest(t: GlobalTestState) { nameSuffix: "exchange2", }); - const bank = await BankService.create(t, { + const bank = await FakebankService.create(t, { allowRegistrations: true, currency: "TESTKUDOS", database: dbDefault.connStr, diff --git a/packages/taler-harness/src/integrationtests/test-denom-lost.ts b/packages/taler-harness/src/integrationtests/test-denom-lost.ts @@ -20,8 +20,8 @@ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, - withdrawViaBankV2, + createSimpleTestkudosEnvironmentV3, + withdrawViaBankV3, } from "../harness/helpers.js"; /** @@ -30,14 +30,14 @@ import { export async function runDenomLostTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bank, exchange, merchant } = - await createSimpleTestkudosEnvironmentV2(t); + const { walletClient, bankClient, exchange } = + await createSimpleTestkudosEnvironmentV3(t); // Withdraw digital cash into the wallet. - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); diff --git a/packages/taler-harness/src/integrationtests/test-denom-unoffered.ts b/packages/taler-harness/src/integrationtests/test-denom-unoffered.ts @@ -21,26 +21,27 @@ import { MerchantApiClient, PreparePayResultType, TalerErrorCode, + TalerMerchantApi, TransactionType, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, - withdrawViaBankV2, + createSimpleTestkudosEnvironmentV3, + withdrawViaBankV3, } from "../harness/helpers.js"; export async function runDenomUnofferedTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bank, exchange, merchant } = - await createSimpleTestkudosEnvironmentV2(t); + const { walletClient, bankClient, exchange, merchant } = + await createSimpleTestkudosEnvironmentV3(t); // Withdraw digital cash into the wallet. - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); @@ -62,7 +63,7 @@ export async function runDenomUnofferedTest(t: GlobalTestState) { await merchant.start(); await merchant.pingUntilAvailable(); - const order = { + const order: TalerMerchantApi.Order = { summary: "Buy me!", amount: "TESTKUDOS:5", fulfillment_url: "taler://fulfillment-success/thx", @@ -139,9 +140,9 @@ export async function runDenomUnofferedTest(t: GlobalTestState) { }); // Now withdrawal should work again. - await withdrawViaBankV2(t, { + await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); diff --git a/packages/taler-harness/src/integrationtests/test-deposit.ts b/packages/taler-harness/src/integrationtests/test-deposit.ts @@ -27,8 +27,8 @@ import { import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState, generateRandomPayto } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, - withdrawViaBankV2, + createSimpleTestkudosEnvironmentV3, + withdrawViaBankV3, } from "../harness/helpers.js"; /** @@ -37,14 +37,14 @@ import { export async function runDepositTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bank, exchange } = - await createSimpleTestkudosEnvironmentV2(t); + const { walletClient, bankClient, exchange } = + await createSimpleTestkudosEnvironmentV3(t); // Withdraw digital cash into the wallet. - const withdrawalResult = await withdrawViaBankV2(t, { + const withdrawalResult = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); diff --git a/packages/taler-harness/src/integrationtests/test-exchange-management-fault.ts b/packages/taler-harness/src/integrationtests/test-exchange-management-fault.ts @@ -31,7 +31,7 @@ import { FaultInjectionResponseContext, } from "../harness/faultInjection.js"; import { - BankService, + BankService, ExchangeService, GlobalTestState, MerchantService, @@ -71,11 +71,17 @@ export async function runExchangeManagementFaultTest( database: db.connStr, }); - const exchangeBankAccount = await bank.createExchangeAccount( - "myexchange", - "x", - ); - exchange.addBankAccount("1", exchangeBankAccount); + 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, + }); const faultyExchange = new FaultInjectedExchangeService(t, exchange, 8091); // Base URL must contain port that the proxy is listening on. @@ -85,7 +91,7 @@ export async function runExchangeManagementFaultTest( bank.setSuggestedExchange( faultyExchange, - exchangeBankAccount.accountPaytoUri, + exchangePaytoUri, ); await bank.start(); @@ -262,9 +268,19 @@ export async function runExchangeManagementFaultTest( // Create withdrawal operation - const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl); + const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, { + auth: { + username: "admin", + password: "adminpw", + }, + }); const user = await bankClient.createRandomBankUser(); + bankClient.setAuth({ + username: user.username, + password: user.password, + }); + const wop = await bankClient.createWithdrawalOperation( user.username, "TESTKUDOS:10", diff --git a/packages/taler-harness/src/integrationtests/test-exchange-management.ts b/packages/taler-harness/src/integrationtests/test-exchange-management.ts @@ -19,7 +19,7 @@ */ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState } from "../harness/harness.js"; -import { createSimpleTestkudosEnvironmentV2 } from "../harness/helpers.js"; +import { createSimpleTestkudosEnvironmentV3 } from "../harness/helpers.js"; /** * Test if the wallet handles outdated exchange versions correctly. @@ -30,7 +30,7 @@ export async function runExchangeManagementTest( // Set up test environment const { walletClient, exchange } = - await createSimpleTestkudosEnvironmentV2(t); + await createSimpleTestkudosEnvironmentV3(t); // Since the default exchanges can change, we start the wallet in tests // with no built-in defaults. Thus the list of exchanges is empty here. diff --git a/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts b/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts @@ -25,6 +25,7 @@ import { Duration, ExchangeKeysJson, Logger, + TalerCorebankApiClient, } from "@gnu-taler/taler-util"; import { createPlatformHttpLib, @@ -32,7 +33,7 @@ import { } from "@gnu-taler/taler-util/http"; import { makeNoFeeCoinConfig } from "../harness/denomStructures.js"; import { - BankService, + BankService, ExchangeService, generateRandomPayto, GlobalTestState, @@ -42,7 +43,7 @@ import { import { applyTimeTravelV2, createWalletDaemonWithClient, - withdrawViaBankV2, + withdrawViaBankV3, } from "../harness/helpers.js"; const logger = new Logger("test-exchange-timetravel.ts"); @@ -124,18 +125,39 @@ export async function runExchangeTimetravelTest(t: GlobalTestState) { database: db.connStr, }); - const exchangeBankAccount = await bank.createExchangeAccount( - "myexchange", - "x", - ); - exchange.addBankAccount("1", exchangeBankAccount); + 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, exchangeBankAccount.accountPaytoUri); + 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(); @@ -166,9 +188,9 @@ export async function runExchangeTimetravelTest(t: GlobalTestState) { // Withdraw digital cash into the wallet. - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:15", }); diff --git a/packages/taler-harness/src/integrationtests/test-fee-regression.ts b/packages/taler-harness/src/integrationtests/test-fee-regression.ts @@ -17,6 +17,10 @@ /** * Imports. */ +import { + TalerCorebankApiClient, + TalerMerchantApi, +} from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { BankService, @@ -27,10 +31,10 @@ import { setupDb, } from "../harness/harness.js"; import { - SimpleTestEnvironmentNg, + SimpleTestEnvironmentNg3, createWalletDaemonWithClient, makeTestPaymentV2, - withdrawViaBankV2, + withdrawViaBankV3, } from "../harness/helpers.js"; /** @@ -39,7 +43,7 @@ import { */ export async function createMyTestkudosEnvironment( t: GlobalTestState, -): Promise<SimpleTestEnvironmentNg> { +): Promise<SimpleTestEnvironmentNg3> { const db = await setupDb(t); const bank = await BankService.create(t, { @@ -63,18 +67,42 @@ export async function createMyTestkudosEnvironment( database: db.connStr, }); - const exchangeBankAccount = await bank.createExchangeAccount( - "myexchange", - "x", - ); - exchange.addBankAccount("1", exchangeBankAccount); + 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, exchangeBankAccount.accountPaytoUri); + 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, + }); + const coinCommon = { cipher: "RSA" as const, durationLegal: "3 years", @@ -160,8 +188,13 @@ export async function createMyTestkudosEnvironment( merchant, walletClient, walletService, - bank, - exchangeBankAccount, + bankClient, + exchangeBankAccount: { + accountName: "", + accountPassword: "", + accountPaytoUri: "", + wireGatewayApiBaseUrl: "", + }, }; } @@ -171,14 +204,14 @@ export async function createMyTestkudosEnvironment( export async function runFeeRegressionTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bank, exchange, merchant } = + const { walletClient, bankClient, exchange, merchant } = await createMyTestkudosEnvironment(t); // Withdraw digital cash into the wallet. - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:1.92", }); @@ -190,7 +223,7 @@ export async function runFeeRegressionTest(t: GlobalTestState) { // Make sure we really withdraw one 0.64 and one 1.28 coin. t.assertTrue(coins.coins.length === 2); - const order = { + const order: TalerMerchantApi.Order = { summary: "Buy me!", amount: "TESTKUDOS:1.30", fulfillment_url: "taler://fulfillment-success/thx", diff --git a/packages/taler-harness/src/integrationtests/test-kyc.ts b/packages/taler-harness/src/integrationtests/test-kyc.ts @@ -32,7 +32,7 @@ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import * as http from "node:http"; import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; import { - BankService, + BankService, ExchangeService, GlobalTestState, MerchantService, @@ -41,7 +41,7 @@ import { generateRandomPayto, setupDb, } from "../harness/harness.js"; -import { EnvOptions, SimpleTestEnvironmentNg } from "../harness/helpers.js"; +import { EnvOptions, SimpleTestEnvironmentNg3 } from "../harness/helpers.js"; const logger = new Logger("test-kyc.ts"); @@ -49,7 +49,7 @@ export async function createKycTestkudosEnvironment( t: GlobalTestState, coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")), opts: EnvOptions = {}, -): Promise<SimpleTestEnvironmentNg> { +): Promise<SimpleTestEnvironmentNg3> { const db = await setupDb(t); const bank = await BankService.create(t, { @@ -73,18 +73,39 @@ export async function createKycTestkudosEnvironment( database: db.connStr, }); - const exchangeBankAccount = await bank.createExchangeAccount( - "myexchange", - "x", - ); - exchange.addBankAccount("1", exchangeBankAccount); + 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, exchangeBankAccount.accountPaytoUri); + 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, + }); + const ageMaskSpec = opts.ageMaskSpec; if (ageMaskSpec) { @@ -213,8 +234,13 @@ export async function createKycTestkudosEnvironment( merchant, walletClient, walletService, - bank, - exchangeBankAccount, + bankClient, + exchangeBankAccount: { + accountName: '', + accountPassword: '', + accountPaytoUri: '', + wireGatewayApiBaseUrl: '', + }, }; } @@ -310,17 +336,20 @@ async function runTestfakeKycService(): Promise<TestfakeKycService> { export async function runKycTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bank, exchange, merchant } = + const { walletClient, bankClient, exchange, merchant } = await createKycTestkudosEnvironment(t); const kycServer = await runTestfakeKycService(); // Withdraw digital cash into the wallet. - const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl); - const amount = "TESTKUDOS:20"; const user = await bankClient.createRandomBankUser(); + bankClient.setAuth({ + username: user.username, + password: user.password, + }); + const wop = await bankClient.createWithdrawalOperation(user.username, amount); // Hand it to the wallet diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-bank.ts b/packages/taler-harness/src/integrationtests/test-libeufin-bank.ts @@ -72,11 +72,9 @@ export async function runLibeufinBankTest(t: GlobalTestState) { database: db.connStr, }); - const exchangeIban = generateRandomTestIban(); const exchangeBankUsername = "exchange"; const exchangeBankPw = "mypw"; - const exchangePlainPayto = `payto://iban/${exchangeIban}`; - const exchangeExtendedPayto = `payto://iban/${exchangeIban}?receiver-name=Exchange`; + const exchangePayto = generateRandomPayto(exchangeBankUsername); const wireGatewayApiBaseUrl = new URL( "accounts/exchange/taler-wire-gateway/", bank.baseUrl, @@ -88,7 +86,7 @@ export async function runLibeufinBankTest(t: GlobalTestState) { wireGatewayApiBaseUrl, accountName: exchangeBankUsername, accountPassword: exchangeBankPw, - accountPaytoUri: exchangeExtendedPayto, + accountPaytoUri: exchangePayto, }); bank.setSuggestedExchange(exchange); @@ -138,7 +136,7 @@ export async function runLibeufinBankTest(t: GlobalTestState) { password: exchangeBankPw, username: exchangeBankUsername, is_taler_exchange: true, - payto_uri: exchangePlainPayto, + payto_uri: exchangePayto, }); const bankUser = await bankClient.registerAccount("user1", "pw1"); diff --git a/packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts b/packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts @@ -22,6 +22,7 @@ import { ConfirmPayResultType, MerchantApiClient, PreparePayResultType, + TalerCorebankApiClient, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { URL } from "url"; @@ -31,7 +32,7 @@ import { FaultInjectedMerchantService, } from "../harness/faultInjection.js"; import { - BankService, + BankService, ExchangeService, generateRandomPayto, GlobalTestState, @@ -41,8 +42,8 @@ import { } from "../harness/harness.js"; import { createWalletDaemonWithClient, - FaultyMerchantTestEnvironment, - withdrawViaBankV2, + FaultyMerchantTestEnvironmentNg, + withdrawViaBankV3, } from "../harness/helpers.js"; /** @@ -51,7 +52,7 @@ import { */ export async function createConfusedMerchantTestkudosEnvironment( t: GlobalTestState, -): Promise<FaultyMerchantTestEnvironment> { +): Promise<FaultyMerchantTestEnvironmentNg> { const db = await setupDb(t); const bank = await BankService.create(t, { @@ -83,21 +84,39 @@ export async function createConfusedMerchantTestkudosEnvironment( config.setString("exchange", "base_url", "http://localhost:9081/"); }); - const exchangeBankAccount = await bank.createExchangeAccount( - "myexchange", - "x", - ); - exchange.addBankAccount("1", exchangeBankAccount); + let receiverName = "Exchange"; + let exchangeBankUsername = "exchange"; + let exchangeBankPassword = "mypw"; + let exchangePaytoUri = generateRandomPayto(exchangeBankUsername); - bank.setSuggestedExchange( - faultyExchange, - exchangeBankAccount.accountPaytoUri, - ); + 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.addOfferedCoins(defaultCoinConfig); await exchange.start(); @@ -132,8 +151,7 @@ export async function createConfusedMerchantTestkudosEnvironment( exchange, merchant, walletClient, - bank, - exchangeBankAccount, + bankClient, faultyMerchant, faultyExchange, }; @@ -146,14 +164,14 @@ export async function createConfusedMerchantTestkudosEnvironment( export async function runMerchantExchangeConfusionTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bank, faultyExchange, faultyMerchant } = + const { walletClient, bankClient, faultyExchange, faultyMerchant } = await createConfusedMerchantTestkudosEnvironment(t); // Withdraw digital cash into the wallet. - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange: faultyExchange, amount: "TESTKUDOS:20", }); diff --git a/packages/taler-harness/src/integrationtests/test-merchant-longpolling.ts b/packages/taler-harness/src/integrationtests/test-merchant-longpolling.ts @@ -27,8 +27,8 @@ import { import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState, harnessHttpLib } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, - withdrawViaBankV2, + createSimpleTestkudosEnvironmentV3, + withdrawViaBankV3, } from "../harness/helpers.js"; /** @@ -36,14 +36,14 @@ import { */ export async function runMerchantLongpollingTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bank, exchange, merchant } = - await createSimpleTestkudosEnvironmentV2(t); + const { walletClient, bankClient, exchange, merchant } = + await createSimpleTestkudosEnvironmentV3(t); // Withdraw digital cash into the wallet. - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); diff --git a/packages/taler-harness/src/integrationtests/test-merchant-refund-api.ts b/packages/taler-harness/src/integrationtests/test-merchant-refund-api.ts @@ -25,7 +25,6 @@ import { } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { - BankServiceHandle, ExchangeServiceInterface, GlobalTestState, MerchantServiceInterface, @@ -33,15 +32,14 @@ import { harnessHttpLib, } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, - withdrawViaBankV2, + createSimpleTestkudosEnvironmentV3, + withdrawViaBankV3, } from "../harness/helpers.js"; async function testRefundApiWithFulfillmentUrl( t: GlobalTestState, env: { merchant: MerchantServiceInterface; - bank: BankServiceHandle; walletClient: WalletClient; exchange: ExchangeServiceInterface; }, @@ -157,7 +155,6 @@ async function testRefundApiWithFulfillmentMessage( t: GlobalTestState, env: { merchant: MerchantServiceInterface; - bank: BankServiceHandle; walletClient: WalletClient; exchange: ExchangeServiceInterface; }, @@ -276,14 +273,14 @@ async function testRefundApiWithFulfillmentMessage( export async function runMerchantRefundApiTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bank, exchange, merchant } = - await createSimpleTestkudosEnvironmentV2(t); + const { walletClient, bankClient, exchange, merchant } = + await createSimpleTestkudosEnvironmentV3(t); // Withdraw digital cash into the wallet. - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); @@ -291,14 +288,12 @@ export async function runMerchantRefundApiTest(t: GlobalTestState) { await testRefundApiWithFulfillmentUrl(t, { walletClient, - bank, exchange, merchant, }); await testRefundApiWithFulfillmentMessage(t, { walletClient, - bank, exchange, merchant, }); diff --git a/packages/taler-harness/src/integrationtests/test-merchant-spec-public-orders.ts b/packages/taler-harness/src/integrationtests/test-merchant-spec-public-orders.ts @@ -21,28 +21,28 @@ import { ConfirmPayResultType, MerchantApiClient, PreparePayResultType, + TalerCorebankApiClient, URL, encodeCrock, getRandomBytes, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { - BankService, ExchangeService, GlobalTestState, MerchantService, harnessHttpLib, } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, + createSimpleTestkudosEnvironmentV3, createWalletDaemonWithClient, - withdrawViaBankV2, + withdrawViaBankV3, } from "../harness/helpers.js"; interface Context { merchant: MerchantService; merchantBaseUrl: string; - bank: BankService; + bankClient: TalerCorebankApiClient; exchange: ExchangeService; } @@ -55,11 +55,11 @@ async function testWithClaimToken( const { walletClient } = await createWalletDaemonWithClient(t, { name: "wct", }); - const { bank, exchange } = c; + const { bankClient, exchange } = c; const { merchant, merchantBaseUrl } = c; - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); @@ -317,12 +317,12 @@ async function testWithoutClaimToken( name: "wnoct", }); const sessionId = "mysession2"; - const { bank, exchange } = c; + const { bankClient, exchange } = c; const { merchant, merchantBaseUrl } = c; const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); @@ -573,8 +573,8 @@ async function testWithoutClaimToken( * specification of the endpoint. */ export async function runMerchantSpecPublicOrdersTest(t: GlobalTestState) { - const { bank, exchange, merchant } = - await createSimpleTestkudosEnvironmentV2(t); + const { bankClient, exchange, merchant } = + await createSimpleTestkudosEnvironmentV3(t); // Base URL for the default instance. const merchantBaseUrl = merchant.makeInstanceBaseUrl(); @@ -617,14 +617,14 @@ export async function runMerchantSpecPublicOrdersTest(t: GlobalTestState) { merchant, merchantBaseUrl, exchange, - bank, + bankClient, }); await testWithoutClaimToken(t, { merchant, merchantBaseUrl, exchange, - bank, + bankClient, }); } diff --git a/packages/taler-harness/src/integrationtests/test-multiexchange.ts b/packages/taler-harness/src/integrationtests/test-multiexchange.ts @@ -17,12 +17,12 @@ /** * Imports. */ -import { Duration } from "@gnu-taler/taler-util"; +import { Duration, 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, MerchantService, generateRandomPayto, @@ -45,7 +45,7 @@ export async function runMultiExchangeTest(t: GlobalTestState) { nameSuffix: "exchange2", }); - const bank = await BankService.create(t, { + const bank = await FakebankService.create(t, { allowRegistrations: true, currency: "TESTKUDOS", database: dbDefault.connStr, @@ -157,7 +157,7 @@ export async function runMultiExchangeTest(t: GlobalTestState) { await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); - const order = { + const order: TalerMerchantApi.Order = { summary: "Buy me!", amount: "TESTKUDOS:10", fulfillment_url: "taler://fulfillment-success/thx", diff --git a/packages/taler-harness/src/integrationtests/test-otp.ts b/packages/taler-harness/src/integrationtests/test-otp.ts @@ -30,8 +30,8 @@ import { import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, - withdrawViaBankV2, + createSimpleTestkudosEnvironmentV3, + withdrawViaBankV3, } from "../harness/helpers.js"; /** @@ -40,8 +40,8 @@ import { export async function runOtpTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bank, exchange, merchant } = - await createSimpleTestkudosEnvironmentV2(t); + const { walletClient, bankClient, exchange, merchant } = + await createSimpleTestkudosEnvironmentV3(t); const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); const createOtpRes = await merchantClient.createOtpDevice({ @@ -69,12 +69,12 @@ export async function runOtpTest(t: GlobalTestState) { const getTemplateResp = await merchantClient.getTemplate("tpl1"); narrowOpSuccessOrThrow("getTemplate", getTemplateResp); - + console.log(`template: ${j2s(getTemplateResp.body)}`); - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); diff --git a/packages/taler-harness/src/integrationtests/test-payment-claim.ts b/packages/taler-harness/src/integrationtests/test-payment-claim.ts @@ -25,9 +25,9 @@ import { import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, + createSimpleTestkudosEnvironmentV3, createWalletDaemonWithClient, - withdrawViaBankV2, + withdrawViaBankV3, } from "../harness/helpers.js"; /** @@ -36,8 +36,8 @@ import { export async function runPaymentClaimTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bank, exchange, merchant } = - await createSimpleTestkudosEnvironmentV2(t); + const { walletClient, bankClient, exchange, merchant } = + await createSimpleTestkudosEnvironmentV3(t); const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); @@ -45,9 +45,9 @@ export async function runPaymentClaimTest(t: GlobalTestState) { // Withdraw digital cash into the wallet. - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); diff --git a/packages/taler-harness/src/integrationtests/test-payment-deleted.ts b/packages/taler-harness/src/integrationtests/test-payment-deleted.ts @@ -26,8 +26,8 @@ import { import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, - withdrawViaBankV2, + createSimpleTestkudosEnvironmentV3, + withdrawViaBankV3, } from "../harness/helpers.js"; /** @@ -36,16 +36,16 @@ import { export async function runPaymentDeletedTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bank, exchange, merchant } = - await createSimpleTestkudosEnvironmentV2(t); + const { walletClient, bankClient, exchange, merchant } = + await createSimpleTestkudosEnvironmentV3(t); // First, make a "free" payment when we don't even have // any money in the // Withdraw digital cash into the wallet. - await withdrawViaBankV2(t, { + await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); diff --git a/packages/taler-harness/src/integrationtests/test-payment-expired.ts b/packages/taler-harness/src/integrationtests/test-payment-expired.ts @@ -22,16 +22,16 @@ import { ConfirmPayResultType, Duration, MerchantApiClient, - MerchantContractTerms, PreparePayResultType, + TalerMerchantApi, j2s, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState } from "../harness/harness.js"; import { applyTimeTravelV2, - createSimpleTestkudosEnvironmentV2, - withdrawViaBankV2, + createSimpleTestkudosEnvironmentV3, + withdrawViaBankV3, } from "../harness/helpers.js"; /** @@ -49,14 +49,14 @@ import { export async function runPaymentExpiredTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bank, exchange, merchant } = - await createSimpleTestkudosEnvironmentV2(t); + const { walletClient, bankClient, exchange, merchant } = + await createSimpleTestkudosEnvironmentV3(t); // Withdraw digital cash into the wallet. - await withdrawViaBankV2(t, { + await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); @@ -64,7 +64,7 @@ export async function runPaymentExpiredTest(t: GlobalTestState) { await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); // Order that can only be paid within five minutes. - const order: Partial<MerchantContractTerms> = { + const order: TalerMerchantApi.Order = { summary: "Buy me!", amount: "TESTKUDOS:5", fulfillment_url: "taler://fulfillment-success/thx", diff --git a/packages/taler-harness/src/integrationtests/test-payment-fault.ts b/packages/taler-harness/src/integrationtests/test-payment-fault.ts @@ -21,7 +21,11 @@ /** * Imports. */ -import { ConfirmPayResultType, MerchantApiClient } from "@gnu-taler/taler-util"; +import { + ConfirmPayResultType, + MerchantApiClient, + TalerCorebankApiClient, +} from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { defaultCoinConfig } from "../harness/denomStructures.js"; import { @@ -39,7 +43,7 @@ import { } from "../harness/harness.js"; import { createWalletDaemonWithClient, - withdrawViaBankV2, + withdrawViaBankV3, } from "../harness/helpers.js"; /** @@ -64,10 +68,20 @@ export async function runPaymentFaultTest(t: GlobalTestState) { database: db.connStr, }); - const exchangeBankAccount = await bank.createExchangeAccount( - "myexchange", - "x", - ); + 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, + }); const faultyExchange = new FaultInjectedExchangeService(t, exchange, 8091); // Base URL must contain port that the proxy is listening on. @@ -75,16 +89,27 @@ export async function runPaymentFaultTest(t: GlobalTestState) { config.setString("exchange", "base_url", "http://localhost:8091/"); }); - bank.setSuggestedExchange( - faultyExchange, - exchangeBankAccount.accountPaytoUri, - ); + bank.setSuggestedExchange(faultyExchange, exchangePaytoUri); await bank.start(); await bank.pingUntilAvailable(); - await exchange.addBankAccount("1", exchangeBankAccount); + 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.addOfferedCoins(defaultCoinConfig); await exchange.start(); @@ -128,9 +153,9 @@ export async function runPaymentFaultTest(t: GlobalTestState) { await walletClient.call(WalletApiOperation.GetBalances, {}); - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange: faultyExchange, amount: "TESTKUDOS:20", }); diff --git a/packages/taler-harness/src/integrationtests/test-payment-forgettable.ts b/packages/taler-harness/src/integrationtests/test-payment-forgettable.ts @@ -20,10 +20,11 @@ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, + createSimpleTestkudosEnvironmentV3, makeTestPaymentV2, - withdrawViaBankV2, + withdrawViaBankV3, } from "../harness/helpers.js"; +import { TalerMerchantApi } from "@gnu-taler/taler-util"; /** * Run test for payment with a contract that has forgettable fields. @@ -31,14 +32,14 @@ import { export async function runPaymentForgettableTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bank, exchange, merchant } = - await createSimpleTestkudosEnvironmentV2(t); + const { walletClient, bankClient, exchange, merchant } = + await createSimpleTestkudosEnvironmentV3(t); // Withdraw digital cash into the wallet. - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); @@ -46,7 +47,7 @@ export async function runPaymentForgettableTest(t: GlobalTestState) { await wres.withdrawalFinishedCond; { - const order = { + const order: TalerMerchantApi.Order = { summary: "Buy me!", amount: "TESTKUDOS:5", fulfillment_url: "taler://fulfillment-success/thx", @@ -64,7 +65,7 @@ export async function runPaymentForgettableTest(t: GlobalTestState) { console.log("testing with forgettable field without hash"); { - const order = { + const order: TalerMerchantApi.Order = { summary: "Buy me!", amount: "TESTKUDOS:5", fulfillment_url: "taler://fulfillment-success/thx", diff --git a/packages/taler-harness/src/integrationtests/test-payment-idempotency.ts b/packages/taler-harness/src/integrationtests/test-payment-idempotency.ts @@ -21,8 +21,8 @@ import { MerchantApiClient, PreparePayResultType } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, - withdrawViaBankV2, + createSimpleTestkudosEnvironmentV3, + withdrawViaBankV3, } from "../harness/helpers.js"; /** @@ -32,14 +32,14 @@ import { export async function runPaymentIdempotencyTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bank, exchange, merchant } = - await createSimpleTestkudosEnvironmentV2(t); + const { walletClient, bankClient, exchange, merchant } = + await createSimpleTestkudosEnvironmentV3(t); // Withdraw digital cash into the wallet. - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); diff --git a/packages/taler-harness/src/integrationtests/test-payment-multiple.ts b/packages/taler-harness/src/integrationtests/test-payment-multiple.ts @@ -17,7 +17,7 @@ /** * Imports. */ -import { MerchantApiClient } from "@gnu-taler/taler-util"; +import { MerchantApiClient, TalerCorebankApiClient } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { coin_ct10, coin_u1 } from "../harness/denomStructures.js"; import { @@ -30,13 +30,13 @@ import { } from "../harness/harness.js"; import { createWalletDaemonWithClient, - withdrawViaBankV2, + withdrawViaBankV3, } from "../harness/helpers.js"; async function setupTest(t: GlobalTestState): Promise<{ merchant: MerchantService; exchange: ExchangeService; - bank: BankService; + bankClient: TalerCorebankApiClient; }> { const db = await setupDb(t); @@ -54,20 +54,40 @@ async function setupTest(t: GlobalTestState): Promise<{ database: db.connStr, }); - const exchangeBankAccount = await bank.createExchangeAccount( - "myexchange", - "x", - ); - exchange.addOfferedCoins([coin_ct10, coin_u1]); - bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); + 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(); - await exchange.addBankAccount("1", exchangeBankAccount); + 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, + }); await exchange.start(); await exchange.pingUntilAvailable(); @@ -100,7 +120,7 @@ async function setupTest(t: GlobalTestState): Promise<{ return { merchant, - bank, + bankClient, exchange, }; } @@ -113,7 +133,7 @@ async function setupTest(t: GlobalTestState): Promise<{ export async function runPaymentMultipleTest(t: GlobalTestState) { // Set up test environment - const { merchant, bank, exchange } = await setupTest(t); + const { merchant, bankClient, exchange } = await setupTest(t); const { walletClient } = await createWalletDaemonWithClient(t, { name: "default", @@ -123,9 +143,9 @@ export async function runPaymentMultipleTest(t: GlobalTestState) { // Withdraw digital cash into the wallet. - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:100", }); diff --git a/packages/taler-harness/src/integrationtests/test-payment-share.ts b/packages/taler-harness/src/integrationtests/test-payment-share.ts @@ -18,6 +18,7 @@ * Imports. */ import { + AmountString, ConfirmPayResultType, MerchantApiClient, NotificationType, @@ -27,9 +28,9 @@ import { import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, + createSimpleTestkudosEnvironmentV3, createWalletDaemonWithClient, - withdrawViaBankV2, + withdrawViaBankV3, } from "../harness/helpers.js"; /** @@ -39,17 +40,17 @@ export async function runPaymentShareTest(t: GlobalTestState) { // Set up test environment const { walletClient: firstWallet, - bank, + bankClient, exchange, merchant, - } = await createSimpleTestkudosEnvironmentV2(t); + } = await createSimpleTestkudosEnvironmentV3(t); const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); // Withdraw digital cash into the wallet. - await withdrawViaBankV2(t, { + await withdrawViaBankV3(t, { walletClient: firstWallet, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); @@ -59,9 +60,9 @@ export async function runPaymentShareTest(t: GlobalTestState) { name: "wallet2", }); - await withdrawViaBankV2(t, { + await withdrawViaBankV3(t, { walletClient: secondWallet, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); @@ -80,7 +81,7 @@ export async function runPaymentShareTest(t: GlobalTestState) { async function createOrder(amount: string) { const order = { summary: "Buy me!", - amount, + amount: amount as AmountString, fulfillment_url: "taler://fulfillment-success/thx", }; diff --git a/packages/taler-harness/src/integrationtests/test-payment-template.ts b/packages/taler-harness/src/integrationtests/test-payment-template.ts @@ -18,6 +18,7 @@ * Imports. */ import { + AmountString, ConfirmPayResultType, Duration, MerchantApiClient, @@ -27,8 +28,8 @@ import { import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, - withdrawViaBankV2, + createSimpleTestkudosEnvironmentV3, + withdrawViaBankV3, } from "../harness/helpers.js"; /** @@ -37,11 +38,13 @@ import { export async function runPaymentTemplateTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bank, exchange, merchant } = - await createSimpleTestkudosEnvironmentV2(t); + const { walletClient, bankClient, exchange, merchant } = + await createSimpleTestkudosEnvironmentV3(t); const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); + const mySummary = "hello, I'm a summary"; + const createTemplateRes = await merchantClient.createTemplate({ template_id: "template1", template_description: "my test template", @@ -52,27 +55,44 @@ export async function runPaymentTemplateTest(t: GlobalTestState) { minutes: 2, }), ), - summary: "hello, I'm a summary", + summary: mySummary, + }, + editable_defaults: { + amount: "TESTKUDOS:1" as AmountString, }, }); narrowOpSuccessOrThrow("createTemplate", createTemplateRes); // Withdraw digital cash into the wallet. - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); await wres.withdrawalFinishedCond; + const talerPayTemplateUri = `taler+http://pay-template/localhost:${merchant.port}/template1`; + + const checkPayTemplateResult = await walletClient.call( + WalletApiOperation.CheckPayForTemplate, + { + talerPayTemplateUri, + }, + ); + + t.assertDeepEqual( + checkPayTemplateResult.templateDetails.template_contract.summary, + mySummary, + ); + // Request a template payment const preparePayResult = await walletClient.call( WalletApiOperation.PreparePayForTemplate, { - talerPayTemplateUri: `taler+http://pay-template/localhost:${merchant.port}/template1?amount=TESTKUDOS:1`, + talerPayTemplateUri, templateParams: {}, }, ); diff --git a/packages/taler-harness/src/integrationtests/test-payment-zero.ts b/packages/taler-harness/src/integrationtests/test-payment-zero.ts @@ -20,8 +20,8 @@ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, - withdrawViaBankV2, + createSimpleTestkudosEnvironmentV3, + withdrawViaBankV3, makeTestPaymentV2, } from "../harness/helpers.js"; import { TransactionMajorState } from "@gnu-taler/taler-util"; @@ -33,14 +33,14 @@ import { TransactionMajorState } from "@gnu-taler/taler-util"; export async function runPaymentZeroTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bank, exchange, merchant } = - await createSimpleTestkudosEnvironmentV2(t); + const { walletClient, bankClient, exchange, merchant } = + await createSimpleTestkudosEnvironmentV3(t); // First, make a "free" payment when we don't even have // any money in the // Withdraw digital cash into the wallet. - await withdrawViaBankV2(t, { walletClient, bank, exchange, amount: "TESTKUDOS:20" }); + await withdrawViaBankV3(t, { walletClient, bankClient, exchange, amount: "TESTKUDOS:20" }); await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); diff --git a/packages/taler-harness/src/integrationtests/test-payment.ts b/packages/taler-harness/src/integrationtests/test-payment.ts @@ -17,14 +17,14 @@ /** * Imports. */ +import { TalerMerchantApi, j2s } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, - withdrawViaBankV2, + createSimpleTestkudosEnvironmentV3, makeTestPaymentV2, + withdrawViaBankV3, } from "../harness/helpers.js"; -import { j2s } from "@gnu-taler/taler-util"; /** * Run test for basic, bank-integrated withdrawal and payment. @@ -32,12 +32,18 @@ import { j2s } from "@gnu-taler/taler-util"; export async function runPaymentTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bank, exchange, merchant } = - await createSimpleTestkudosEnvironmentV2(t); + const { bankClient, walletClient, exchange, merchant } = + await createSimpleTestkudosEnvironmentV3(t); // Withdraw digital cash into the wallet. - await withdrawViaBankV2(t, { walletClient, bank, exchange, amount: "TESTKUDOS:20" }); + t.assertTrue(bankClient !== undefined); + await withdrawViaBankV3(t, { + walletClient, + exchange, + amount: "TESTKUDOS:20", + bankClient, + }); await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); @@ -45,7 +51,7 @@ export async function runPaymentTest(t: GlobalTestState) { summary: "Buy me!", amount: "TESTKUDOS:5", fulfillment_url: "taler://fulfillment-success/thx", - }; + } satisfies TalerMerchantApi.Order; await makeTestPaymentV2(t, { walletClient, merchant, order }); await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); @@ -56,7 +62,7 @@ export async function runPaymentTest(t: GlobalTestState) { summary: "Testing “unicode” characters: 😁😱😇🥺🫦", amount: "TESTKUDOS:5", fulfillment_url: "taler://fulfillment-success/thx", - }; + } satisfies TalerMerchantApi.Order; await makeTestPaymentV2(t, { walletClient, merchant, order: order2 }); await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); @@ -67,7 +73,7 @@ export async function runPaymentTest(t: GlobalTestState) { summary: "Testing\nNewlines\rAnd\tStuff\nHere\b", amount: "TESTKUDOS:5", fulfillment_url: "taler://fulfillment-success/thx", - }; + } satisfies TalerMerchantApi.Order; await makeTestPaymentV2(t, { walletClient, merchant, order: order3 }); await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); diff --git a/packages/taler-harness/src/integrationtests/test-paywall-flow.ts b/packages/taler-harness/src/integrationtests/test-paywall-flow.ts @@ -27,8 +27,8 @@ import { import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState, harnessHttpLib } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, - withdrawViaBankV2, + createSimpleTestkudosEnvironmentV3, + withdrawViaBankV3, } from "../harness/helpers.js"; /** @@ -37,16 +37,16 @@ import { export async function runPaywallFlowTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bank, exchange, merchant } = - await createSimpleTestkudosEnvironmentV2(t); + const { walletClient, bankClient, exchange, merchant } = + await createSimpleTestkudosEnvironmentV3(t); const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); // Withdraw digital cash into the wallet. - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); diff --git a/packages/taler-harness/src/integrationtests/test-peer-repair.ts b/packages/taler-harness/src/integrationtests/test-peer-repair.ts @@ -31,15 +31,15 @@ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import * as fs from "node:fs"; import { GlobalTestState } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, + createSimpleTestkudosEnvironmentV3, createWalletDaemonWithClient, - withdrawViaBankV2, + withdrawViaBankV3, } from "../harness/helpers.js"; export async function runPeerRepairTest(t: GlobalTestState) { // Set up test environment - const { bank, exchange } = await createSimpleTestkudosEnvironmentV2(t); + const { bankClient, exchange } = await createSimpleTestkudosEnvironmentV3(t); let allW1Notifications: WalletNotification[] = []; let allW2Notifications: WalletNotification[] = []; @@ -69,9 +69,9 @@ export async function runPeerRepairTest(t: GlobalTestState) { x.transactionId.startsWith("txn:withdrawal:"), ); - await withdrawViaBankV2(t, { + await withdrawViaBankV3(t, { walletClient: wallet1, - bank, + bankClient, exchange, amount: "TESTKUDOS:5", }); @@ -190,9 +190,9 @@ export async function runPeerRepairTest(t: GlobalTestState) { // Now withdraw so we have enough coins to re-select - const withdraw2Res = await withdrawViaBankV2(t, { + const withdraw2Res = await withdrawViaBankV3(t, { walletClient: wallet1, - bank, + bankClient, exchange, amount: "TESTKUDOS:5", }); diff --git a/packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts b/packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts @@ -23,6 +23,7 @@ import { Duration, j2s, NotificationType, + TalerCorebankApiClient, TransactionMajorState, TransactionMinorState, TransactionType, @@ -30,15 +31,14 @@ import { } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { - BankServiceHandle, ExchangeService, GlobalTestState, WalletClient, } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, + createSimpleTestkudosEnvironmentV3, createWalletDaemonWithClient, - withdrawViaBankV2, + withdrawViaBankV3, } from "../harness/helpers.js"; /** @@ -47,7 +47,7 @@ import { export async function runPeerToPeerPullTest(t: GlobalTestState) { // Set up test environment - const { bank, exchange } = await createSimpleTestkudosEnvironmentV2(t); + const { bankClient, exchange } = await createSimpleTestkudosEnvironmentV3(t); let allW1Notifications: WalletNotification[] = []; let allW2Notifications: WalletNotification[] = []; @@ -71,26 +71,26 @@ export async function runPeerToPeerPullTest(t: GlobalTestState) { const wallet1 = w1.walletClient; const wallet2 = w2.walletClient; - await checkNormalPeerPull(t, bank, exchange, wallet1, wallet2); + await checkNormalPeerPull(t, bankClient, exchange, wallet1, wallet2); console.log(`w1 notifications: ${j2s(allW1Notifications)}`); // Check that we don't have an excessive number of notifications. t.assertTrue(allW1Notifications.length <= 60); - await checkAbortedPeerPull(t, bank, exchange, wallet1, wallet2); + await checkAbortedPeerPull(t, bankClient, exchange, wallet1, wallet2); } async function checkNormalPeerPull( t: GlobalTestState, - bank: BankServiceHandle, + bankClient: TalerCorebankApiClient, exchange: ExchangeService, wallet1: WalletClient, wallet2: WalletClient, ): Promise<void> { - const withdrawRes = await withdrawViaBankV2(t, { + const withdrawRes = await withdrawViaBankV3(t, { walletClient: wallet2, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); @@ -179,14 +179,14 @@ async function checkNormalPeerPull( async function checkAbortedPeerPull( t: GlobalTestState, - bank: BankServiceHandle, + bankClient: TalerCorebankApiClient, exchange: ExchangeService, wallet1: WalletClient, wallet2: WalletClient, ): Promise<void> { - const withdrawRes = await withdrawViaBankV2(t, { + const withdrawRes = await withdrawViaBankV3(t, { walletClient: wallet2, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); diff --git a/packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts b/packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts @@ -31,16 +31,16 @@ import { import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, + createSimpleTestkudosEnvironmentV3, createWalletDaemonWithClient, - withdrawViaBankV2, + withdrawViaBankV3, } from "../harness/helpers.js"; /** * Run a test for basic peer-push payments. */ export async function runPeerToPeerPushTest(t: GlobalTestState) { - const { bank, exchange } = await createSimpleTestkudosEnvironmentV2(t); + const { bankClient, exchange } = await createSimpleTestkudosEnvironmentV3(t); let allW1Notifications: WalletNotification[] = []; let allW2Notifications: WalletNotification[] = []; @@ -60,9 +60,9 @@ export async function runPeerToPeerPushTest(t: GlobalTestState) { // Withdraw digital cash into the wallet. - const withdrawRes = await withdrawViaBankV2(t, { + const withdrawRes = await withdrawViaBankV3(t, { walletClient: w1.walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); diff --git a/packages/taler-harness/src/integrationtests/test-refund-auto.ts b/packages/taler-harness/src/integrationtests/test-refund-auto.ts @@ -21,8 +21,8 @@ import { Duration, MerchantApiClient } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, - withdrawViaBankV2, + createSimpleTestkudosEnvironmentV3, + withdrawViaBankV3, } from "../harness/helpers.js"; /** @@ -31,16 +31,16 @@ import { export async function runRefundAutoTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bank, exchange, merchant } = - await createSimpleTestkudosEnvironmentV2(t); + const { walletClient, bankClient, exchange, merchant } = + await createSimpleTestkudosEnvironmentV3(t); const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); // Withdraw digital cash into the wallet. - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); diff --git a/packages/taler-harness/src/integrationtests/test-refund-gone.ts b/packages/taler-harness/src/integrationtests/test-refund-gone.ts @@ -28,8 +28,8 @@ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState } from "../harness/harness.js"; import { applyTimeTravelV2, - createSimpleTestkudosEnvironmentV2, - withdrawViaBankV2, + createSimpleTestkudosEnvironmentV3, + withdrawViaBankV3, } from "../harness/helpers.js"; /** @@ -39,16 +39,16 @@ import { export async function runRefundGoneTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bank, exchange, merchant } = - await createSimpleTestkudosEnvironmentV2(t); + const { walletClient, bankClient, exchange, merchant } = + await createSimpleTestkudosEnvironmentV3(t); const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); // Withdraw digital cash into the wallet. - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); diff --git a/packages/taler-harness/src/integrationtests/test-refund-incremental.ts b/packages/taler-harness/src/integrationtests/test-refund-incremental.ts @@ -26,8 +26,8 @@ import { import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState, delayMs } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, - withdrawViaBankV2, + createSimpleTestkudosEnvironmentV3, + withdrawViaBankV3, } from "../harness/helpers.js"; /** @@ -36,16 +36,16 @@ import { export async function runRefundIncrementalTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bank, exchange, merchant } = - await createSimpleTestkudosEnvironmentV2(t); + const { walletClient, bankClient, exchange, merchant } = + await createSimpleTestkudosEnvironmentV3(t); const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); // Withdraw digital cash into the wallet. - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); diff --git a/packages/taler-harness/src/integrationtests/test-refund.ts b/packages/taler-harness/src/integrationtests/test-refund.ts @@ -28,8 +28,8 @@ import { import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, - withdrawViaBankV2, + createSimpleTestkudosEnvironmentV3, + withdrawViaBankV3, } from "../harness/helpers.js"; export async function runRefundTest(t: GlobalTestState) { @@ -37,18 +37,18 @@ export async function runRefundTest(t: GlobalTestState) { const { walletClient: wallet, - bank, + bankClient, exchange, merchant, - } = await createSimpleTestkudosEnvironmentV2(t); + } = await createSimpleTestkudosEnvironmentV3(t); const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); // Withdraw digital cash into the wallet. - const withdrawalRes = await withdrawViaBankV2(t, { + const withdrawalRes = await withdrawViaBankV3(t, { walletClient: wallet, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); diff --git a/packages/taler-harness/src/integrationtests/test-revocation.ts b/packages/taler-harness/src/integrationtests/test-revocation.ts @@ -17,6 +17,10 @@ /** * Imports. */ +import { + TalerCorebankApiClient, + TalerMerchantApi, +} from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { CoinConfig } from "../harness/denomStructures.js"; import { @@ -31,10 +35,10 @@ import { setupDb, } from "../harness/harness.js"; import { - SimpleTestEnvironmentNg, + SimpleTestEnvironmentNg3, createWalletDaemonWithClient, makeTestPaymentV2, - withdrawViaBankV2, + withdrawViaBankV3, } from "../harness/helpers.js"; async function revokeAllWalletCoins(req: { @@ -62,7 +66,7 @@ async function revokeAllWalletCoins(req: { async function createTestEnvironment( t: GlobalTestState, -): Promise<SimpleTestEnvironmentNg> { +): Promise<SimpleTestEnvironmentNg3> { const db = await setupDb(t); const bank = await BankService.create(t, { @@ -86,18 +90,42 @@ async function createTestEnvironment( database: db.connStr, }); - const exchangeBankAccount = await bank.createExchangeAccount( - "myexchange", - "x", - ); - exchange.addBankAccount("1", exchangeBankAccount); + 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, exchangeBankAccount.accountPaytoUri); + 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, + }); + const coin_u1: CoinConfig = { cipher: "RSA" as const, durationLegal: "3 years", @@ -151,8 +179,13 @@ async function createTestEnvironment( merchant, walletClient, walletService, - bank, - exchangeBankAccount, + bankClient, + exchangeBankAccount: { + accountName: "", + accountPassword: "", + accountPaytoUri: "", + wireGatewayApiBaseUrl: "", + }, }; } @@ -162,14 +195,14 @@ async function createTestEnvironment( export async function runRevocationTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bank, exchange, merchant } = + const { walletClient, bankClient, exchange, merchant } = await createTestEnvironment(t); // Withdraw digital cash into the wallet. - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:15", }); @@ -191,15 +224,15 @@ export async function runRevocationTest(t: GlobalTestState) { summary: "Buy me!", amount: "TESTKUDOS:10", fulfillment_url: "taler://fulfillment-success/thx", - }; + } satisfies TalerMerchantApi.Order; await makeTestPaymentV2(t, { walletClient, merchant, order }); await walletClient.call(WalletApiOperation.ClearDb, {}); - await withdrawViaBankV2(t, { + await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:15", }); diff --git a/packages/taler-harness/src/integrationtests/test-simple-payment.ts b/packages/taler-harness/src/integrationtests/test-simple-payment.ts @@ -24,6 +24,7 @@ import { makeTestPaymentV2, useSharedTestkudosEnvironment, } from "../harness/helpers.js"; +import { TalerMerchantApi } from "@gnu-taler/taler-util"; /** * Run test for basic, bank-integrated withdrawal and payment. @@ -49,7 +50,7 @@ export async function runSimplePaymentTest(t: GlobalTestState) { summary: "Buy me!", amount: "TESTKUDOS:5", fulfillment_url: "taler://fulfillment-success/thx", - }; + } satisfies TalerMerchantApi.Order; await makeTestPaymentV2(t, { walletClient, merchant, order }); await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); diff --git a/packages/taler-harness/src/integrationtests/test-stored-backups.ts b/packages/taler-harness/src/integrationtests/test-stored-backups.ts @@ -24,6 +24,7 @@ import { makeTestPaymentV2, useSharedTestkudosEnvironment, } from "../harness/helpers.js"; +import { TalerMerchantApi } from "@gnu-taler/taler-util"; /** * Test stored backup wallet-core API. @@ -62,7 +63,7 @@ export async function runStoredBackupsTest(t: GlobalTestState) { summary: "Buy me!", amount: "TESTKUDOS:5", fulfillment_url: "taler://fulfillment-success/thx", - }; + } satisfies TalerMerchantApi.Order; await makeTestPaymentV2(t, { walletClient, merchant, order }); await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); diff --git a/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts b/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts @@ -23,11 +23,12 @@ import { MerchantApiClient, NotificationType, PreparePayResultType, + TalerCorebankApiClient, } 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, @@ -37,7 +38,7 @@ import { import { applyTimeTravelV2, createWalletDaemonWithClient, - withdrawViaBankV2, + withdrawViaBankV3, } from "../harness/helpers.js"; /** @@ -69,18 +70,39 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) { database: db.connStr, }); - const exchangeBankAccount = await bank.createExchangeAccount( - "myexchange", - "x", - ); - exchange.addBankAccount("1", exchangeBankAccount); + 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, exchangeBankAccount.accountPaytoUri); + 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(); @@ -113,9 +135,9 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) { // Withdraw digital cash into the wallet. - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:15", }); @@ -143,9 +165,9 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) { await exchangeUpdated1Cond; await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); - const wres2 = await withdrawViaBankV2(t, { + const wres2 = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); diff --git a/packages/taler-harness/src/integrationtests/test-timetravel-withdraw.ts b/packages/taler-harness/src/integrationtests/test-timetravel-withdraw.ts @@ -26,8 +26,8 @@ import { import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, - withdrawViaBankV2, + createSimpleTestkudosEnvironmentV3, + withdrawViaBankV3, } from "../harness/helpers.js"; /** @@ -36,14 +36,14 @@ import { export async function runTimetravelWithdrawTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bank, exchange, merchant } = - await createSimpleTestkudosEnvironmentV2(t); + const { walletClient, bankClient, exchange, merchant } = + await createSimpleTestkudosEnvironmentV3(t); // Withdraw digital cash into the wallet. - const wres1 = await withdrawViaBankV2(t, { + const wres1 = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:15", }); @@ -70,9 +70,9 @@ export async function runTimetravelWithdrawTest(t: GlobalTestState) { console.log("starting withdrawal via bank"); // This should fail, as the wallet didn't time travel yet. - const wres2 = await withdrawViaBankV2(t, { + const wres2 = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); diff --git a/packages/taler-harness/src/integrationtests/test-wallet-backup-basic.ts b/packages/taler-harness/src/integrationtests/test-wallet-backup-basic.ts @@ -21,9 +21,9 @@ import { j2s } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, + createSimpleTestkudosEnvironmentV3, createWalletDaemonWithClient, - withdrawViaBankV2, + withdrawViaBankV3, } from "../harness/helpers.js"; import { SyncService } from "../harness/sync.js"; @@ -33,8 +33,8 @@ import { SyncService } from "../harness/sync.js"; export async function runWalletBackupBasicTest(t: GlobalTestState) { // Set up test environment - const { commonDb, merchant, walletClient, bank, exchange } = - await createSimpleTestkudosEnvironmentV2(t); + const { commonDb, merchant, walletClient, bankClient, exchange } = + await createSimpleTestkudosEnvironmentV3(t); const sync = await SyncService.create(t, { currency: "TESTKUDOS", @@ -83,9 +83,9 @@ export async function runWalletBackupBasicTest(t: GlobalTestState) { ); } - await withdrawViaBankV2(t, { + await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:10", }); @@ -99,9 +99,9 @@ export async function runWalletBackupBasicTest(t: GlobalTestState) { console.log(bi); } - await withdrawViaBankV2(t, { + await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:5", }); @@ -158,9 +158,9 @@ export async function runWalletBackupBasicTest(t: GlobalTestState) { t.assertAmountEquals(bal1.balances[0].available, "TESTKUDOS:14.1"); - await withdrawViaBankV2(t, { + await withdrawViaBankV3(t, { walletClient: walletClient2, - bank, + bankClient, exchange, amount: "TESTKUDOS:10", }); diff --git a/packages/taler-harness/src/integrationtests/test-wallet-backup-doublespend.ts b/packages/taler-harness/src/integrationtests/test-wallet-backup-doublespend.ts @@ -21,18 +21,18 @@ import { MerchantApiClient, PreparePayResultType } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, + createSimpleTestkudosEnvironmentV3, createWalletDaemonWithClient, makeTestPaymentV2, - withdrawViaBankV2, + withdrawViaBankV3, } from "../harness/helpers.js"; import { SyncService } from "../harness/sync.js"; export async function runWalletBackupDoublespendTest(t: GlobalTestState) { // Set up test environment - const { commonDb, merchant, walletClient, bank, exchange } = - await createSimpleTestkudosEnvironmentV2(t); + const { commonDb, merchant, walletClient, bankClient, exchange } = + await createSimpleTestkudosEnvironmentV3(t); const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); @@ -56,9 +56,9 @@ export async function runWalletBackupDoublespendTest(t: GlobalTestState) { name: sync.baseUrl, }); - await withdrawViaBankV2(t, { + await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:10", }); @@ -161,9 +161,9 @@ export async function runWalletBackupDoublespendTest(t: GlobalTestState) { // FIXME: wait for a notification that indicates insufficient funds! - await withdrawViaBankV2(t, { + await withdrawViaBankV3(t, { walletClient: walletClientTwo, - bank, + bankClient, exchange, amount: "TESTKUDOS:50", }); diff --git a/packages/taler-harness/src/integrationtests/test-wallet-balance-notifications.ts b/packages/taler-harness/src/integrationtests/test-wallet-balance-notifications.ts @@ -24,7 +24,7 @@ import { } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState } from "../harness/harness.js"; -import { createSimpleTestkudosEnvironmentV2 } from "../harness/helpers.js"; +import { createSimpleTestkudosEnvironmentV3 } from "../harness/helpers.js"; /** * Test behavior when an order is deleted while the wallet is paying for it. @@ -32,14 +32,17 @@ import { createSimpleTestkudosEnvironmentV2 } from "../harness/helpers.js"; export async function runWalletBalanceNotificationsTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bank, exchange, merchant, walletService } = - await createSimpleTestkudosEnvironmentV2(t); + const { walletClient, bankClient, exchange, walletService } = + await createSimpleTestkudosEnvironmentV3(t); const amount = "TESTKUDOS:20"; - const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl); - const user = await bankClient.createRandomBankUser(); + bankClient.setAuth({ + username: user.username, + password: user.password, + }); + const wop = await bankClient.createWithdrawalOperation(user.username, amount); // Hand it to the wallet diff --git a/packages/taler-harness/src/integrationtests/test-wallet-balance-zero.ts b/packages/taler-harness/src/integrationtests/test-wallet-balance-zero.ts @@ -22,9 +22,9 @@ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { makeNoFeeCoinConfig } from "../harness/denomStructures.js"; import { GlobalTestState } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, + createSimpleTestkudosEnvironmentV3, makeTestPaymentV2, - withdrawViaBankV2, + withdrawViaBankV3, } from "../harness/helpers.js"; /** @@ -36,12 +36,12 @@ export async function runWalletBalanceZeroTest(t: GlobalTestState) { const coinConfig = makeNoFeeCoinConfig("TESTKUDOS"); console.log(`coin config ${j2s(coinConfig)}`); - const { merchant, walletClient, exchange, bank } = - await createSimpleTestkudosEnvironmentV2(t, coinConfig); + const { merchant, walletClient, exchange, bankClient } = + await createSimpleTestkudosEnvironmentV3(t, coinConfig); - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { amount: "TESTKUDOS:10", - bank, + bankClient, exchange, walletClient, }); diff --git a/packages/taler-harness/src/integrationtests/test-wallet-balance.ts b/packages/taler-harness/src/integrationtests/test-wallet-balance.ts @@ -23,12 +23,13 @@ import { MerchantApiClient, MerchantContractTerms, PreparePayResultType, + TalerMerchantApi, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, - withdrawViaBankV2, + createSimpleTestkudosEnvironmentV3, + withdrawViaBankV3, } from "../harness/helpers.js"; /** @@ -42,8 +43,8 @@ import { export async function runWalletBalanceTest(t: GlobalTestState) { // Set up test environment - const { merchant, walletClient, exchange, bank } = - await createSimpleTestkudosEnvironmentV2(t); + const { merchant, walletClient, exchange, bankClient } = + await createSimpleTestkudosEnvironmentV3(t); await merchant.addInstanceWithWireAccount({ id: "myinst", @@ -60,9 +61,9 @@ export async function runWalletBalanceTest(t: GlobalTestState) { // Withdraw digital cash into the wallet. - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); @@ -71,7 +72,7 @@ export async function runWalletBalanceTest(t: GlobalTestState) { console.log("withdrawal finished"); - const order: Partial<MerchantContractTerms> = { + const order: TalerMerchantApi.Order = { summary: "Buy me!", amount: "TESTKUDOS:5", fulfillment_url: "taler://fulfillment-success/thx", diff --git a/packages/taler-harness/src/integrationtests/test-wallet-blocked-deposit.ts b/packages/taler-harness/src/integrationtests/test-wallet-blocked-deposit.ts @@ -28,10 +28,10 @@ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { CoinConfig } from "../harness/denomStructures.js"; import { GlobalTestState, generateRandomPayto } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, + createSimpleTestkudosEnvironmentV3, createWalletDaemonWithClient, makeTestPaymentV2, - withdrawViaBankV2, + withdrawViaBankV3, } from "../harness/helpers.js"; const coinCommon = { @@ -65,8 +65,8 @@ export async function runWalletBlockedDepositTest(t: GlobalTestState) { }, ]; - const { walletClient, bank, exchange, merchant } = - await createSimpleTestkudosEnvironmentV2(t, coinConfigList); + const { bankClient, exchange, merchant } = + await createSimpleTestkudosEnvironmentV3(t, coinConfigList); // Withdraw digital cash into the wallet. @@ -80,9 +80,9 @@ export async function runWalletBlockedDepositTest(t: GlobalTestState) { }, }); - await withdrawViaBankV2(t, { + await withdrawViaBankV3(t, { walletClient: w1, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); diff --git a/packages/taler-harness/src/integrationtests/test-wallet-cli-termination.ts b/packages/taler-harness/src/integrationtests/test-wallet-cli-termination.ts @@ -21,8 +21,8 @@ import { AmountString } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; import { - BankService, ExchangeService, + FakebankService, GlobalTestState, MerchantService, WalletCli, @@ -38,7 +38,7 @@ export async function runWalletCliTerminationTest(t: GlobalTestState) { const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")); - const bank = await BankService.create(t, { + const bank = await FakebankService.create(t, { allowRegistrations: true, currency: "TESTKUDOS", database: db.connStr, diff --git a/packages/taler-harness/src/integrationtests/test-wallet-dd48.ts b/packages/taler-harness/src/integrationtests/test-wallet-dd48.ts @@ -20,6 +20,7 @@ import { ExchangeEntryStatus, NotificationType, + TalerCorebankApiClient, TalerError, TalerErrorCode, WalletNotification, @@ -28,14 +29,15 @@ import { import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; import { + BankService, ExchangeService, - FakebankService, GlobalTestState, WalletClient, WalletService, + generateRandomPayto, setupDb, } from "../harness/harness.js"; -import { withdrawViaBankV2 } from "../harness/helpers.js"; +import { withdrawViaBankV3 } from "../harness/helpers.js"; /** * Test for DD48 notifications. @@ -45,7 +47,7 @@ export async function runWalletDd48Test(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, @@ -59,18 +61,39 @@ export async function runWalletDd48Test(t: GlobalTestState) { database: db.connStr, }); - const exchangeBankAccount = await bank.createExchangeAccount( - "myexchange", - "x", - ); - exchange.addBankAccount("1", exchangeBankAccount); + 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, exchangeBankAccount.accountPaytoUri); + 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, + }); + const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")); exchange.addCoinConfigList(coinConfig); @@ -129,10 +152,10 @@ export async function runWalletDd48Test(t: GlobalTestState) { t.assertDeepEqual(resources.hasResources, false); } - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient, amount: "TESTKUDOS:20", - bank, + bankClient, exchange, }); diff --git a/packages/taler-harness/src/integrationtests/test-wallet-denom-expire.ts b/packages/taler-harness/src/integrationtests/test-wallet-denom-expire.ts @@ -17,12 +17,13 @@ /** * Imports. */ -import { Duration, Logger, NotificationType, j2s } from "@gnu-taler/taler-util"; +import { Duration, Logger, NotificationType, 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, + FakebankService, GlobalTestState, MerchantService, generateRandomPayto, @@ -31,7 +32,7 @@ import { import { applyTimeTravelV2, createWalletDaemonWithClient, - withdrawViaBankV2, + withdrawViaBankV3, } from "../harness/helpers.js"; const logger = new Logger("test-exchange-timetravel.ts"); @@ -65,18 +66,39 @@ export async function runWalletDenomExpireTest(t: GlobalTestState) { database: db.connStr, }); - const exchangeBankAccount = await bank.createExchangeAccount( - "myexchange", - "x", - ); - exchange.addBankAccount("1", exchangeBankAccount); + 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, exchangeBankAccount.accountPaytoUri); + 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(); @@ -109,9 +131,9 @@ export async function runWalletDenomExpireTest(t: GlobalTestState) { // Withdraw digital cash into the wallet. - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:15", }); diff --git a/packages/taler-harness/src/integrationtests/test-wallet-exchange-update.ts b/packages/taler-harness/src/integrationtests/test-wallet-exchange-update.ts @@ -28,6 +28,7 @@ import { defaultCoinConfig } from "../harness/denomStructures.js"; import { BankService, ExchangeService, + FakebankService, GlobalTestState, setupDb, } from "../harness/harness.js"; @@ -50,7 +51,7 @@ export async function runWalletExchangeUpdateTest( nameSuffix: "two", }); - const bank = await BankService.create(t, { + const bank = await FakebankService.create(t, { allowRegistrations: true, currency: "TESTKUDOS", database: db.connStr, diff --git a/packages/taler-harness/src/integrationtests/test-wallet-gendb.ts b/packages/taler-harness/src/integrationtests/test-wallet-gendb.ts @@ -17,22 +17,22 @@ /** * Imports. */ -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { GlobalTestState } from "../harness/harness.js"; -import { - createSimpleTestkudosEnvironmentV2, - withdrawViaBankV2, - makeTestPaymentV2, -} from "../harness/helpers.js"; import { AbsoluteTime, AmountString, Duration, NotificationType, + TalerMerchantApi, TransactionMajorState, TransactionMinorState, - j2s, } from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { GlobalTestState } from "../harness/harness.js"; +import { + createSimpleTestkudosEnvironmentV3, + makeTestPaymentV2, + withdrawViaBankV3, +} from "../harness/helpers.js"; /** * Test that creates various transactions and exports the resulting @@ -42,21 +42,21 @@ import { export async function runWalletGenDbTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bank, exchange, merchant } = - await createSimpleTestkudosEnvironmentV2(t); + const { walletClient, bankClient, exchange, merchant } = + await createSimpleTestkudosEnvironmentV3(t); // Withdraw digital cash into the wallet. - await withdrawViaBankV2(t, { + await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:50", }); await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); - const order = { + const order: TalerMerchantApi.Order = { summary: "Buy me!", amount: "TESTKUDOS:10", fulfillment_url: "taler://fulfillment-success/thx", diff --git a/packages/taler-harness/src/integrationtests/test-wallet-notifications.ts b/packages/taler-harness/src/integrationtests/test-wallet-notifications.ts @@ -26,12 +26,13 @@ import { import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; import { + BankService, ExchangeService, - FakebankService, GlobalTestState, MerchantService, WalletClient, WalletService, + generateRandomPayto, generateRandomTestIban, setupDb, } from "../harness/harness.js"; @@ -44,7 +45,7 @@ export async function runWalletNotificationsTest(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, @@ -58,6 +59,11 @@ export async function runWalletNotificationsTest(t: GlobalTestState) { database: db.connStr, }); + let receiverName = "Exchange"; + let exchangeBankUsername = "exchange"; + let exchangeBankPassword = "mypw"; + let exchangePaytoUri = generateRandomPayto(exchangeBankUsername); + const merchant = await MerchantService.create(t, { name: "testmerchant-1", currency: "TESTKUDOS", @@ -65,18 +71,34 @@ export async function runWalletNotificationsTest(t: GlobalTestState) { database: db.connStr, }); - const exchangeBankAccount = await bank.createExchangeAccount( - "myexchange", - "x", - ); - exchange.addBankAccount("1", exchangeBankAccount); + await exchange.addBankAccount("1", { + accountName: exchangeBankUsername, + accountPassword: exchangeBankPassword, + wireGatewayApiBaseUrl: new URL("accounts/exchange/taler-wire-gateway/", bank.baseUrl).href, + accountPaytoUri: exchangePaytoUri, + }); - bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); + 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, + }); + const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")); exchange.addCoinConfigList(coinConfig); @@ -126,12 +148,9 @@ export async function runWalletNotificationsTest(t: GlobalTestState) { } }); - const bankAccessApiClient = new TalerCorebankApiClient( - bank.corebankApiBaseUrl, - ); - const user = await bankAccessApiClient.createRandomBankUser(); - bankAccessApiClient.setAuth(user); - const wop = await bankAccessApiClient.createWithdrawalOperation( + const user = await bankClient.createRandomBankUser(); + bankClient.setAuth(user); + const wop = await bankClient.createWithdrawalOperation( user.username, "TESTKUDOS:20", ); @@ -166,7 +185,7 @@ export async function runWalletNotificationsTest(t: GlobalTestState) { // Confirm it - await bankAccessApiClient.confirmWithdrawalOperation(user.username, { + await bankClient.confirmWithdrawalOperation(user.username, { withdrawalOperationId: wop.withdrawal_id, }); diff --git a/packages/taler-harness/src/integrationtests/test-wallet-observability.ts b/packages/taler-harness/src/integrationtests/test-wallet-observability.ts @@ -17,25 +17,26 @@ /** * Imports. */ -import { NotificationType, WalletNotification } from "@gnu-taler/taler-util"; +import { NotificationType, TalerCorebankApiClient, WalletNotification } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; import { + BankService, ExchangeService, - FakebankService, GlobalTestState, WalletClient, WalletService, + generateRandomPayto, setupDb, } from "../harness/harness.js"; -import { withdrawViaBankV2 } from "../harness/helpers.js"; +import { withdrawViaBankV3 } from "../harness/helpers.js"; export async function runWalletObservabilityTest(t: GlobalTestState) { // Set up test environment const db = await setupDb(t); - const bank = await FakebankService.create(t, { + const bank = await BankService.create(t, { allowRegistrations: true, currency: "TESTKUDOS", database: db.connStr, @@ -49,18 +50,39 @@ export async function runWalletObservabilityTest(t: GlobalTestState) { database: db.connStr, }); - const exchangeBankAccount = await bank.createExchangeAccount( - "myexchange", - "x", - ); - exchange.addBankAccount("1", exchangeBankAccount); + 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, exchangeBankAccount.accountPaytoUri); + 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, + }); + const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")); exchange.addCoinConfigList(coinConfig); @@ -94,9 +116,9 @@ export async function runWalletObservabilityTest(t: GlobalTestState) { }, }); - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { amount: "TESTKUDOS:10", - bank, + bankClient, exchange, walletClient, }); diff --git a/packages/taler-harness/src/integrationtests/test-wallet-refresh.ts b/packages/taler-harness/src/integrationtests/test-wallet-refresh.ts @@ -20,6 +20,7 @@ import { AmountString, NotificationType, + TalerMerchantApi, TransactionIdStr, TransactionMajorState, TransactionType, @@ -31,9 +32,9 @@ import { } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState, generateRandomPayto } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, + createSimpleTestkudosEnvironmentV3, makeTestPaymentV2, - withdrawViaBankV2, + withdrawViaBankV3, } from "../harness/helpers.js"; /** @@ -42,21 +43,21 @@ import { export async function runWalletRefreshTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bank, exchange, merchant } = - await createSimpleTestkudosEnvironmentV2(t); + const { walletClient, bankClient, exchange, merchant } = + await createSimpleTestkudosEnvironmentV3(t); // Withdraw digital cash into the wallet. - await withdrawViaBankV2(t, { + await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); - const order = { + const order: TalerMerchantApi.Order = { summary: "Buy me!", amount: "TESTKUDOS:5", fulfillment_url: "taler://fulfillment-success/thx", @@ -143,9 +144,9 @@ export async function runWalletRefreshTest(t: GlobalTestState) { ); } - const wres = await withdrawViaBankV2(t, { + const wres = await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); diff --git a/packages/taler-harness/src/integrationtests/test-wallet-wirefees.ts b/packages/taler-harness/src/integrationtests/test-wallet-wirefees.ts @@ -20,8 +20,9 @@ import { Duration, MerchantApiClient, - MerchantContractTerms, PreparePayResultType, + TalerCorebankApiClient, + TalerMerchantApi, TransactionMajorState, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; @@ -36,7 +37,7 @@ import { } from "../harness/harness.js"; import { createWalletDaemonWithClient, - withdrawViaBankV2, + withdrawViaBankV3, } from "../harness/helpers.js"; /** @@ -70,17 +71,42 @@ export async function runWalletWirefeesTest(t: GlobalTestState) { database: db.connStr, }); - const exchangeBankAccount = await bank.createExchangeAccount( - "myexchange", - "x", - ); - await exchange.addBankAccount("1", exchangeBankAccount); + 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); - bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); 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, + }); + const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")); exchange.addCoinConfigList(coinConfig); @@ -119,22 +145,22 @@ export async function runWalletWirefeesTest(t: GlobalTestState) { // Withdraw digital cash into the wallet. - await withdrawViaBankV2(t, { + await withdrawViaBankV3(t, { walletClient, - bank, + bankClient, exchange, amount: "TESTKUDOS:20", }); await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); - const order = { + const order: TalerMerchantApi.Order = { summary: "Buy me!", amount: "TESTKUDOS:1", fulfillment_url: "taler://fulfillment-success/thx", //max_wire_fee: "TESTKUDOS:0.1", max_fee: "TESTKUDOS:0.1", - } satisfies Partial<MerchantContractTerms>; + }; const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); diff --git a/packages/taler-harness/src/integrationtests/test-wallettesting.ts b/packages/taler-harness/src/integrationtests/test-wallettesting.ts @@ -26,12 +26,12 @@ import { AmountString, Amounts, CoinStatus } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; import { - BankService, ExchangeService, GlobalTestState, MerchantService, setupDb, generateRandomPayto, + FakebankService, } from "../harness/harness.js"; import { SimpleTestEnvironmentNg, @@ -50,7 +50,7 @@ export async function createMyEnvironment( ): Promise<SimpleTestEnvironmentNg> { const db = await setupDb(t); - const bank = await BankService.create(t, { + const bank = await FakebankService.create(t, { allowRegistrations: true, currency: "TESTKUDOS", database: db.connStr, diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-abort-bank.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-abort-bank.ts @@ -17,10 +17,10 @@ /** * Imports. */ -import { TalerCorebankApiClient, TalerErrorCode } from "@gnu-taler/taler-util"; +import { TalerErrorCode } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState } from "../harness/harness.js"; -import { createSimpleTestkudosEnvironmentV2 } from "../harness/helpers.js"; +import { createSimpleTestkudosEnvironmentV3 } from "../harness/helpers.js"; /** * Run test for basic, bank-integrated withdrawal. @@ -28,17 +28,14 @@ import { createSimpleTestkudosEnvironmentV2 } from "../harness/helpers.js"; export async function runWithdrawalAbortBankTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bank, exchange } = - await createSimpleTestkudosEnvironmentV2(t); + const { walletClient, bankClient, exchange } = + await createSimpleTestkudosEnvironmentV3(t); // Create a withdrawal operation - const bankAccessApiClient = new TalerCorebankApiClient( - bank.corebankApiBaseUrl, - ); - const user = await bankAccessApiClient.createRandomBankUser(); - bankAccessApiClient.setAuth(user); - const wop = await bankAccessApiClient.createWithdrawalOperation( + const user = await bankClient.createRandomBankUser(); + bankClient.setAuth(user); + const wop = await bankClient.createWithdrawalOperation( user.username, "TESTKUDOS:10", ); @@ -53,7 +50,7 @@ export async function runWithdrawalAbortBankTest(t: GlobalTestState) { // Abort it - await bankAccessApiClient.abortWithdrawalOperation(wop); + await bankClient.abortWithdrawalOperationV2(user.username, wop); // Withdraw diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-amount.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-amount.ts @@ -0,0 +1,94 @@ +/* + 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 { + AmountString, + Logger, + WireGatewayApiClient, + 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"; + +const logger = new Logger("test-withdrawal-manual.ts"); + +/** + * Check what happens when the withdrawal amount unexpectedly changes. + */ +export async function runWithdrawalAmountTest(t: GlobalTestState) { + // Set up test environment + + const { walletClient, bankClient, exchange, exchangeBankAccount } = + await createSimpleTestkudosEnvironmentV3(t); + + const wireGatewayApiClient = new WireGatewayApiClient( + exchangeBankAccount.wireGatewayApiBaseUrl, + { + auth: { + username: "admin", + password: "adminpw", + }, + }, + ); + + // Create a withdrawal operation + + const user = await bankClient.createRandomBankUser(); + + await walletClient.call(WalletApiOperation.AddExchange, { + exchangeBaseUrl: exchange.baseUrl, + }); + + logger.info("starting AcceptManualWithdrawal request"); + + const wres = await walletClient.call( + WalletApiOperation.AcceptManualWithdrawal, + { + exchangeBaseUrl: exchange.baseUrl, + amount: "TESTKUDOS:10" as AmountString, + }, + ); + + logger.info("AcceptManualWithdrawal finished"); + logger.info(`result: ${j2s(wres)}`); + + const reservePub: string = wres.reservePub; + + await wireGatewayApiClient.adminAddIncoming({ + amount: "TESTKUDOS:5", + debitAccountPayto: user.accountPaytoUri, + reservePub: reservePub, + }); + + await exchange.runWirewatchOnce(); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); + + // Check balance + + const balResp = await walletClient.call(WalletApiOperation.GetBalances, {}); + + // We managed to withdraw the actually transferred amount! + t.assertAmountEquals(balResp.balances[0].available, "TESTKUDOS:4.85"); + + await t.shutdown(); +} + +runWithdrawalAmountTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts @@ -18,17 +18,16 @@ * Imports. */ import { - TalerCorebankApiClient, - j2s, NotificationType, TransactionMajorState, TransactionMinorState, TransactionType, WithdrawalType, + j2s, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState } from "../harness/harness.js"; -import { createSimpleTestkudosEnvironmentV2 } from "../harness/helpers.js"; +import { createSimpleTestkudosEnvironmentV3 } from "../harness/helpers.js"; /** * Run test for basic, bank-integrated withdrawal. @@ -36,17 +35,13 @@ import { createSimpleTestkudosEnvironmentV2 } from "../harness/helpers.js"; export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bank, exchange } = - await createSimpleTestkudosEnvironmentV2(t); + const { walletClient, bankClient, exchange } = + await createSimpleTestkudosEnvironmentV3(t); // Create a withdrawal operation - - const corebankApiClient = new TalerCorebankApiClient( - bank.corebankApiBaseUrl, - ); - const user = await corebankApiClient.createRandomBankUser(); - corebankApiClient.setAuth(user); - const wop = await corebankApiClient.createWithdrawalOperation( + const user = await bankClient.createRandomBankUser(); + bankClient.setAuth(user); + const wop = await bankClient.createWithdrawalOperation( user.username, "TESTKUDOS:10", ); @@ -129,7 +124,7 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) { // Confirm it - await corebankApiClient.confirmWithdrawalOperation(user.username, { + await bankClient.confirmWithdrawalOperation(user.username, { withdrawalOperationId: wop.withdrawal_id, }); diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-conversion.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-conversion.ts @@ -20,7 +20,6 @@ import { AbsoluteTime, AmountString, - Amounts, Duration, Logger, TalerBankConversionApi, @@ -34,8 +33,8 @@ 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, MerchantService, generateRandomPayto, @@ -102,7 +101,7 @@ async function runTestfakeConversionService(): Promise<TestfakeConversionService cashout_ratio: "1", cashout_rounding_mode: "zero", cashout_tiny_amount: "A:1" as AmountString, - } + }, } satisfies TalerBankConversionApi.IntegrationConfig), ); } else if (path === "/cashin-rate") { @@ -136,7 +135,7 @@ export async function runWithdrawalConversionTest(t: GlobalTestState) { const db = await setupDb(t); - const bank = await BankService.create(t, { + const bank = await FakebankService.create(t, { allowRegistrations: true, currency: "TESTKUDOS", database: db.connStr, diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts @@ -25,6 +25,7 @@ import { ExchangeService, GlobalTestState, WalletCli, + generateRandomPayto, setupDb, } from "../harness/harness.js"; @@ -81,16 +82,39 @@ export async function runWithdrawalFeesTest(t: GlobalTestState) { database: db.connStr, }); - const exchangeBankAccount = await bank.createExchangeAccount( - "myexchange", - "x", - ); - await exchange.addBankAccount("1", exchangeBankAccount); + 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, + }); + const coinConfig: CoinConfig[] = weirdCoinConfig.map((x) => x("TESTKUDOS")); exchange.addCoinConfigList(coinConfig); @@ -107,12 +131,9 @@ export async function runWithdrawalFeesTest(t: GlobalTestState) { const amount = "TESTKUDOS:7.5"; - const bankAccessApiClient = new TalerCorebankApiClient( - bank.corebankApiBaseUrl, - ); - const user = await bankAccessApiClient.createRandomBankUser(); - bankAccessApiClient.setAuth(user); - const wop = await bankAccessApiClient.createWithdrawalOperation( + const user = await bankClient.createRandomBankUser(); + bankClient.setAuth(user); + const wop = await bankClient.createWithdrawalOperation( user.username, amount, ); @@ -152,7 +173,7 @@ export async function runWithdrawalFeesTest(t: GlobalTestState) { // Confirm it - await bankAccessApiClient.confirmWithdrawalOperation(user.username, { + await bankClient.confirmWithdrawalOperation(user.username, { withdrawalOperationId: wop.withdrawal_id, }); await wallet.runUntilDone(); diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-handover.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-handover.ts @@ -27,7 +27,7 @@ import { import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironmentV2, + createSimpleTestkudosEnvironmentV3, createWalletDaemonWithClient, } from "../harness/helpers.js"; @@ -37,21 +37,20 @@ import { export async function runWithdrawalHandoverTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bank, exchange } = - await createSimpleTestkudosEnvironmentV2(t); + const { walletClient, bankClient, exchange } = + await createSimpleTestkudosEnvironmentV3(t); // Do one normal withdrawal with the new split API { // Create a withdrawal operation - const bankAccessApiClient = new TalerCorebankApiClient( - bank.corebankApiBaseUrl, - ); - const user = await bankAccessApiClient.createRandomBankUser(); - bankAccessApiClient.setAuth(user); - const wop = await bankAccessApiClient.createWithdrawalOperation( + const user = await bankClient.createRandomBankUser(); + const userBankClient = new TalerCorebankApiClient(bankClient.baseUrl); + userBankClient.setAuth(user); + const amount = "TESTKUDOS:10"; + const wop = await userBankClient.createWithdrawalOperation( user.username, - "TESTKUDOS:10", + amount, ); const checkResp = await walletClient.call( @@ -66,13 +65,15 @@ export async function runWithdrawalHandoverTest(t: GlobalTestState) { const prepareResp = await walletClient.call( WalletApiOperation.PrepareBankIntegratedWithdrawal, { - exchangeBaseUrl: checkResp.defaultExchangeBaseUrl, + // exchangeBaseUrl: checkResp.defaultExchangeBaseUrl, talerWithdrawUri: wop.taler_withdraw_uri, }, ); console.log(`prepareResp: ${j2s(prepareResp)}`); + t.assertTrue(!!prepareResp.transactionId); + const txns1 = await walletClient.call(WalletApiOperation.GetTransactions, { sort: "stable-ascending", }); @@ -80,6 +81,8 @@ export async function runWithdrawalHandoverTest(t: GlobalTestState) { await walletClient.call(WalletApiOperation.ConfirmWithdrawal, { transactionId: prepareResp.transactionId, + amount, + exchangeBaseUrl: checkResp.defaultExchangeBaseUrl, }); await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { @@ -90,7 +93,7 @@ export async function runWithdrawalHandoverTest(t: GlobalTestState) { }, }); - await bankAccessApiClient.confirmWithdrawalOperation(user.username, { + await userBankClient.confirmWithdrawalOperation(user.username, { withdrawalOperationId: wop.withdrawal_id, }); @@ -112,14 +115,14 @@ export async function runWithdrawalHandoverTest(t: GlobalTestState) { // Create a withdrawal operation - const bankAccessApiClient = new TalerCorebankApiClient( - bank.corebankApiBaseUrl, - ); - const user = await bankAccessApiClient.createRandomBankUser(); - bankAccessApiClient.setAuth(user); - const wop = await bankAccessApiClient.createWithdrawalOperation( + const user = await bankClient.createRandomBankUser(); + const userBankClient = new TalerCorebankApiClient(bankClient.baseUrl); + userBankClient.setAuth(user); + const amount = "TESTKUDOS:10"; + + const wop = await userBankClient.createWithdrawalOperation( user.username, - "TESTKUDOS:10", + amount, ); const checkResp = await walletClient.call( @@ -134,7 +137,7 @@ export async function runWithdrawalHandoverTest(t: GlobalTestState) { const prepareRespW1 = await walletClient.call( WalletApiOperation.PrepareBankIntegratedWithdrawal, { - exchangeBaseUrl: checkResp.defaultExchangeBaseUrl, + // exchangeBaseUrl: checkResp.defaultExchangeBaseUrl, talerWithdrawUri: wop.taler_withdraw_uri, }, ); @@ -142,13 +145,17 @@ export async function runWithdrawalHandoverTest(t: GlobalTestState) { const prepareRespW2 = await w2.walletClient.call( WalletApiOperation.PrepareBankIntegratedWithdrawal, { - exchangeBaseUrl: checkResp.defaultExchangeBaseUrl, + // exchangeBaseUrl: checkResp.defaultExchangeBaseUrl, talerWithdrawUri: wop.taler_withdraw_uri, }, ); + t.assertTrue(!!prepareRespW2.transactionId); + await w2.walletClient.call(WalletApiOperation.ConfirmWithdrawal, { transactionId: prepareRespW2.transactionId, + amount, + exchangeBaseUrl: checkResp.defaultExchangeBaseUrl, }); await w2.walletClient.call(WalletApiOperation.TestingWaitTransactionState, { @@ -159,7 +166,7 @@ export async function runWithdrawalHandoverTest(t: GlobalTestState) { }, }); - await bankAccessApiClient.confirmWithdrawalOperation(user.username, { + await userBankClient.confirmWithdrawalOperation(user.username, { withdrawalOperationId: wop.withdrawal_id, }); diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-huge.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-huge.ts @@ -21,15 +21,16 @@ import { GlobalTestState, setupDb, ExchangeService, - FakebankService, WalletService, WalletClient, + BankService, } from "../harness/harness.js"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; import { AmountString, NotificationType, + TalerCorebankApiClient, TransactionMajorState, URL, } from "@gnu-taler/taler-util"; @@ -45,7 +46,7 @@ export async function runWithdrawalHugeTest(t: GlobalTestState) { const db = await setupDb(t); - const bank = await FakebankService.create(t, { + const bank = await BankService.create(t, { currency: "TESTKUDOS", httpPort: 8082, allowRegistrations: true, @@ -60,17 +61,36 @@ export async function runWithdrawalHugeTest(t: GlobalTestState) { database: db.connStr, }); - exchange.addBankAccount("1", { + let paytoUri = "payto://x-taler-bank/localhost/exchange"; + + await exchange.addBankAccount("1", { accountName: "exchange", accountPassword: "x", - wireGatewayApiBaseUrl: new URL("/exchange/", bank.baseUrl).href, - accountPaytoUri: "payto://x-taler-bank/localhost/exchange", + wireGatewayApiBaseUrl: new URL("accounts/exchange/taler-wire-gateway/", bank.baseUrl).href, + accountPaytoUri: paytoUri, }); + bank.setSuggestedExchange(exchange, paytoUri); + await bank.start(); await bank.pingUntilAvailable(); + const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, { + auth: { + username: "admin", + password: "adminpw", + } + }); + + await bankClient.registerAccountExtended({ + name: "Exchange", + password: "x", + username: "exchange", + is_taler_exchange: true, + payto_uri: paytoUri, + }); + const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")); exchange.addCoinConfigList(coinConfig); diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-manual.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-manual.ts @@ -27,7 +27,7 @@ import { } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState } from "../harness/harness.js"; -import { createSimpleTestkudosEnvironmentV2 } from "../harness/helpers.js"; +import { createSimpleTestkudosEnvironmentV3 } from "../harness/helpers.js"; const logger = new Logger("test-withdrawal-manual.ts"); @@ -37,16 +37,12 @@ const logger = new Logger("test-withdrawal-manual.ts"); export async function runWithdrawalManualTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bank, exchange, exchangeBankAccount } = - await createSimpleTestkudosEnvironmentV2(t); + const { walletClient, bankClient, exchange, exchangeBankAccount } = + await createSimpleTestkudosEnvironmentV3(t); // 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, @@ -80,8 +76,8 @@ export async function runWithdrawalManualTest(t: GlobalTestState) { exchangeBankAccount.wireGatewayApiBaseUrl, { auth: { - username: exchangeBankAccount.accountName, - password: exchangeBankAccount.accountPassword, + username: "admin", + password: "adminpw", }, }, ); diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -120,6 +120,7 @@ import { runWithdrawalFeesTest } from "./test-withdrawal-fees.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. @@ -230,6 +231,7 @@ const allTests: TestMainFunction[] = [ runPeerPullLargeTest, runPeerPushLargeTest, runWithdrawalHandoverTest, + runWithdrawalAmountTest, ]; export interface TestRunSpec { diff --git a/packages/taler-util/src/MerchantApiClient.ts b/packages/taler-util/src/MerchantApiClient.ts @@ -19,6 +19,7 @@ import { TalerMerchantApi, codecForMerchantConfig, codecForMerchantOrderPrivateStatusResponse, + codecForPostOrderResponse, } from "./http-client/types.js"; import { HttpStatusCode } from "./http-status-codes.js"; import { @@ -31,13 +32,6 @@ import { FacadeCredentials } from "./libeufin-api-types.js"; import { LibtoolVersion } from "./libtool-version.js"; import { Logger } from "./logging.js"; import { - MerchantInstancesResponse, - MerchantPostOrderRequest, - MerchantPostOrderResponse, - MerchantTemplateAddDetails, - codecForMerchantPostOrderResponse, -} from "./merchant-api-types.js"; -import { FailCasesByMethod, OperationFail, OperationOk, @@ -206,7 +200,7 @@ export class MerchantApiClient { }); } - async getInstances(): Promise<MerchantInstancesResponse> { + async getInstances(): Promise<TalerMerchantApi.InstancesResponse> { const url = new URL("management/instances", this.baseUrl); const resp = await this.httpClient.fetch(url.href, { headers: this.makeAuthHeader(), @@ -227,18 +221,15 @@ export class MerchantApiClient { } async createOrder( - req: MerchantPostOrderRequest, - ): Promise<MerchantPostOrderResponse> { + req: TalerMerchantApi.PostOrderRequest, + ): Promise<TalerMerchantApi.PostOrderResponse> { let url = new URL("private/orders", this.baseUrl); const resp = await this.httpClient.fetch(url.href, { method: "POST", body: req, headers: this.makeAuthHeader(), }); - return readSuccessResponseJsonOrThrow( - resp, - codecForMerchantPostOrderResponse(), - ); + return readSuccessResponseJsonOrThrow(resp, codecForPostOrderResponse()); } async deleteOrder(req: { orderId: string; force?: boolean }): Promise<void> { @@ -292,7 +283,7 @@ export class MerchantApiClient { }; } - async createTemplate(req: MerchantTemplateAddDetails) { + async createTemplate(req: TalerMerchantApi.MerchantTemplateAddDetails) { let url = new URL("private/templates", this.baseUrl); const resp = await this.httpClient.fetch(url.href, { method: "POST", diff --git a/packages/taler-util/src/bank-api-client.ts b/packages/taler-util/src/bank-api-client.ts @@ -43,8 +43,10 @@ import { import { checkSuccessResponseOrThrow, createPlatformHttpLib, + expectSuccessResponseOrThrow, HttpRequestLibrary, readSuccessResponseJsonOrThrow, + readSuccessResponseTextOrThrow, readTalerErrorResponse, } from "@gnu-taler/taler-util/http"; @@ -238,7 +240,7 @@ export class TalerCorebankApiClient { httpLib: HttpRequestLibrary; constructor( - private baseUrl: string, + public baseUrl: string, private args: BankAccessApiClientArgs = {}, ) { this.httpLib = args.httpClient ?? createPlatformHttpLib(); @@ -437,4 +439,20 @@ export class TalerCorebankApiClient { }); await readSuccessResponseJsonOrThrow(resp, codecForAny()); } + + async abortWithdrawalOperationV2( + username: string, + wopi: WithdrawalOperationInfo, + ): Promise<void> { + const url = new URL( + `accounts/${username}/withdrawals/${wopi.withdrawal_id}/abort`, + this.baseUrl, + ); + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body: {}, + headers: this.makeAuthHeader(), + }); + await expectSuccessResponseOrThrow(resp); + } } diff --git a/packages/taler-util/src/codec.ts b/packages/taler-util/src/codec.ts @@ -146,7 +146,7 @@ class UnionCodecBuilder< constructor( private discriminator: TagPropertyLabel, private baseCodec?: Codec<CommonBaseType>, - ) {} + ) { } /** * Define a property for the object. @@ -491,6 +491,17 @@ export function codecOptional<V>(innerCodec: Codec<V>): Codec<V | undefined> { }; } +export function codecOptionalDefault<V>(innerCodec: Codec<V>, def: V): Codec<V> { + return { + decode(x: any, c?: Context): V { + if (x === undefined || x === null) { + return def; + } + return innerCodec.decode(x, c); + }, + }; +} + export function codecForLazy<V>(innerCodec: () => Codec<V>): Codec<V> { let instance: Codec<V> | undefined = undefined return { diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts @@ -32,6 +32,7 @@ import { codecForInventorySummaryResponse, codecForMerchantConfig, codecForMerchantOrderPrivateStatusResponse, + codecForMerchantPosProductDetail, codecForMerchantRefundResponse, codecForOrderHistory, codecForOtpDeviceDetails, @@ -122,7 +123,7 @@ export enum TalerMerchantManagementCacheEviction { * Uses libtool's current:revision:age versioning. */ export class TalerMerchantInstanceHttpClient { - public readonly PROTOCOL_VERSION = "10:0:6"; + public readonly PROTOCOL_VERSION = "15:0:0"; readonly httpLib: HttpRequestLibrary; readonly cacheEvictor: CacheEvictor<TalerMerchantInstanceCacheEviction>; @@ -859,6 +860,32 @@ export class TalerMerchantInstanceHttpClient { } /** + * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-pos + */ + async getPointOfSaleInventory(token: AccessToken | undefined) { + const url = new URL(`private/pos`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForMerchantPosProductDetail()); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + + } + + /** * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-products-$PRODUCT_ID */ async getProductDetails(token: AccessToken | undefined, productId: string) { @@ -1611,6 +1638,8 @@ export class TalerMerchantInstanceHttpClient { ) { const url = new URL(`private/templates`, this.baseUrl); + addMerchantPaginationParams(url, params); + const headers: Record<string, string> = {}; if (token) { headers.Authorization = makeBearerTokenAuthHeader(token); diff --git a/packages/taler-util/src/http-client/types.ts b/packages/taler-util/src/http-client/types.ts @@ -1,4 +1,3 @@ -import { deprecate } from "util"; import { codecForAmountString } from "../amounts.js"; import { Codec, @@ -14,11 +13,14 @@ import { codecForNumber, codecForString, codecOptional, + codecOptionalDefault, } from "../codec.js"; import { PaytoString, codecForPaytoString } from "../payto.js"; import { AmountString, + ExchangeWireAccount, InternationalizedString, + codecForExchangeWireAccount, codecForInternationalizedString, codecForLocation, } from "../taler-types.js"; @@ -27,7 +29,6 @@ import { AbsoluteTime, TalerProtocolDuration, TalerProtocolTimestamp, - codecForAbsoluteTime, codecForDuration, codecForTimestamp, } from "../time.js"; @@ -340,7 +341,7 @@ export const codecForCoreBankConfig = (): Codec<TalerCorebankApi.Config> => .property("name", codecForConstString("libeufin-bank")) .property("version", codecForString()) .property("bank_name", codecForString()) - .property("base_url", codecForString()) + .property("base_url", codecOptional(codecForString())) .property("allow_conversion", codecForBoolean()) .property("allow_registrations", codecForBoolean()) .property("allow_deletions", codecForBoolean()) @@ -358,7 +359,7 @@ export const codecForCoreBankConfig = (): Codec<TalerCorebankApi.Config> => ), ), ) - .property("wire_type", codecForString()) + .property("wire_type", codecOptionalDefault(codecForString(), "iban")) .build("TalerCorebankApi.Config"); //FIXME: implement this codec @@ -603,6 +604,37 @@ export const codecForInventoryEntry = .property("product_serial", codecForNumber()) .build("TalerMerchantApi.InventoryEntry"); +export const codecForMerchantPosProductDetail = + (): Codec<TalerMerchantApi.MerchantPosProductDetail> => + buildCodecForObject<TalerMerchantApi.MerchantPosProductDetail>() + .property("product_serial", codecForNumber()) + .property("product_id", codecOptional(codecForString())) + .property("categories", codecForList(codecForNumber())) + .property("description", codecForString()) + .property("description_i18n", codecForInternationalizedString()) + .property("unit", codecForString()) + .property("price", codecForAmountString()) + .property("image", codecForString()) + .property("taxes", codecOptional(codecForList(codecForTax()))) + .property("total_stock", codecForNumber()) + .property("minimum_age", codecOptional(codecForNumber())) + .build("TalerMerchantApi.MerchantPosProductDetail"); + +export const codecForMerchantCategory = + (): Codec<TalerMerchantApi.MerchantCategory> => + buildCodecForObject<TalerMerchantApi.MerchantCategory>() + .property("id", codecForNumber()) + .property("name", codecForString()) + .property("name_i18n", codecForInternationalizedString()) + .build("TalerMerchantApi.MerchantCategory"); + +export const codecForFullInventoryDetailsResponse = + (): Codec<TalerMerchantApi.FullInventoryDetailsResponse> => + buildCodecForObject<TalerMerchantApi.FullInventoryDetailsResponse>() + .property("categories", codecForList(codecForMerchantCategory())) + .property("products", codecForList(codecForMerchantPosProductDetail())) + .build("TalerMerchantApi.FullInventoryDetailsResponse"); + export const codecForProductDetail = (): Codec<TalerMerchantApi.ProductDetail> => buildCodecForObject<TalerMerchantApi.ProductDetail>() @@ -611,9 +643,9 @@ export const codecForProductDetail = .property("unit", codecForString()) .property("price", codecForAmountString()) .property("image", codecForString()) - .property("taxes", codecForList(codecForTax())) - .property("address", codecForLocation()) - .property("next_restock", codecForTimestamp) + .property("taxes", codecOptional(codecForList(codecForTax()))) + .property("address", codecOptional(codecForLocation())) + .property("next_restock", codecOptional(codecForTimestamp)) .property("total_stock", codecForNumber()) .property("total_sold", codecForNumber()) .property("total_lost", codecForNumber()) @@ -893,8 +925,6 @@ export const codecForTemplateContractDetailsDefaults = .property("summary", codecOptional(codecForString())) .property("currency", codecOptional(codecForString())) .property("amount", codecOptional(codecForAmountString())) - .property("minimum_age", codecOptional(codecForNumber())) - .property("pay_duration", codecOptional(codecForDuration)) .build("TalerMerchantApi.TemplateContractDetailsDefaults"); export const codecForWalletTemplateDetails = @@ -1587,6 +1617,21 @@ export const codecForChallengerInfoResponse = .property("expires", codecForTimestamp) .build("ChallengerApi.ChallengerInfoResponse"); +export const codecForTemplateEditableDetails = + (): Codec<TalerMerchantApi.TemplateEditableDetails> => + buildCodecForObject<TalerMerchantApi.TemplateEditableDetails>() + .property("summary", codecOptional(codecForString())) + .property("currency", codecOptional(codecForString())) + .property("amount", codecOptional(codecForAmountString())) + .build("TemplateEditableDetails"); + +export const codecForMerchantReserveCreateConfirmation = + (): Codec<TalerMerchantApi.MerchantReserveCreateConfirmation> => + buildCodecForObject<TalerMerchantApi.MerchantReserveCreateConfirmation>() + .property("accounts", codecForList(codecForExchangeWireAccount())) + .property("reserve_pub", codecForString()) + .build("MerchantReserveCreateConfirmation"); + type EmailAddress = string; type PhoneNumber = string; type EddsaSignature = string; @@ -2394,7 +2439,7 @@ export namespace TalerCorebankApi { // Is 2FA enabled and what channel is used for challenges? tan_channel?: TanChannel; - + // Current status of the account // active: the account can be used // deleted: the account has been deleted but is retained for compliance @@ -4068,6 +4113,68 @@ export namespace TalerMerchantApi { product_serial: Integer; } + export interface FullInventoryDetailsResponse { + // List of products that are present in the inventory. + products: MerchantPosProductDetail[]; + + // List of categories in the inventory. + categories: MerchantCategory[]; + } + + export interface MerchantPosProductDetail { + // A unique numeric ID of the product + product_serial: number; + + // A merchant-internal unique identifier for the product + product_id?: string; + + // A list of category IDs this product belongs to. + // Typically, a product only belongs to one category, but more than one is supported. + categories: number[]; + + // Human-readable product description. + description: string; + + // Map from IETF BCP 47 language tags to localized descriptions. + description_i18n: { [lang_tag: string]: string }; + + // Unit in which the product is measured (liters, kilograms, packages, etc.). + unit: string; + + // The price for one unit of the product. Zero is used + // to imply that this product is not sold separately, or + // that the price is not fixed, and must be supplied by the + // front-end. If non-zero, this price MUST include applicable + // taxes. + price: AmountString; + + // An optional base64-encoded product image. + image?: ImageDataUrl; + + // A list of taxes paid by the merchant for one unit of this product. + taxes?: Tax[]; + + // Number of units of the product in stock in sum in total, + // including all existing sales ever. Given in product-specific + // units. + // Optional, if missing treat as "infinite". + total_stock?: Integer; + + // Minimum age buyer must have (in years). + minimum_age?: Integer; + } + + export interface MerchantCategory { + // A unique numeric ID of the category + id: number; + + // The name of the category. This will be shown to users and used in the order summary. + name: string; + + // Map from IETF BCP 47 language tags to localized names + name_i18n?: { [lang_tag: string]: string }; + } + export interface ProductDetail { // Human-readable product description. description: string; @@ -4089,7 +4196,7 @@ export namespace TalerMerchantApi { image: ImageDataUrl; // A list of taxes paid by the merchant for one unit of this product. - taxes: Tax[]; + taxes?: Tax[]; // Number of units of the product in stock in sum in total, // including all existing sales ever. Given in product-specific @@ -4104,7 +4211,7 @@ export namespace TalerMerchantApi { total_lost: Integer; // Identifies where the product is in stock. - address: Location; + address?: Location; // Identifies when we expect the next restocking to happen. next_restock?: Timestamp; @@ -4165,9 +4272,9 @@ export namespace TalerMerchantApi { otp_id?: string; } - type Order = MinimalOrderDetail | ContractTerms; + export type Order = MinimalOrderDetail & Partial<ContractTerms>; - interface MinimalOrderDetail { + export interface MinimalOrderDetail { // Amount to be paid by the customer. amount: AmountString; @@ -4186,7 +4293,7 @@ export namespace TalerMerchantApi { fulfillment_message?: string; } - interface MinimalInventoryProduct { + export interface MinimalInventoryProduct { // Which product is requested (here mandatory!). product_id: string; @@ -4464,7 +4571,6 @@ export namespace TalerMerchantApi { confirmed?: boolean; } - export interface OtpDeviceAddDetails { // Device ID to use. otp_device_id: string; @@ -4627,12 +4733,12 @@ export namespace TalerMerchantApi { currency?: string; - amount?: AmountString; - - minimum_age?: Integer; - - pay_duration?: RelativeTime; + /** + * Amount *or* a plain currency string. + */ + amount?: string; } + export interface TemplatePatchDetails { // Human-readable description for the template. template_description: string; @@ -5158,6 +5264,68 @@ export namespace TalerMerchantApi { // Master public key of the exchange. master_pub: EddsaPublicKey; } + + export interface MerchantReserveCreateConfirmation { + // Public key identifying the reserve. + reserve_pub: EddsaPublicKey; + + // Wire accounts of the exchange where to transfer the funds. + accounts: ExchangeWireAccount[]; + } + + export interface TemplateEditableDetails { + // Human-readable summary for the template. + summary?: string; + + // Required currency for payments to the template. + // The user may specify any amount, but it must be + // in this currency. + // This parameter is optional and should not be present + // if "amount" is given. + currency?: string; + + // The price is imposed by the merchant and cannot be changed by the customer. + // This parameter is optional. + amount?: AmountString; + } + + export interface MerchantTemplateContractDetails { + // Human-readable summary for the template. + summary?: string; + + // The price is imposed by the merchant and cannot be changed by the customer. + // This parameter is optional. + amount?: string; + + // Minimum age buyer must have (in years). Default is 0. + minimum_age: number; + + // The time the customer need to pay before his order will be deleted. + // It is deleted if the customer did not pay and if the duration is over. + pay_duration: TalerProtocolDuration; + } + + export interface MerchantTemplateAddDetails { + // Template ID to use. + template_id: string; + + // Human-readable description for the template. + template_description: string; + + // A base64-encoded image selected by the merchant. + // This parameter is optional. + // We are not sure about it. + image?: string; + + editable_defaults?: TemplateEditableDetails; + + // Additional information in a separate template. + template_contract: MerchantTemplateContractDetails; + + // OTP device ID. + // This parameter is optional. + otp_id?: string; + } } export namespace ChallengerApi { diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts @@ -18,18 +18,18 @@ export * from "./contract-terms.js"; export * from "./errors.js"; export { fnutil } from "./fnutils.js"; export * from "./helpers.js"; -export * from "./http-client/bank-conversion.js"; export * from "./http-client/authentication.js"; +export * from "./http-client/bank-conversion.js"; export * from "./http-client/bank-core.js"; -export * from "./http-client/merchant.js"; -export * from "./http-client/challenger.js"; export * from "./http-client/bank-integration.js"; export * from "./http-client/bank-revenue.js"; export * from "./http-client/bank-wire.js"; +export * from "./http-client/challenger.js"; export * from "./http-client/exchange.js"; -export { CacheEvictor } from "./http-client/utils.js"; +export * from "./http-client/merchant.js"; export * from "./http-client/officer-account.js"; export * from "./http-client/types.js"; +export { CacheEvictor } from "./http-client/utils.js"; export * from "./http-status-codes.js"; export * from "./i18n.js"; export * from "./iban.js"; @@ -38,7 +38,6 @@ export * from "./kdf.js"; export * from "./libeufin-api-types.js"; export * from "./libtool-version.js"; export * from "./logging.js"; -export * from "./merchant-api-types.js"; export { crypto_sign_keyPair_fromSeed, randomBytes, diff --git a/packages/taler-util/src/merchant-api-types.ts b/packages/taler-util/src/merchant-api-types.ts @@ -1,352 +0,0 @@ -/* - 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/> - */ - -/** - * Test harness for various GNU Taler components. - * Also provides a fault-injection proxy. - * - * @author Florian Dold <dold@taler.net> - */ - -/** - * Imports. - */ -import { - AbsoluteTime, - AmountString, - Codec, - CoinPublicKeyString, - EddsaPublicKeyString, - ExchangeWireAccount, - FacadeCredentials, - MerchantContractTerms, - TalerProtocolDuration, - TalerProtocolTimestamp, - buildCodecForObject, - buildCodecForUnion, - codecForAmountString, - codecForAny, - codecForBoolean, - codecForCheckPaymentClaimedResponse, - codecForCheckPaymentUnpaidResponse, - codecForConstString, - codecForExchangeWireAccount, - codecForList, - codecForMerchantContractTerms, - codecForNumber, - codecForString, - codecForTimestamp, - codecOptional, -} from "@gnu-taler/taler-util"; - -export interface MerchantPostOrderRequest { - // The order must at least contain the minimal - // order detail, but can override all - order: Partial<MerchantContractTerms>; - - // if set, the backend will then set the refund deadline to the current - // time plus the specified delay. - refund_delay?: TalerProtocolDuration; - - // specifies the payment target preferred by the client. Can be used - // to select among the various (active) wire methods supported by the instance. - payment_target?: string; - - // FIXME: some fields are missing - - // Should a token for claiming the order be generated? - // False can make sense if the ORDER_ID is sufficiently - // high entropy to prevent adversarial claims (like it is - // if the backend auto-generates one). Default is 'true'. - create_token?: boolean; -} - -export type ClaimToken = string; - -export interface MerchantPostOrderResponse { - order_id: string; - token?: ClaimToken; -} - -export const codecForMerchantPostOrderResponse = - (): Codec<MerchantPostOrderResponse> => - buildCodecForObject<MerchantPostOrderResponse>() - .property("order_id", codecForString()) - .property("token", codecOptional(codecForString())) - .build("PostOrderResponse"); - -export const codecForMerchantRefundDetails = (): Codec<RefundDetails> => - buildCodecForObject<RefundDetails>() - .property("reason", codecForString()) - .property("pending", codecForBoolean()) - .property("amount", codecForAmountString()) - .property("timestamp", codecForTimestamp) - .build("PostOrderResponse"); - -export const codecForMerchantCheckPaymentPaidResponse = - (): Codec<MerchantCheckPaymentPaidResponse> => - buildCodecForObject<MerchantCheckPaymentPaidResponse>() - .property("order_status_url", codecForString()) - .property("order_status", codecForConstString("paid")) - .property("refunded", codecForBoolean()) - .property("wired", codecForBoolean()) - .property("deposit_total", codecForAmountString()) - .property("exchange_ec", codecForNumber()) - .property("exchange_hc", codecForNumber()) - .property("refund_amount", codecForAmountString()) - .property("contract_terms", codecForMerchantContractTerms()) - // FIXME: specify - .property("wire_details", codecForAny()) - .property("wire_reports", codecForAny()) - .property("refund_details", codecForAny()) - .build("CheckPaymentPaidResponse"); - -export type MerchantOrderPrivateStatusResponse = - | MerchantCheckPaymentPaidResponse - | CheckPaymentUnpaidResponse - | CheckPaymentClaimedResponse; - -export interface CheckPaymentClaimedResponse { - // Wallet claimed the order, but didn't pay yet. - order_status: "claimed"; - - contract_terms: MerchantContractTerms; -} - -export interface MerchantCheckPaymentPaidResponse { - // did the customer pay for this contract - order_status: "paid"; - - // Was the payment refunded (even partially) - refunded: boolean; - - // Did the exchange wire us the funds - wired: boolean; - - // Total amount the exchange deposited into our bank account - // for this contract, excluding fees. - deposit_total: AmountString; - - // Numeric error code indicating errors the exchange - // encountered tracking the wire transfer for this purchase (before - // we even got to specific coin issues). - // 0 if there were no issues. - exchange_ec: number; - - // HTTP status code returned by the exchange when we asked for - // information to track the wire transfer for this purchase. - // 0 if there were no issues. - exchange_hc: number; - - // Total amount that was refunded, 0 if refunded is false. - refund_amount: AmountString; - - // Contract terms - contract_terms: MerchantContractTerms; - - // Ihe wire transfer status from the exchange for this order if available, otherwise empty array - wire_details: TransactionWireTransfer[]; - - // Reports about trouble obtaining wire transfer details, empty array if no trouble were encountered. - wire_reports: TransactionWireReport[]; - - // The refund details for this order. One entry per - // refunded coin; empty array if there are no refunds. - refund_details: RefundDetails[]; - - order_status_url: string; -} - -export interface CheckPaymentUnpaidResponse { - order_status: "unpaid"; - - // URI that the wallet must process to complete the payment. - taler_pay_uri: string; - - order_status_url: string; - - // Alternative order ID which was paid for already in the same session. - // Only given if the same product was purchased before in the same session. - already_paid_order_id?: string; - - // We do we NOT return the contract terms here because they may not - // exist in case the wallet did not yet claim them. -} - -export interface RefundDetails { - // Reason given for the refund - reason: string; - - // when was the refund approved - timestamp: TalerProtocolTimestamp; - - // has not been taken yet - pending: boolean; - - // Total amount that was refunded (minus a refund fee). - amount: AmountString; -} - -export interface TransactionWireTransfer { - // Responsible exchange - exchange_url: string; - - // 32-byte wire transfer identifier - wtid: string; - - // execution time of the wire transfer - execution_time: AbsoluteTime; - - // Total amount that has been wire transferred - // to the merchant - amount: AmountString; - - // Was this transfer confirmed by the merchant via the - // POST /transfers API, or is it merely claimed by the exchange? - confirmed: boolean; -} - -export interface TransactionWireReport { - // Numerical error code - code: number; - - // Human-readable error description - hint: string; - - // Numerical error code from the exchange. - exchange_ec: number; - - // HTTP status code received from the exchange. - exchange_hc: number; - - // Public key of the coin for which we got the exchange error. - coin_pub: CoinPublicKeyString; -} - -export interface ReserveStatusEntry { - // Public key of the reserve - reserve_pub: string; - - // Timestamp when it was established - creation_time: AbsoluteTime; - - // Timestamp when it expires - expiration_time: AbsoluteTime; - - // Initial amount as per reserve creation call - merchant_initial_amount: AmountString; - - // Initial amount as per exchange, 0 if exchange did - // not confirm reserve creation yet. - exchange_initial_amount: AmountString; - - // Amount picked up so far. - pickup_amount: AmountString; - - // Amount approved for tips that exceeds the pickup_amount. - committed_amount: AmountString; - - // Is this reserve active (false if it was deleted but not purged) - active: boolean; -} - -export interface MerchantInstancesResponse { - // List of instances that are present in the backend (see Instance) - instances: MerchantInstanceDetail[]; -} - -export interface MerchantInstanceDetail { - // Merchant name corresponding to this instance. - name: string; - - // Merchant instance this response is about ($INSTANCE) - id: string; - - // Public key of the merchant/instance, in Crockford Base32 encoding. - merchant_pub: EddsaPublicKeyString; - - // List of the payment targets supported by this instance. Clients can - // specify the desired payment target in /order requests. Note that - // front-ends do not have to support wallets selecting payment targets. - payment_targets: string[]; -} - -export interface MerchantTemplateContractDetails { - // Human-readable summary for the template. - summary?: string; - - // The price is imposed by the merchant and cannot be changed by the customer. - // This parameter is optional. - amount?: string; - - // Minimum age buyer must have (in years). Default is 0. - minimum_age: number; - - // The time the customer need to pay before his order will be deleted. - // It is deleted if the customer did not pay and if the duration is over. - pay_duration: TalerProtocolDuration; -} - -export interface MerchantTemplateAddDetails { - // Template ID to use. - template_id: string; - - // Human-readable description for the template. - template_description: string; - - // A base64-encoded image selected by the merchant. - // This parameter is optional. - // We are not sure about it. - image?: string; - - // Additional information in a separate template. - template_contract: MerchantTemplateContractDetails; - - // OTP device ID. - // This parameter is optional. - otp_id?: string; -} - -export interface MerchantReserveCreateConfirmation { - // Public key identifying the reserve. - reserve_pub: EddsaPublicKeyString; - - // Wire accounts of the exchange where to transfer the funds. - accounts: ExchangeWireAccount[]; -} - -export const codecForMerchantReserveCreateConfirmation = - (): Codec<MerchantReserveCreateConfirmation> => - buildCodecForObject<MerchantReserveCreateConfirmation>() - .property("accounts", codecForList(codecForExchangeWireAccount())) - .property("reserve_pub", codecForString()) - .build("MerchantReserveCreateConfirmation"); - -export interface AccountAddDetails { - // payto:// URI of the account. - payto_uri: string; - - // URL from where the merchant can download information - // about incoming wire transfers to this account. - credit_facade_url?: string; - - // Credentials to use when accessing the credit facade. - // Never returned on a GET (as this may be somewhat - // sensitive data). Can be set in POST - // or PATCH requests to update (or delete) credentials. - // To really delete credentials, set them to the type: "none". - credit_facade_credentials?: FacadeCredentials; -} diff --git a/packages/taler-util/src/taler-crypto.ts b/packages/taler-util/src/taler-crypto.ts @@ -21,23 +21,23 @@ /** * Imports. */ -import * as nacl from "./nacl-fast.js"; -import { hmacSha256, hmacSha512 } from "./kdf.js"; import bigint from "big-integer"; +import * as fflate from "fflate"; +import { AmountLike, Amounts } from "./amounts.js"; import * as argon2 from "./argon2.js"; +import { canonicalJson } from "./helpers.js"; +import { hmacSha256, hmacSha512 } from "./kdf.js"; +import { Logger } from "./logging.js"; +import * as nacl from "./nacl-fast.js"; +import { secretbox } from "./nacl-fast.js"; import { CoinEnvelope, CoinPublicKeyString, - DenominationPubKey, DenomKeyType, + DenominationPubKey, HashCodeString, } from "./taler-types.js"; -import { Logger } from "./logging.js"; -import { secretbox } from "./nacl-fast.js"; -import * as fflate from "fflate"; -import { canonicalJson } from "./helpers.js"; import { TalerProtocolDuration, TalerProtocolTimestamp } from "./time.js"; -import { AmountLike, Amounts } from "./amounts.js"; export type Flavor<T, FlavorT extends string> = T & { _flavor?: `taler.${FlavorT}`; @@ -974,6 +974,7 @@ export function hashWire(paytoUri: string, salt: string): string { export enum TalerSignaturePurpose { MERCHANT_TRACK_TRANSACTION = 1103, WALLET_RESERVE_WITHDRAW = 1200, + WALLET_RESERVE_HISTORY = 1208, WALLET_COIN_DEPOSIT = 1201, GLOBAL_FEES = 1022, MASTER_DENOMINATION_KEY_VALIDITY = 1025, diff --git a/packages/taler-util/src/taler-types.ts b/packages/taler-util/src/taler-types.ts @@ -1329,8 +1329,12 @@ export const codecForDenominationPubKey = () => .alternative(DenomKeyType.ClauseSchnorr, codecForCsDenominationPubKey()) .build("DenominationPubKey"); +export type LitAmountString = `${string}:${number}`; + declare const __amount_str: unique symbol; -export type AmountString = string & { [__amount_str]: true }; +export type AmountString = + | (string & { [__amount_str]: true }) + | LitAmountString; // export type AmountString = string; export type Base32String = string; export type EddsaSignatureString = string; diff --git a/packages/taler-util/src/taleruri.test.ts b/packages/taler-util/src/taleruri.test.ts @@ -314,7 +314,7 @@ test("taler peer to peer pull URI (stringify)", (t) => { test("taler pay template URI (parsing)", (t) => { const url1 = - "taler://pay-template/merchant.example.com/FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY?amount=KUDOS:5"; + "taler://pay-template/merchant.example.com/FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY"; const r1 = parsePayTemplateUri(url1); if (!r1) { t.fail(); @@ -322,12 +322,11 @@ test("taler pay template URI (parsing)", (t) => { } t.deepEqual(r1.merchantBaseUrl, "https://merchant.example.com/"); t.deepEqual(r1.templateId, "FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY"); - t.deepEqual(r1.templateParams.amount, "KUDOS:5"); }); test("taler pay template URI (parsing, http with port)", (t) => { const url1 = - "taler+http://pay-template/merchant.example.com:1234/FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY?amount=KUDOS:5"; + "taler+http://pay-template/merchant.example.com:1234/FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY"; const r1 = parsePayTemplateUri(url1); if (!r1) { t.fail(); @@ -335,20 +334,16 @@ test("taler pay template URI (parsing, http with port)", (t) => { } t.deepEqual(r1.merchantBaseUrl, "http://merchant.example.com:1234/"); t.deepEqual(r1.templateId, "FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY"); - t.deepEqual(r1.templateParams.amount, "KUDOS:5"); }); test("taler pay template URI (stringify)", (t) => { const url1 = stringifyPayTemplateUri({ merchantBaseUrl: "http://merchant.example.com:1234/", templateId: "FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY", - templateParams: { - amount: "KUDOS:5", - }, }); t.deepEqual( url1, - "taler+http://pay-template/merchant.example.com:1234/FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY?amount=KUDOS%3A5", + "taler+http://pay-template/merchant.example.com:1234/FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY", ); }); @@ -423,24 +418,27 @@ test("taler dev exp URI (stringify)", (t) => { */ test("taler withdraw exchange URI (parse)", (t) => { + // Pubkey has been phased out, may no longer be specified. { - const r1 = parseWithdrawExchangeUri( + const rx1 = parseWithdrawExchangeUri( "taler://withdraw-exchange/exchange.demo.taler.net/someroot/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0?a=KUDOS%3A2", ); - if (!r1) { + if (rx1) { t.fail(); return; } - t.deepEqual( - r1.exchangePub, - "GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0", - ); - t.deepEqual( - r1.exchangeBaseUrl, - "https://exchange.demo.taler.net/someroot/", + } + { + const rx2 = parseWithdrawExchangeUri( + "taler://withdraw-exchange/exchange.demo.taler.net/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0", ); - t.deepEqual(r1.amount, "KUDOS:2"); + if (rx2) { + t.fail(); + return; + } } + + // Now test well-formed URIs { const r2 = parseWithdrawExchangeUri( "taler://withdraw-exchange/exchange.demo.taler.net/someroot/", @@ -449,7 +447,6 @@ test("taler withdraw exchange URI (parse)", (t) => { t.fail(); return; } - t.deepEqual(r2.exchangePub, undefined); t.deepEqual(r2.amount, undefined); t.deepEqual( r2.exchangeBaseUrl, @@ -465,7 +462,6 @@ test("taler withdraw exchange URI (parse)", (t) => { t.fail(); return; } - t.deepEqual(r3.exchangePub, undefined); t.deepEqual(r3.amount, undefined); t.deepEqual(r3.exchangeBaseUrl, "https://exchange.demo.taler.net/"); } @@ -479,7 +475,6 @@ test("taler withdraw exchange URI (parse)", (t) => { t.fail(); return; } - t.deepEqual(r4.exchangePub, undefined); t.deepEqual(r4.amount, undefined); t.deepEqual(r4.exchangeBaseUrl, "https://exchange.demo.taler.net/"); } @@ -488,27 +483,21 @@ test("taler withdraw exchange URI (parse)", (t) => { test("taler withdraw exchange URI (stringify)", (t) => { const url = stringifyWithdrawExchange({ exchangeBaseUrl: "https://exchange.demo.taler.net", - exchangePub: "GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0", }); - t.deepEqual( - url, - "taler://withdraw-exchange/exchange.demo.taler.net/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0", - ); + t.deepEqual(url, "taler://withdraw-exchange/exchange.demo.taler.net/"); }); test("taler withdraw exchange URI with amount (stringify)", (t) => { const url = stringifyWithdrawExchange({ exchangeBaseUrl: "https://exchange.demo.taler.net", - exchangePub: "GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0", amount: "KUDOS:19" as AmountString, }); t.deepEqual( url, - "taler://withdraw-exchange/exchange.demo.taler.net/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0?a=KUDOS%3A19", + "taler://withdraw-exchange/exchange.demo.taler.net/?a=KUDOS%3A19", ); }); - /** * 5.13 action: add-exchange https://lsd.gnunet.org/lsd0006/#name-action-add-exchange */ @@ -522,10 +511,7 @@ test("taler add exchange URI (parse)", (t) => { t.fail(); return; } - t.deepEqual( - r1.exchangeBaseUrl, - "https://exchange.example.com/", - ); + t.deepEqual(r1.exchangeBaseUrl, "https://exchange.example.com/"); } { const r2 = parseAddExchangeUri( @@ -535,22 +521,15 @@ test("taler add exchange URI (parse)", (t) => { t.fail(); return; } - t.deepEqual( - r2.exchangeBaseUrl, - "https://exchanges.example.com/api/", - ); + t.deepEqual(r2.exchangeBaseUrl, "https://exchanges.example.com/api/"); } - }); test("taler add exchange URI (stringify)", (t) => { const url = stringifyAddExchange({ exchangeBaseUrl: "https://exchange.demo.taler.net", }); - t.deepEqual( - url, - "taler://add-exchange/exchange.demo.taler.net/", - ); + t.deepEqual(url, "taler://add-exchange/exchange.demo.taler.net/"); }); /** diff --git a/packages/taler-util/src/taleruri.ts b/packages/taler-util/src/taleruri.ts @@ -83,7 +83,6 @@ export interface PayTemplateUriResult { type: TalerUriAction.PayTemplate; merchantBaseUrl: string; templateId: string; - templateParams: TemplateParams; } export interface WithdrawUriResult { @@ -124,7 +123,6 @@ export interface BackupRestoreUri { export interface WithdrawExchangeUri { type: TalerUriAction.WithdrawExchange; exchangeBaseUrl: string; - exchangePub?: string; amount?: AmountString; } @@ -212,9 +210,7 @@ export function parseAddExchangeUriWithError(s: string) { const result: AddExchangeUri = { type: TalerUriAction.AddExchange, - exchangeBaseUrl: canonicalizeBaseUrl( - `${pi.body.innerProto}://${p}/`, - ), + exchangeBaseUrl: canonicalizeBaseUrl(`${pi.body.innerProto}://${p}/`), }; return opFixedSuccess(result); } @@ -440,7 +436,6 @@ export function parsePayTemplateUri( type: TalerUriAction.PayTemplate, merchantBaseUrl, templateId, - templateParams: params, }; } @@ -507,7 +502,14 @@ export function parseWithdrawExchangeUri( return undefined; } const host = parts[0].toLowerCase(); - const exchangePub = parts.length > 1 ? parts[parts.length - 1] : undefined; + // Used to be the reserve public key, now it's empty! + const lastPathComponent = + parts.length > 1 ? parts[parts.length - 1] : undefined; + + if (lastPathComponent) { + // invalid taler://withdraw-exchange URI, must end with a slash + return undefined; + } const pathSegments = parts.slice(1, parts.length - 1); const hostAndSegments = [host, ...pathSegments].join("/"); const exchangeBaseUrl = canonicalizeBaseUrl( @@ -519,7 +521,6 @@ export function parseWithdrawExchangeUri( return { type: TalerUriAction.WithdrawExchange, exchangeBaseUrl, - exchangePub: exchangePub != "" ? exchangePub : undefined, amount, }; } @@ -641,13 +642,12 @@ export function stringifyRestoreUri({ export function stringifyWithdrawExchange({ exchangeBaseUrl, - exchangePub, amount, }: Omit<WithdrawExchangeUri, "type">): string { const { proto, path, query } = getUrlInfo(exchangeBaseUrl, { a: amount, }); - return `${proto}://withdraw-exchange/${path}${exchangePub ?? ""}${query}`; + return `${proto}://withdraw-exchange/${path}${query}`; } export function stringifyAddExchange({ @@ -666,9 +666,8 @@ export function stringifyDevExperimentUri({ export function stringifyPayTemplateUri({ merchantBaseUrl, templateId, - templateParams, }: Omit<PayTemplateUriResult, "type">): string { - const { proto, path, query } = getUrlInfo(merchantBaseUrl, templateParams); + const { proto, path, query } = getUrlInfo(merchantBaseUrl); return `${proto}://pay-template/${path}${templateId}${query}`; } diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts @@ -48,6 +48,7 @@ import { } from "./codec.js"; import { CurrencySpecification, + TalerMerchantApi, TemplateParams, WithdrawalOperationStatus, canonicalizeBaseUrl, @@ -487,6 +488,7 @@ export interface PartialWalletRunConfig { builtin?: Partial<WalletRunConfig["builtin"]>; testing?: Partial<WalletRunConfig["testing"]>; features?: Partial<WalletRunConfig["features"]>; + lazyTaskLoop?: Partial<WalletRunConfig["lazyTaskLoop"]>; } export interface WalletRunConfig { @@ -521,6 +523,16 @@ export interface WalletRunConfig { features: { allowHttp: boolean; }; + + /** + * Start processing tasks only when explicitly required, even after + * init has been called. + * + * Useful when the wallet is started to make single read-only request, + * as otherwise wallet-core starts making network request and process + * unrelated pending tasks. + */ + lazyTaskLoop: boolean; } export interface InitRequest { @@ -651,11 +663,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 */ @@ -789,7 +801,7 @@ export const codecForPreparePayResultPaymentPossible = ) .build("PreparePayResultPaymentPossible"); -export interface BalanceDetails {} +export interface BalanceDetails { } /** * Detailed reason for why the wallet's balance is insufficient. @@ -1721,15 +1733,12 @@ export interface AddExchangeRequest { * @deprecated use a separate API call to start a forced exchange update instead */ forceUpdate?: boolean; - - masterPub?: string; } export const codecForAddExchangeRequest = (): Codec<AddExchangeRequest> => buildCodecForObject<AddExchangeRequest>() .property("exchangeBaseUrl", codecForCanonBaseUrl()) .property("forceUpdate", codecOptional(codecForBoolean())) - .property("masterPub", codecOptional(codecForString())) .build("AddExchangeRequest"); export interface UpdateExchangeEntryRequest { @@ -1837,32 +1846,37 @@ export interface GetWithdrawalDetailsForAmountRequest { export interface PrepareBankIntegratedWithdrawalRequest { talerWithdrawUri: string; - exchangeBaseUrl: string; - forcedDenomSel?: ForcedDenomSel; - restrictAge?: number; + selectedExchange?: string; } export const codecForPrepareBankIntegratedWithdrawalRequest = (): Codec<PrepareBankIntegratedWithdrawalRequest> => buildCodecForObject<PrepareBankIntegratedWithdrawalRequest>() - .property("exchangeBaseUrl", codecForCanonBaseUrl()) .property("talerWithdrawUri", codecForString()) - .property("forcedDenomSel", codecForAny()) - .property("restrictAge", codecOptional(codecForNumber())) + .property("selectedExchange", codecOptional(codecForString())) .build("PrepareBankIntegratedWithdrawalRequest"); export interface PrepareBankIntegratedWithdrawalResponse { - transactionId: string; + transactionId?: string; + info: WithdrawUriInfoResponse; } export interface ConfirmWithdrawalRequest { transactionId: string; + exchangeBaseUrl: string; + amount: AmountString; + forcedDenomSel?: ForcedDenomSel; + restrictAge?: number; } export const codecForConfirmWithdrawalRequestRequest = (): Codec<ConfirmWithdrawalRequest> => buildCodecForObject<ConfirmWithdrawalRequest>() .property("transactionId", codecForString()) + .property("amount", codecForAmountString()) + .property("exchangeBaseUrl", codecForCanonBaseUrl()) + .property("forcedDenomSel", codecForAny()) + .property("restrictAge", codecOptional(codecForNumber())) .build("ConfirmWithdrawalRequest"); export interface AcceptBankIntegratedWithdrawalRequest { @@ -1931,6 +1945,9 @@ export const codecForApplyRefundFromPurchaseIdRequest = export interface GetWithdrawalDetailsForUriRequest { talerWithdrawUri: string; + /** + * @deprecated not used + */ restrictAge?: number; } @@ -2023,6 +2040,21 @@ export const codecForSharePaymentResult = (): Codec<SharePaymentResult> => .property("privatePayUri", codecForString()) .build("SharePaymentResult"); +export interface CheckPayTemplateRequest { + talerPayTemplateUri: string; +} + +export type CheckPayTemplateReponse = { + templateDetails: TalerMerchantApi.WalletTemplateDetails; + supportedCurrencies: string[]; +} + +export const codecForCheckPayTemplateRequest = + (): Codec<CheckPayTemplateRequest> => + buildCodecForObject<CheckPayTemplateRequest>() + .property("talerPayTemplateUri", codecForString()) + .build("CheckPayTemplateRequest"); + export interface PreparePayTemplateRequest { talerPayTemplateUri: string; templateParams?: TemplateParams; @@ -3010,6 +3042,18 @@ export interface TestingWaitTransactionRequest { txState: TransactionState; } +export interface TestingGetReserveHistoryRequest { + reservePub: string; + exchangeBaseUrl: string; +} + +export const codecForTestingGetReserveHistoryRequest = + (): Codec<TestingGetReserveHistoryRequest> => + buildCodecForObject<TestingGetReserveHistoryRequest>() + .property("reservePub", codecForString()) + .property("exchangeBaseUrl", codecForString()) + .build("TestingGetReserveHistoryRequest"); + export interface TestingGetDenomStatsRequest { exchangeBaseUrl: string; } @@ -3294,3 +3338,13 @@ export const codecForSyncTermsOfServiceResponse = .property("annual_fee", codecForAmountString()) .property("version", codecForString()) .build("SyncTermsOfServiceResponse"); + +export interface HintNetworkAvailabilityRequest { + isNetworkAvailable: boolean; +} + +export const codecForHintNetworkAvailabilityRequest = + (): Codec<HintNetworkAvailabilityRequest> => + buildCodecForObject<HintNetworkAvailabilityRequest>() + .property("isNetworkAvailable", codecForBoolean()) + .build("HintNetworkAvailabilityRequest"); diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts @@ -29,6 +29,7 @@ import { encodeCrock, getErrorDetailFromException, getRandomBytes, + InitRequest, j2s, Logger, NotificationType, @@ -252,8 +253,8 @@ interface CreateWalletResult { async function createLocalWallet( walletCliArgs: WalletCliArgsType, + args: WalletRunArgs, notificationHandler?: (n: WalletNotification) => void, - noInit?: boolean, ): Promise<CreateWalletResult> { const dbPath = walletCliArgs.wallet.walletDbFile ?? defaultWalletDbPath; const myHttpLib = createPlatformHttpLib({ @@ -275,23 +276,27 @@ async function createLocalWallet( applyVerbose(walletCliArgs.wallet.verbose); const res = { wallet: wh.wallet, getStats: wh.getDbStats }; - if (noInit) { + if (args.noInit) { return res; } try { - await wh.wallet.handleCoreApiRequest("initWallet", "native-init", { - config: { - features: {}, - testing: { - devModeActive: checkEnvFlag("TALER_WALLET_DEV_MODE"), - denomselAllowLate: checkEnvFlag( - "TALER_WALLET_DEBUG_DENOMSEL_ALLOW_LATE", - ), - emitObservabilityEvents: observabilityEventFile != null, - skipDefaults: walletCliArgs.wallet.skipDefaults, + await wh.wallet.handleCoreApiRequest( + WalletApiOperation.InitWallet, + "native-init", + { + config: { + lazyTaskLoop: args.lazyTaskLoop, + testing: { + devModeActive: checkEnvFlag("TALER_WALLET_DEV_MODE"), + denomselAllowLate: checkEnvFlag( + "TALER_WALLET_DEBUG_DENOMSEL_ALLOW_LATE", + ), + emitObservabilityEvents: observabilityEventFile != null, + skipDefaults: walletCliArgs.wallet.skipDefaults, + }, }, - }, - }); + } satisfies InitRequest, + ); return res; } catch (e) { const ed = getErrorDetailFromException(e); @@ -312,8 +317,14 @@ function writeObservabilityLog(notif: WalletNotification): void { } } +export interface WalletRunArgs { + lazyTaskLoop?: boolean; + noInit?: boolean; +} + async function withWallet<T>( walletCliArgs: WalletCliArgsType, + args: WalletRunArgs = {}, f: (ctx: WalletContext) => Promise<T>, ): Promise<T> { const waiter = makeNotificationWaiter(); @@ -341,7 +352,7 @@ async function withWallet<T>( w.close(); return res; } else { - const wh = await createLocalWallet(walletCliArgs, onNotif); + const wh = await createLocalWallet(walletCliArgs, args, onNotif); const ctx: WalletContext = { client: wh.wallet.client, waitForNotificationCond: waiter.waitForNotificationCond, @@ -365,13 +376,19 @@ walletCli help: "Show raw JSON.", }) .action(async (args) => { - await withWallet(args, async (wallet) => { - const balance = await wallet.client.call( - WalletApiOperation.GetBalances, - {}, - ); - console.log(JSON.stringify(balance, undefined, 2)); - }); + await withWallet( + args, + { + lazyTaskLoop: true, + }, + async (wallet) => { + const balance = await wallet.client.call( + WalletApiOperation.GetBalances, + {}, + ); + console.log(JSON.stringify(balance, undefined, 2)); + }, + ); }); walletCli @@ -382,7 +399,7 @@ walletCli help: "Exit with non-zero status code when request fails instead of returning error JSON.", }) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, {}, async (wallet) => { let requestJson; logger.info(`handling 'api' request (${args.api.operation})`); const jsonContent = args.api.request.startsWith("@") @@ -425,18 +442,24 @@ const transactionsCli = walletCli // Default action transactionsCli.action(async (args) => { - await withWallet(args, async (wallet) => { - const pending = await wallet.client.call( - WalletApiOperation.GetTransactions, - { - currency: args.transactions.currency, - search: args.transactions.search, - includeRefreshes: args.transactions.includeRefreshes, - sort: "stable-ascending", - }, - ); - console.log(JSON.stringify(pending, undefined, 2)); - }); + await withWallet( + args, + { + lazyTaskLoop: true, + }, + async (wallet) => { + const pending = await wallet.client.call( + WalletApiOperation.GetTransactions, + { + currency: args.transactions.currency, + search: args.transactions.search, + includeRefreshes: args.transactions.includeRefreshes, + sort: "stable-ascending", + }, + ); + console.log(JSON.stringify(pending, undefined, 2)); + }, + ); }); transactionsCli @@ -447,11 +470,18 @@ transactionsCli help: "Identifier of the transaction to delete", }) .action(async (args) => { - await withWallet(args, async (wallet) => { - await wallet.client.call(WalletApiOperation.DeleteTransaction, { - transactionId: args.deleteTransaction.transactionId as TransactionIdStr, - }); - }); + await withWallet( + args, + { + lazyTaskLoop: true, + }, + async (wallet) => { + await wallet.client.call(WalletApiOperation.DeleteTransaction, { + transactionId: args.deleteTransaction + .transactionId as TransactionIdStr, + }); + }, + ); }); transactionsCli @@ -462,12 +492,18 @@ transactionsCli help: "Identifier of the transaction to suspend.", }) .action(async (args) => { - await withWallet(args, async (wallet) => { - await wallet.client.call(WalletApiOperation.SuspendTransaction, { - transactionId: args.suspendTransaction - .transactionId as TransactionIdStr, - }); - }); + await withWallet( + args, + { + lazyTaskLoop: true, + }, + async (wallet) => { + await wallet.client.call(WalletApiOperation.SuspendTransaction, { + transactionId: args.suspendTransaction + .transactionId as TransactionIdStr, + }); + }, + ); }); transactionsCli @@ -478,11 +514,17 @@ transactionsCli help: "Identifier of the transaction to fail.", }) .action(async (args) => { - await withWallet(args, async (wallet) => { - await wallet.client.call(WalletApiOperation.FailTransaction, { - transactionId: args.fail.transactionId as TransactionIdStr, - }); - }); + await withWallet( + args, + { + lazyTaskLoop: true, + }, + async (wallet) => { + await wallet.client.call(WalletApiOperation.FailTransaction, { + transactionId: args.fail.transactionId as TransactionIdStr, + }); + }, + ); }); transactionsCli @@ -493,7 +535,7 @@ transactionsCli help: "Identifier of the transaction to suspend.", }) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { await wallet.client.call(WalletApiOperation.ResumeTransaction, { transactionId: args.resumeTransaction.transactionId as TransactionIdStr, }); @@ -508,7 +550,7 @@ transactionsCli help: "Identifier of the transaction to delete", }) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const tx = await wallet.client.call( WalletApiOperation.GetTransactionById, { @@ -527,7 +569,7 @@ transactionsCli help: "Identifier of the transaction to delete", }) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { await wallet.client.call(WalletApiOperation.AbortTransaction, { transactionId: args.abortTransaction.transactionId as TransactionIdStr, }); @@ -539,7 +581,7 @@ walletCli help: "Show version details.", }) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const versionInfo = await wallet.client.call( WalletApiOperation.GetVersion, {}, @@ -554,7 +596,7 @@ transactionsCli }) .requiredArgument("transactionId", clk.STRING) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { await wallet.client.call(WalletApiOperation.RetryTransaction, { transactionId: args.retryTransaction.transactionId as TransactionIdStr, }); @@ -566,7 +608,7 @@ walletCli help: "Run until no more work is left.", }) .action(async (args) => { - await withWallet(args, async (ctx) => { + await withWallet(args, { lazyTaskLoop: false }, async (ctx) => { await ctx.client.call(WalletApiOperation.TestingWaitTasksDone, {}); }); }); @@ -583,7 +625,7 @@ withdrawCli const uri = args.withdrawCheckUri.uri; const restrictAge = args.withdrawCheckUri.restrictAge; console.log(`age restriction requested (${restrictAge})`); - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const withdrawInfo = await wallet.client.call( WalletApiOperation.GetWithdrawalDetailsForUri, { @@ -603,7 +645,7 @@ withdrawCli .action(async (args) => { const restrictAge = args.withdrawCheckAmount.restrictAge; console.log(`age restriction requested (${restrictAge})`); - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const withdrawInfo = await wallet.client.call( WalletApiOperation.GetWithdrawalDetailsForAmount, { @@ -625,7 +667,7 @@ withdrawCli const uri = args.withdrawAcceptUri.uri; const restrictAge = args.withdrawAcceptUri.restrictAge; console.log(`age restriction requested (${restrictAge})`); - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const res = await wallet.client.call( WalletApiOperation.AcceptBankIntegratedWithdrawal, { @@ -649,7 +691,7 @@ walletCli .maybeOption("restrictAge", ["--restrict-age"], clk.INT) .flag("autoYes", ["-y", "--yes"]) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { let uri; if (args.handleUri.uri) { uri = args.handleUri.uri; @@ -727,7 +769,7 @@ withdrawCli .maybeOption("forcedReservePriv", ["--forced-reserve-priv"], clk.STRING, {}) .maybeOption("restrictAge", ["--restrict-age"], clk.INT) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const exchangeBaseUrl = args.withdrawManually.exchange; const amount = args.withdrawManually.amount; const d = await wallet.client.call( @@ -771,7 +813,7 @@ exchangesCli }) .action(async (args) => { console.log("Listing exchanges ..."); - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const exchanges = await wallet.client.call( WalletApiOperation.ListExchanges, {}, @@ -789,7 +831,7 @@ exchangesCli }) .flag("force", ["-f", "--force"]) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { await wallet.client.call(WalletApiOperation.UpdateExchangeEntry, { exchangeBaseUrl: args.exchangesUpdateCmd.url, force: args.exchangesUpdateCmd.force, @@ -805,7 +847,7 @@ exchangesCli help: "Base URL of the exchange.", }) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const resp = await wallet.client.call( WalletApiOperation.GetExchangeDetailedInfo, { @@ -824,7 +866,7 @@ exchangesCli help: "Base URL of the exchange.", }) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { await wallet.client.call(WalletApiOperation.AddExchange, { exchangeBaseUrl: args.exchangesAddCmd.url, }); @@ -840,7 +882,7 @@ exchangesCli }) .flag("purge", ["--purge"]) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { await wallet.client.call(WalletApiOperation.DeleteExchange, { exchangeBaseUrl: args.exchangesAddCmd.url, purge: args.exchangesAddCmd.purge, @@ -856,7 +898,7 @@ exchangesCli help: "Base URL of the exchange.", }) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { await wallet.client.call(WalletApiOperation.SetExchangeTosAccepted, { exchangeBaseUrl: args.exchangesAcceptTosCmd.url, }); @@ -879,7 +921,7 @@ exchangesCli .map((x) => x.trim()); acceptedFormat = split; } - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const tosResult = await wallet.client.call( WalletApiOperation.GetExchangeTos, { @@ -896,14 +938,14 @@ const backupCli = walletCli.subcommand("backupArgs", "backup", { }); backupCli.subcommand("exportDb", "export-db").action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const backup = await wallet.client.call(WalletApiOperation.ExportDb, {}); console.log(JSON.stringify(backup, undefined, 2)); }); }); backupCli.subcommand("storeBackup", "store").action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const resp = await wallet.client.call( WalletApiOperation.CreateStoredBackup, {}, @@ -913,7 +955,7 @@ backupCli.subcommand("storeBackup", "store").action(async (args) => { }); backupCli.subcommand("storeBackup", "list-stored").action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const resp = await wallet.client.call( WalletApiOperation.ListStoredBackups, {}, @@ -926,7 +968,7 @@ backupCli .subcommand("storeBackup", "delete-stored") .requiredArgument("name", clk.STRING) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const resp = await wallet.client.call( WalletApiOperation.DeleteStoredBackup, { @@ -941,7 +983,7 @@ backupCli .subcommand("recoverBackup", "recover-stored") .requiredArgument("name", clk.STRING) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const resp = await wallet.client.call( WalletApiOperation.RecoverStoredBackup, { @@ -953,7 +995,7 @@ backupCli }); backupCli.subcommand("importDb", "import-db").action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const dumpRaw = await read(process.stdin); const dump = JSON.parse(dumpRaw); await wallet.client.call(WalletApiOperation.ImportDb, { @@ -971,7 +1013,7 @@ depositCli .requiredArgument("amount", clk.AMOUNT) .requiredArgument("targetPayto", clk.STRING) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const resp = await wallet.client.call( WalletApiOperation.CreateDepositGroup, { @@ -995,7 +1037,7 @@ peerCli help: "Amount to pay", }) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const resp = await wallet.client.call( WalletApiOperation.CheckPeerPushDebit, { @@ -1014,7 +1056,7 @@ peerCli help: "Amount to request", }) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const resp = await wallet.client.call( WalletApiOperation.CheckPeerPullCredit, { @@ -1029,7 +1071,7 @@ peerCli .subcommand("prepareIncomingPayPull", "prepare-pull-debit") .requiredArgument("talerUri", clk.STRING) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const resp = await wallet.client.call( WalletApiOperation.PreparePeerPullDebit, { @@ -1044,7 +1086,7 @@ peerCli .subcommand("confirmIncomingPayPull", "confirm-pull-debit") .requiredArgument("transactionId", clk.STRING) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const resp = await wallet.client.call( WalletApiOperation.ConfirmPeerPullDebit, { @@ -1060,7 +1102,7 @@ peerCli .subcommand("confirmIncomingPayPush", "confirm-push-credit") .requiredArgument("transactionId", clk.STRING) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const resp = await wallet.client.call( WalletApiOperation.ConfirmPeerPushCredit, { @@ -1098,7 +1140,7 @@ peerCli ); } - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const resp = await wallet.client.call( WalletApiOperation.InitiatePeerPullCredit, { @@ -1118,7 +1160,7 @@ peerCli .subcommand("preparePushCredit", "prepare-push-credit") .requiredArgument("talerUri", clk.STRING) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const resp = await wallet.client.call( WalletApiOperation.PreparePeerPushCredit, { @@ -1155,7 +1197,7 @@ peerCli ); } - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const resp = await wallet.client.call( WalletApiOperation.InitiatePeerPushDebit, { @@ -1193,7 +1235,7 @@ advancedCli help: "Show active wallet-core tasks.", }) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const tasks = await wallet.client.call( WalletApiOperation.GetActiveTasks, {}, @@ -1244,7 +1286,11 @@ advancedCli const onNotif = (notif: WalletNotification) => { writeObservabilityLog(notif); }; - const wh = await createLocalWallet(args, onNotif, args.serve.noInit); + const wh = await createLocalWallet( + args, + { lazyTaskLoop: false, noInit: args.serve.noInit }, + onNotif, + ); const w = wh.wallet; let nextClientId = 1; const notifyHandlers = new Map<number, (n: WalletNotification) => void>(); @@ -1293,7 +1339,7 @@ advancedCli help: "Initialize the wallet (with DB) and exit.", }) .action(async (args) => { - await withWallet(args, async () => {}); + await withWallet(args, { lazyTaskLoop: true }, async () => {}); }); advancedCli @@ -1309,7 +1355,7 @@ advancedCli advancedCli .subcommand("pending", "pending", { help: "Show pending operations." }) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const pending = await wallet.client.call( WalletApiOperation.GetPendingOperations, {}, @@ -1360,7 +1406,7 @@ currenciesCli help: "List global-currency auditors.", }) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const currencies = await wallet.client.call( WalletApiOperation.ListGlobalCurrencyAuditors, {}, @@ -1374,7 +1420,7 @@ currenciesCli help: "List global-currency exchanges.", }) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const currencies = await wallet.client.call( WalletApiOperation.ListGlobalCurrencyExchanges, {}, @@ -1391,7 +1437,7 @@ currenciesCli .requiredOption("exchangeBaseUrl", ["--url"], clk.STRING) .requiredOption("exchangePub", ["--pub"], clk.STRING) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const currencies = await wallet.client.call( WalletApiOperation.AddGlobalCurrencyExchange, { @@ -1412,7 +1458,7 @@ currenciesCli .requiredOption("exchangeBaseUrl", ["--url"], clk.STRING) .requiredOption("exchangePub", ["--pub"], clk.STRING) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const currencies = await wallet.client.call( WalletApiOperation.RemoveGlobalCurrencyExchange, { @@ -1433,7 +1479,7 @@ currenciesCli .requiredOption("auditorBaseUrl", ["--url"], clk.STRING) .requiredOption("auditorPub", ["--pub"], clk.STRING) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const currencies = await wallet.client.call( WalletApiOperation.AddGlobalCurrencyAuditor, { @@ -1454,7 +1500,7 @@ currenciesCli .requiredOption("auditorBaseUrl", ["--url"], clk.STRING) .requiredOption("auditorPub", ["--pub"], clk.STRING) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const currencies = await wallet.client.call( WalletApiOperation.RemoveGlobalCurrencyAuditor, { @@ -1472,7 +1518,7 @@ advancedCli help: "Clear the database, irrevocable deleting all data in the wallet.", }) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { await wallet.client.call(WalletApiOperation.ClearDb, {}); }); }); @@ -1482,7 +1528,7 @@ advancedCli help: "Export, clear and re-import the database via the backup mechanism.", }) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { await wallet.client.call(WalletApiOperation.Recycle, {}); }); }); @@ -1493,7 +1539,7 @@ advancedCli }) .requiredArgument("url", clk.STRING) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const res = await wallet.client.call( WalletApiOperation.PreparePayForUri, { @@ -1526,7 +1572,7 @@ advancedCli }) .requiredArgument("transactionId", clk.STRING) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { await wallet.client.call(WalletApiOperation.StartRefundQuery, { transactionId: args.queryRefund.transactionId as TransactionIdStr, }); @@ -1540,7 +1586,7 @@ advancedCli .requiredArgument("proposalId", clk.STRING) .maybeOption("sessionIdOverride", ["--session-id"], clk.STRING) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { await wallet.client.call(WalletApiOperation.ConfirmPay, { proposalId: args.payConfirm.proposalId, sessionId: args.payConfirm.sessionIdOverride, @@ -1554,7 +1600,7 @@ advancedCli }) .requiredArgument("coinPub", clk.STRING) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { await wallet.client.call(WalletApiOperation.ForceRefresh, { refreshCoinSpecs: [ { @@ -1570,7 +1616,7 @@ advancedCli help: "Dump coins in an easy-to-process format.", }) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const coinDump = await wallet.client.call( WalletApiOperation.DumpCoins, {}, @@ -1587,7 +1633,7 @@ advancedCli }) .requiredArgument("coinPubSpec", clk.STRING) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { let coinPubList: string[]; try { coinPubList = coinPubListCodec.decode( @@ -1612,7 +1658,7 @@ advancedCli }) .requiredArgument("coinPubSpec", clk.STRING) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { let coinPubList: string[]; try { coinPubList = coinPubListCodec.decode( @@ -1636,7 +1682,7 @@ advancedCli help: "List coins.", }) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const coins = await wallet.client.call(WalletApiOperation.DumpCoins, {}); for (const coin of coins.coins) { console.log(`coin ${coin.coin_pub}`); @@ -1654,13 +1700,13 @@ const testCli = walletCli.subcommand("testingArgs", "testing", { testCli .subcommand("withdrawTestkudos", "withdraw-testkudos") .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { await wallet.client.call(WalletApiOperation.WithdrawTestkudos, {}); }); }); testCli.subcommand("withdrawKudos", "withdraw-kudos").action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { await wallet.client.call(WalletApiOperation.WithdrawTestBalance, { amount: "KUDOS:50" as AmountString, corebankApiBaseUrl: "https://bank.demo.taler.net/", diff --git a/packages/taler-wallet-core/src/balance.ts b/packages/taler-wallet-core/src/balance.ts @@ -57,6 +57,7 @@ import { assertUnreachable, BalanceFlag, BalancesResponse, + checkDbInvariant, GetBalanceDetailRequest, j2s, Logger, @@ -350,9 +351,8 @@ export async function getBalancesInsideTransaction( await tx.withdrawalGroups.indexes.byStatus .iter(keyRangeActive) - .forEachAsync(async (wgRecord) => { - const currency = Amounts.currencyOf(wgRecord.denomsSel.totalCoinValue); - switch (wgRecord.status) { + .forEachAsync(async (wg) => { + switch (wg.status) { case WithdrawalGroupStatus.AbortedBank: case WithdrawalGroupStatus.AbortedExchange: case WithdrawalGroupStatus.FailedAbortingBank: @@ -374,34 +374,59 @@ export async function getBalancesInsideTransaction( // Pending, but no special flag. break; case WithdrawalGroupStatus.SuspendedKyc: - case WithdrawalGroupStatus.PendingKyc: - await balanceStore.setFlagIncomingKyc( - currency, - wgRecord.exchangeBaseUrl, + case WithdrawalGroupStatus.PendingKyc: { + checkDbInvariant( + wg.denomsSel !== undefined, + "wg in kyc state should have been initialized", ); + const currency = Amounts.currencyOf(wg.denomsSel.totalCoinValue); + await balanceStore.setFlagIncomingKyc(currency, wg.exchangeBaseUrl); break; + } case WithdrawalGroupStatus.PendingAml: - case WithdrawalGroupStatus.SuspendedAml: - await balanceStore.setFlagIncomingAml( - currency, - wgRecord.exchangeBaseUrl, + case WithdrawalGroupStatus.SuspendedAml: { + checkDbInvariant( + wg.denomsSel !== undefined, + "wg in aml state should have been initialized", ); + const currency = Amounts.currencyOf(wg.denomsSel.totalCoinValue); + await balanceStore.setFlagIncomingAml(currency, wg.exchangeBaseUrl); + break; + } + case WithdrawalGroupStatus.PendingRegisteringBank: { + if (wg.denomsSel && wg.exchangeBaseUrl) { + const currency = Amounts.currencyOf(wg.denomsSel.totalCoinValue); + await balanceStore.setFlagIncomingConfirmation( + currency, + wg.exchangeBaseUrl, + ); + } break; - case WithdrawalGroupStatus.PendingRegisteringBank: - case WithdrawalGroupStatus.PendingWaitConfirmBank: + } + case WithdrawalGroupStatus.PendingWaitConfirmBank: { + checkDbInvariant( + wg.denomsSel !== undefined, + "wg in confirmed state should have been initialized", + ); + const currency = Amounts.currencyOf(wg.denomsSel.totalCoinValue); await balanceStore.setFlagIncomingConfirmation( currency, - wgRecord.exchangeBaseUrl, + wg.exchangeBaseUrl, ); break; + } default: - assertUnreachable(wgRecord.status); + assertUnreachable(wg.status); + } + if (wg.denomsSel && wg.exchangeBaseUrl) { + // only inform pending incoming if amount and exchange has been selected + const currency = Amounts.currencyOf(wg.denomsSel.totalCoinValue); + await balanceStore.addPendingIncoming( + currency, + wg.exchangeBaseUrl, + wg.denomsSel.totalCoinValue, + ); } - await balanceStore.addPendingIncoming( - currency, - wgRecord.exchangeBaseUrl, - wgRecord.denomsSel.totalCoinValue, - ); }); await tx.peerPushDebit.indexes.byStatus diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts @@ -214,6 +214,10 @@ export interface TalerCryptoInterface { signPurseCreation(req: SignPurseCreationRequest): Promise<EddsaSigningResult>; + signReserveHistoryReq( + req: SignReserveHistoryReqRequest, + ): Promise<SignReserveHistoryReqResponse>; + signPurseDeposits( req: SignPurseDepositsRequest, ): Promise<SignPurseDepositsResponse>; @@ -438,6 +442,11 @@ export const nullCrypto: TalerCryptoInterface = { ): Promise<SignCoinHistoryResponse> { throw new Error("Function not implemented."); }, + signReserveHistoryReq: function ( + req: SignReserveHistoryReqRequest, + ): Promise<SignReserveHistoryReqResponse> { + throw new Error("Function not implemented."); + }, }; export type WithArg<X> = X extends (req: infer T) => infer R @@ -475,6 +484,15 @@ export interface SignPurseCreationRequest { minAge: number; } +export interface SignReserveHistoryReqRequest { + reservePriv: string; + startOffset: number; +} + +export interface SignReserveHistoryReqResponse { + sig: string; +} + export interface SpendCoinDetails { coinPub: string; coinPriv: string; @@ -1730,6 +1748,23 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { sig: sigResp.sig, }; }, + async signReserveHistoryReq( + tci: TalerCryptoInterfaceR, + req: SignReserveHistoryReqRequest, + ): Promise<SignReserveHistoryReqResponse> { + const reserveHistoryBlob = buildSigPS( + TalerSignaturePurpose.WALLET_RESERVE_HISTORY, + ) + .put(bufferForUint64(req.startOffset)) + .build(); + const sigResp = await tci.eddsaSign(tci, { + msg: encodeCrock(reserveHistoryBlob), + priv: req.reservePriv, + }); + return { + sig: sigResp.sig, + }; + }, }; export interface EddsaSignRequest { diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts @@ -353,7 +353,7 @@ export enum WithdrawalGroupStatus { * Another wallet confirmed the withdrawal * (by POSTing the reserve pub to the bank) * before we had the chance. - * + * * In this situation, we'll let the other wallet continue * and give up ourselves. */ @@ -376,7 +376,7 @@ export interface ReserveBankInfo { /** * Exchange payto URI that the bank will use to fund the reserve. */ - exchangePaytoUri: string; + exchangePaytoUri?: string; /** * Time when the information about this reserve was posted to the bank. @@ -393,6 +393,8 @@ export interface ReserveBankInfo { * Set to undefined if not confirmed yet. */ timestampBankConfirmed: DbPreciseTimestamp | undefined; + + wireTypes: string[] | undefined; } /** @@ -1562,7 +1564,7 @@ export interface WithdrawalGroupRecord { /** * Amount that was sent by the user to fund the reserve. */ - instructedAmount: AmountString; + instructedAmount?: AmountString; /** * Amount that was observed when querying the reserve that @@ -1579,7 +1581,7 @@ export interface WithdrawalGroupRecord { * (Initial amount confirmed by the user, might differ with denomSel * on reselection.) */ - rawWithdrawalAmount: AmountString; + rawWithdrawalAmount?: AmountString; /** * Amount that will be added to the balance when the withdrawal succeeds. @@ -1587,12 +1589,12 @@ export interface WithdrawalGroupRecord { * (Initial amount confirmed by the user, might differ with denomSel * on reselection.) */ - effectiveWithdrawalAmount: AmountString; + effectiveWithdrawalAmount?: AmountString; /** * Denominations selected for withdrawal. */ - denomsSel: DenomSelectionState; + denomsSel?: DenomSelectionState; /** * UID of the denomination selection. @@ -2677,7 +2679,9 @@ export const WalletStoresV1 = { describeContents<BankWithdrawUriRecord>({ keyPath: "talerWithdrawUri", }), - {}, + { + byGroup: describeIndex("byGroup", "withdrawalGroupId"), + }, ), backupProviders: describeStore( "backupProviders", diff --git a/packages/taler-wallet-core/src/dbless.ts b/packages/taler-wallet-core/src/dbless.ts @@ -113,9 +113,7 @@ export interface TopupReserveWithBankArgs { amount: AmountString; } -export async function topupReserveWithBank( - args: TopupReserveWithBankArgs, -) { +export async function topupReserveWithBank(args: TopupReserveWithBankArgs) { const { http, corebankApiBaseUrl, amount, exchangeInfo, reservePub } = args; const bankClient = new TalerCorebankApiClient(corebankApiBaseUrl); const bankUser = await bankClient.createRandomBankUser(); diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts @@ -776,7 +776,7 @@ async function processDepositGroupPendingTrack( { 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 @@ -1152,9 +1152,7 @@ export async function fetchFreshExchange( wex: WalletExecutionContext, baseUrl: string, options: { - cancellationToken?: CancellationToken; forceUpdate?: boolean; - expectedMasterPub?: string; } = {}, ): Promise<ReadyExchangeSummary> { if (!options.forceUpdate) { @@ -1549,7 +1547,6 @@ export async function updateExchangeFromUrlHandler( r.updateStatus = ExchangeEntryDbUpdateStatus.Ready; r.cachebreakNextUpdate = false; await tx.exchanges.put(r); - logger.info(`putting new exchange details in DB: ${j2s(newDetails)}`); const drRowId = await tx.exchangeDetails.put(newDetails); checkDbInvariant(typeof drRowId.key === "number"); diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts @@ -34,13 +34,17 @@ import { assertUnreachable, AsyncFlag, checkDbInvariant, + CheckPaymentResponse, + CheckPayTemplateReponse, + CheckPayTemplateRequest, codecForAbortResponse, codecForMerchantContractTerms, codecForMerchantOrderStatusPaid, codecForMerchantPayResponse, - codecForMerchantPostOrderResponse, + codecForPostOrderResponse, codecForProposal, codecForWalletRefundResponse, + codecForWalletTemplateDetails, CoinDepositPermission, CoinRefreshRequest, ConfirmPayResult, @@ -76,6 +80,8 @@ import { TalerError, TalerErrorCode, TalerErrorDetail, + TalerMerchantApi, + TalerMerchantInstanceHttpClient, TalerPreciseTimestamp, TalerProtocolViolationError, TalerUriAction, @@ -1578,39 +1584,92 @@ async function internalWaitProposalDownloaded( } } +async function downloadTemplate( + wex: WalletExecutionContext, + merchantBaseUrl: string, + templateId: string, +): Promise<TalerMerchantApi.WalletTemplateDetails> { + const reqUrl = new URL(`templates/${templateId}`, merchantBaseUrl); + const httpReq = await wex.http.fetch(reqUrl.href, { + method: "GET", + cancellationToken: wex.cancellationToken, + }); + const resp = await readSuccessResponseJsonOrThrow( + httpReq, + codecForWalletTemplateDetails(), + ); + return resp; +} + +export async function checkPayForTemplate( + wex: WalletExecutionContext, + req: CheckPayTemplateRequest, +): Promise<CheckPayTemplateReponse> { + const parsedUri = parsePayTemplateUri(req.talerPayTemplateUri); + if (!parsedUri) { + throw Error("invalid taler-template URI"); + } + const templateDetails = await downloadTemplate( + wex, + parsedUri.merchantBaseUrl, + parsedUri.templateId, + ); + + const merchantApi = new TalerMerchantInstanceHttpClient( + parsedUri.merchantBaseUrl, + wex.http, + ); + + const cfg = await merchantApi.getConfig(); + if (cfg.type === "fail") { + throw TalerError.fromUncheckedDetail(cfg.detail); + } + + return { + templateDetails, + supportedCurrencies: Object.keys(cfg.body.currencies), + }; +} + export async function preparePayForTemplate( wex: WalletExecutionContext, req: PreparePayTemplateRequest, ): Promise<PreparePayResult> { const parsedUri = parsePayTemplateUri(req.talerPayTemplateUri); - const templateDetails: MerchantUsingTemplateDetails = {}; if (!parsedUri) { throw Error("invalid taler-template URI"); } logger.trace(`parsed URI: ${j2s(parsedUri)}`); + const templateDetails: MerchantUsingTemplateDetails = {}; - const amountFromUri = parsedUri.templateParams.amount; - if (amountFromUri != null) { - const templateParamsAmount = req.templateParams?.amount; - if (templateParamsAmount != null) { - templateDetails.amount = templateParamsAmount as AmountString; - } else { - if (Amounts.isCurrency(amountFromUri)) { - throw Error( - "Amount from template URI only has a currency without value. The value must be provided in the templateParams.", - ); - } else { - templateDetails.amount = amountFromUri as AmountString; - } + const templateInfo = await downloadTemplate( + wex, + parsedUri.merchantBaseUrl, + parsedUri.templateId, + ); + + const templateParamsAmount = req.templateParams?.amount as + | AmountString + | undefined; + if (templateParamsAmount === null) { + const amountFromUri = templateInfo.editable_defaults?.amount; + if (amountFromUri != null) { + templateDetails.amount = amountFromUri as AmountString; } + } else { + templateDetails.amount = templateParamsAmount; } - if ( - parsedUri.templateParams.summary !== undefined && - typeof parsedUri.templateParams.summary === "string" - ) { - templateDetails.summary = - req.templateParams?.summary ?? parsedUri.templateParams.summary; + + const templateParamsSummary = req.templateParams?.summary; + if (templateParamsSummary === null) { + const summaryFromUri = templateInfo.editable_defaults?.summary; + if (summaryFromUri != null) { + templateDetails.summary = summaryFromUri; + } + } else { + templateDetails.summary = templateParamsSummary; } + const reqUrl = new URL( `templates/${parsedUri.templateId}`, parsedUri.merchantBaseUrl, @@ -1621,7 +1680,7 @@ export async function preparePayForTemplate( }); const resp = await readSuccessResponseJsonOrThrow( httpReq, - codecForMerchantPostOrderResponse(), + codecForPostOrderResponse(), ); const payUri = stringifyPayUri({ @@ -2875,7 +2934,6 @@ async function processPurchaseAutoRefund( ); requestUrl.searchParams.set("timeout_ms", "10000"); - requestUrl.searchParams.set("await_refund_obtained", "yes"); requestUrl.searchParams.set("refund", Amounts.stringify(totalKnownRefund)); const resp = await wex.http.fetch(requestUrl.href, { diff --git a/packages/taler-wallet-core/src/transactions.ts b/packages/taler-wallet-core/src/transactions.ts @@ -93,6 +93,7 @@ import { computeDenomLossTransactionStatus, DenomLossTransactionContext, ExchangeWireDetails, + fetchFreshExchange, getExchangeWireDetailsInTx, } from "./exchanges.js"; import { @@ -243,20 +244,22 @@ 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"); + if ( withdrawalGroupRecord.wgInfo.withdrawalType === WithdrawalRecordType.BankIntegrated ) { return buildTransactionForBankIntegratedWithdraw( withdrawalGroupRecord, + exchangeDetails, ort, ); } - const exchangeDetails = await getExchangeWireDetailsInTx( - tx, - withdrawalGroupRecord.exchangeBaseUrl, - ); - if (!exchangeDetails) throw Error("not exchange details"); return buildTransactionForManualWithdraw( withdrawalGroupRecord, @@ -589,6 +592,8 @@ function buildTransactionForPeerPullCredit( ); }); const txState = computePeerPullCreditTransactionState(pullCredit); + checkDbInvariant(wsr.instructedAmount !== undefined, "wg uninitialized"); + checkDbInvariant(wsr.denomsSel !== undefined, "wg uninitialized"); return { type: TransactionType.PeerPullCredit, txState, @@ -654,13 +659,15 @@ function buildTransactionForPeerPushCredit( pushInc: PeerPushPaymentIncomingRecord, pushOrt: OperationRetryRecord | undefined, peerContractTerms: PeerContractTerms, - wsr: WithdrawalGroupRecord | undefined, + wg: WithdrawalGroupRecord | undefined, wsrOrt: OperationRetryRecord | undefined, ): Transaction { - if (wsr) { - if (wsr.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPushCredit) { + if (wg) { + if (wg.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPushCredit) { throw Error("invalid withdrawal group type for push payment credit"); } + checkDbInvariant(wg.instructedAmount !== undefined, "wg uninitialized"); + checkDbInvariant(wg.denomsSel !== undefined, "wg uninitialized"); const txState = computePeerPushCreditTransactionState(pushInc); return { @@ -668,15 +675,15 @@ function buildTransactionForPeerPushCredit( txState, txActions: computePeerPushCreditTransactionActions(pushInc), amountEffective: isUnsuccessfulTransaction(txState) - ? Amounts.stringify(Amounts.zeroOfAmount(wsr.instructedAmount)) - : Amounts.stringify(wsr.denomsSel.totalCoinValue), - amountRaw: Amounts.stringify(wsr.instructedAmount), - exchangeBaseUrl: wsr.exchangeBaseUrl, + ? Amounts.stringify(Amounts.zeroOfAmount(wg.instructedAmount)) + : Amounts.stringify(wg.denomsSel.totalCoinValue), + amountRaw: Amounts.stringify(wg.instructedAmount), + exchangeBaseUrl: wg.exchangeBaseUrl, info: { expiration: peerContractTerms.purse_expiration, summary: peerContractTerms.summary, }, - timestamp: timestampPreciseFromDb(wsr.timestampStart), + timestamp: timestampPreciseFromDb(wg.timestampStart), transactionId: constructTransactionIdentifier({ tag: TransactionType.PeerPushCredit, peerPushCreditId: pushInc.peerPushCreditId, @@ -712,37 +719,44 @@ function buildTransactionForPeerPushCredit( } function buildTransactionForBankIntegratedWithdraw( - wgRecord: WithdrawalGroupRecord, + wg: WithdrawalGroupRecord, + exchangeDetails: ExchangeWireDetails, ort?: OperationRetryRecord, ): TransactionWithdrawal { - if (wgRecord.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) + if (wg.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) { throw Error(""); - - const txState = computeWithdrawalTransactionStatus(wgRecord); + } + const txState = computeWithdrawalTransactionStatus(wg); + const zero = Amounts.stringify( + Amounts.zeroOfCurrency(exchangeDetails.currency), + ); return { type: TransactionType.Withdrawal, txState, - txActions: computeWithdrawalTransactionActions(wgRecord), - amountEffective: isUnsuccessfulTransaction(txState) - ? Amounts.stringify(Amounts.zeroOfAmount(wgRecord.instructedAmount)) - : Amounts.stringify(wgRecord.denomsSel.totalCoinValue), - amountRaw: Amounts.stringify(wgRecord.instructedAmount), + txActions: computeWithdrawalTransactionActions(wg), + exchangeBaseUrl: wg.exchangeBaseUrl, + amountEffective: + isUnsuccessfulTransaction(txState) || !wg.denomsSel + ? zero + : Amounts.stringify(wg.denomsSel.totalCoinValue), + amountRaw: !wg.instructedAmount + ? zero + : Amounts.stringify(wg.instructedAmount), withdrawalDetails: { type: WithdrawalType.TalerBankIntegrationApi, - confirmed: wgRecord.wgInfo.bankInfo.timestampBankConfirmed ? true : false, - exchangeCreditAccountDetails: wgRecord.wgInfo.exchangeCreditAccounts, - reservePub: wgRecord.reservePub, - bankConfirmationUrl: wgRecord.wgInfo.bankInfo.confirmUrl, + confirmed: wg.wgInfo.bankInfo.timestampBankConfirmed ? true : false, + exchangeCreditAccountDetails: wg.wgInfo.exchangeCreditAccounts, + reservePub: wg.reservePub, + bankConfirmationUrl: wg.wgInfo.bankInfo.confirmUrl, reserveIsReady: - wgRecord.status === WithdrawalGroupStatus.Done || - wgRecord.status === WithdrawalGroupStatus.PendingReady, + wg.status === WithdrawalGroupStatus.Done || + wg.status === WithdrawalGroupStatus.PendingReady, }, - kycUrl: wgRecord.kycUrl, - exchangeBaseUrl: wgRecord.exchangeBaseUrl, - timestamp: timestampPreciseFromDb(wgRecord.timestampStart), + kycUrl: wg.kycUrl, + timestamp: timestampPreciseFromDb(wg.timestampStart), transactionId: constructTransactionIdentifier({ tag: TransactionType.Withdrawal, - withdrawalGroupId: wgRecord.withdrawalGroupId, + withdrawalGroupId: wg.withdrawalGroupId, }), ...(ort?.lastError ? { error: ort.lastError } : {}), }; @@ -759,50 +773,49 @@ export function isUnsuccessfulTransaction(state: TransactionState): boolean { } function buildTransactionForManualWithdraw( - withdrawalGroup: WithdrawalGroupRecord, + wg: WithdrawalGroupRecord, exchangeDetails: ExchangeWireDetails, ort?: OperationRetryRecord, ): TransactionWithdrawal { - if (withdrawalGroup.wgInfo.withdrawalType !== WithdrawalRecordType.BankManual) + if (wg.wgInfo.withdrawalType !== WithdrawalRecordType.BankManual) throw Error(""); const plainPaytoUris = exchangeDetails.wireInfo?.accounts.map((x) => x.payto_uri) ?? []; + checkDbInvariant(wg.instructedAmount !== undefined, "wg uninitialized"); + checkDbInvariant(wg.denomsSel !== undefined, "wg uninitialized"); const exchangePaytoUris = augmentPaytoUrisForWithdrawal( plainPaytoUris, - withdrawalGroup.reservePub, - withdrawalGroup.instructedAmount, + wg.reservePub, + wg.instructedAmount, ); - const txState = computeWithdrawalTransactionStatus(withdrawalGroup); + const txState = computeWithdrawalTransactionStatus(wg); return { type: TransactionType.Withdrawal, txState, - txActions: computeWithdrawalTransactionActions(withdrawalGroup), + txActions: computeWithdrawalTransactionActions(wg), amountEffective: isUnsuccessfulTransaction(txState) - ? Amounts.stringify( - Amounts.zeroOfAmount(withdrawalGroup.instructedAmount), - ) - : Amounts.stringify(withdrawalGroup.denomsSel.totalCoinValue), - amountRaw: Amounts.stringify(withdrawalGroup.instructedAmount), + ? Amounts.stringify(Amounts.zeroOfAmount(wg.instructedAmount)) + : Amounts.stringify(wg.denomsSel.totalCoinValue), + amountRaw: Amounts.stringify(wg.instructedAmount), withdrawalDetails: { type: WithdrawalType.ManualTransfer, - reservePub: withdrawalGroup.reservePub, + reservePub: wg.reservePub, exchangePaytoUris, - exchangeCreditAccountDetails: - withdrawalGroup.wgInfo.exchangeCreditAccounts, + exchangeCreditAccountDetails: wg.wgInfo.exchangeCreditAccounts, reserveIsReady: - withdrawalGroup.status === WithdrawalGroupStatus.Done || - withdrawalGroup.status === WithdrawalGroupStatus.PendingReady, + wg.status === WithdrawalGroupStatus.Done || + wg.status === WithdrawalGroupStatus.PendingReady, }, - kycUrl: withdrawalGroup.kycUrl, - exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl, - timestamp: timestampPreciseFromDb(withdrawalGroup.timestampStart), + kycUrl: wg.kycUrl, + exchangeBaseUrl: wg.exchangeBaseUrl, + timestamp: timestampPreciseFromDb(wg.timestampStart), transactionId: constructTransactionIdentifier({ tag: TransactionType.Withdrawal, - withdrawalGroupId: withdrawalGroup.withdrawalGroupId, + withdrawalGroupId: wg.withdrawalGroupId, }), ...(ort?.lastError ? { error: ort.lastError } : {}), }; @@ -983,16 +996,22 @@ async function lookupMaybeContractData( return contractData; } -async function buildTransactionForPurchase( +function buildTransactionForPurchase( purchaseRecord: PurchaseRecord, contractData: WalletContractData, refundsInfo: RefundGroupRecord[], ort?: OperationRetryRecord, -): Promise<Transaction> { +): Transaction { const zero = Amounts.zeroOfAmount(contractData.amount); const info: OrderShortInfo = { - merchant: contractData.merchant, + merchant: { + name: contractData.merchant.name, + address: contractData.merchant.address, + email: contractData.merchant.email, + jurisdiction: contractData.merchant.jurisdiction, + website: contractData.merchant.website, + }, orderId: contractData.orderId, summary: contractData.summary, summary_i18n: contractData.summaryI18n, @@ -1075,20 +1094,22 @@ export async function getWithdrawalTransactionByUri( 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"); + if ( withdrawalGroupRecord.wgInfo.withdrawalType === WithdrawalRecordType.BankIntegrated ) { return buildTransactionForBankIntegratedWithdraw( withdrawalGroupRecord, + exchangeDetails, ort, ); } - const exchangeDetails = await getExchangeWireDetailsInTx( - tx, - withdrawalGroupRecord.exchangeBaseUrl, - ); - if (!exchangeDetails) throw Error("not exchange details"); return buildTransactionForManualWithdraw( withdrawalGroupRecord, @@ -1331,6 +1352,13 @@ export async function getTransactions( }); await iterRecordsForWithdrawal(tx, filter, async (wsr) => { + if ( + wsr.rawWithdrawalAmount === undefined || + wsr.exchangeBaseUrl == undefined + ) { + // skip prepared withdrawals which has not been confirmed + return; + } const exchangesInTx = [wsr.exchangeBaseUrl]; if ( shouldSkipCurrency( @@ -1360,11 +1388,26 @@ export async function getTransactions( // FIXME: If this is an orphan withdrawal, still report it as a withdrawal! // FIXME: Still report if requested with verbose option? return; - case WithdrawalRecordType.BankIntegrated: + case WithdrawalRecordType.BankIntegrated: { + const exchangeDetails = await getExchangeWireDetailsInTx( + tx, + wsr.exchangeBaseUrl, + ); + if (!exchangeDetails) { + // FIXME: report somehow + return; + } + transactions.push( - buildTransactionForBankIntegratedWithdraw(wsr, ort), + buildTransactionForBankIntegratedWithdraw( + wsr, + exchangeDetails, + ort, + ), ); return; + } + case WithdrawalRecordType.BankManual: { const exchangeDetails = await getExchangeWireDetailsInTx( tx, @@ -1374,7 +1417,6 @@ export async function getTransactions( // FIXME: report somehow return; } - transactions.push( buildTransactionForManualWithdraw(wsr, exchangeDetails, ort), ); @@ -1475,7 +1517,7 @@ export async function getTransactions( ); transactions.push( - await buildTransactionForPurchase( + buildTransactionForPurchase( purchase, contractData, refunds, @@ -1726,7 +1768,14 @@ export async function retryTransaction( logger.info(`resetting retry timeout for ${transactionId}`); const taskId = maybeTaskFromTransaction(transactionId); if (taskId) { - wex.taskScheduler.resetTaskRetries(taskId); + await wex.taskScheduler.resetTaskRetries(taskId); + } +} + +export async function retryAll(wex: WalletExecutionContext): Promise<void> { + const tasks = wex.taskScheduler.getActiveTasks(); + for (const task of tasks) { + await wex.taskScheduler.resetTaskRetries(task); } } diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -40,6 +40,8 @@ import { BalancesResponse, CanonicalizeBaseUrlRequest, CanonicalizeBaseUrlResponse, + CheckPayTemplateReponse, + CheckPayTemplateRequest, CheckPeerPullCreditRequest, CheckPeerPullCreditResponse, CheckPeerPushDebitRequest, @@ -79,6 +81,7 @@ import { GetPlanForOperationResponse, GetWithdrawalDetailsForAmountRequest, GetWithdrawalDetailsForUriRequest, + HintNetworkAvailabilityRequest, ImportDbRequest, InitRequest, InitResponse, @@ -120,6 +123,7 @@ import { StartRefundQueryForUriResponse, StartRefundQueryRequest, StoredBackupList, + TalerMerchantApi, TestPayArgs, TestPayResult, TestingGetDenomStatsRequest, @@ -164,6 +168,7 @@ export enum WalletApiOperation { WithdrawTestBalance = "withdrawTestBalance", PreparePayForUri = "preparePayForUri", SharePayment = "sharePayment", + CheckPayForTemplate = "checkPayForTemplate", PreparePayForTemplate = "preparePayForTemplate", GetContractTermsDetails = "getContractTermsDetails", RunIntegrationTest = "runIntegrationTest", @@ -260,6 +265,7 @@ export enum WalletApiOperation { RemoveGlobalCurrencyAuditor = "removeGlobalCurrencyAuditor", ListAssociatedRefreshes = "listAssociatedRefreshes", Shutdown = "shutdown", + HintNetworkAvailability = "hintNetworkAvailability", CanonicalizeBaseUrl = "canonicalizeBaseUrl", TestingWaitTransactionsFinal = "testingWaitTransactionsFinal", TestingWaitRefreshesFinal = "testingWaitRefreshesFinal", @@ -270,6 +276,7 @@ export enum WalletApiOperation { TestingListTaskForTransaction = "testingListTasksForTransaction", TestingGetDenomStats = "testingGetDenomStats", TestingPing = "testingPing", + TestingGetReserveHistory = "testingGetReserveHistory", } // group: Initialization @@ -310,6 +317,12 @@ export type GetVersionOp = { response: WalletCoreVersion; }; +export type HintNetworkAvailabilityOp = { + op: WalletApiOperation.HintNetworkAvailability; + request: HintNetworkAvailabilityRequest; + response: EmptyObject; +}; + // group: Basic Wallet Information /** @@ -538,6 +551,12 @@ export type SharePaymentOp = { response: SharePaymentResult; }; +export type CheckPayForTemplateOp = { + op: WalletApiOperation.CheckPayForTemplate; + request: CheckPayTemplateRequest; + response: CheckPayTemplateReponse; +}; + /** * Prepare to make a payment based on a taler://pay-template/ URI. */ @@ -1187,6 +1206,12 @@ export type TestingPingOp = { response: EmptyObject; }; +export type TestingGetReserveHistoryOp = { + op: WalletApiOperation.TestingGetReserveHistory; + request: EmptyObject; + response: any; +}; + /** * Get stats about an exchange denomination. */ @@ -1222,6 +1247,7 @@ export type WalletOperations = { [WalletApiOperation.GetVersion]: GetVersionOp; [WalletApiOperation.PreparePayForUri]: PreparePayForUriOp; [WalletApiOperation.SharePayment]: SharePaymentOp; + [WalletApiOperation.CheckPayForTemplate]: CheckPayForTemplateOp; [WalletApiOperation.PreparePayForTemplate]: PreparePayForTemplateOp; [WalletApiOperation.GetContractTermsDetails]: GetContractTermsDetailsOp; [WalletApiOperation.WithdrawTestkudos]: WithdrawTestkudosOp; @@ -1329,6 +1355,8 @@ export type WalletOperations = { [WalletApiOperation.PrepareBankIntegratedWithdrawal]: PrepareBankIntegratedWithdrawalOp; [WalletApiOperation.ConfirmWithdrawal]: ConfirmWithdrawalOp; [WalletApiOperation.CanonicalizeBaseUrl]: CanonicalizeBaseUrlOp; + [WalletApiOperation.TestingGetReserveHistory]: TestingGetReserveHistoryOp; + [WalletApiOperation.HintNetworkAvailability]: HintNetworkAvailabilityOp; }; export type WalletCoreRequestType< diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts @@ -84,6 +84,7 @@ import { codecForAny, codecForApplyDevExperiment, codecForCanonicalizeBaseUrlRequest, + codecForCheckPayTemplateRequest, codecForCheckPeerPullPaymentRequest, codecForCheckPeerPushDebitRequest, codecForConfirmPayRequest, @@ -134,6 +135,7 @@ import { codecForSuspendTransaction, codecForTestPayArgs, codecForTestingGetDenomStatsRequest, + codecForTestingGetReserveHistoryRequest, codecForTestingListTasksForTransactionRequest, codecForTestingSetTimetravelRequest, codecForTransactionByIdRequest, @@ -154,7 +156,10 @@ import { setDangerousTimetravel, validateIban, } from "@gnu-taler/taler-util"; -import type { HttpRequestLibrary } from "@gnu-taler/taler-util/http"; +import { + readSuccessResponseJsonOrThrow, + type HttpRequestLibrary, +} from "@gnu-taler/taler-util/http"; import { getUserAttentions, getUserAttentionsUnreadCount, @@ -224,6 +229,7 @@ import { observeTalerCrypto, } from "./observable-wrappers.js"; import { + checkPayForTemplate, confirmPay, getContractTermsDetails, preparePayForTemplate, @@ -656,9 +662,6 @@ async function handlePrepareWithdrawExchange( } const exchangeBaseUrl = parsedUri.exchangeBaseUrl; const exchange = await fetchFreshExchange(wex, exchangeBaseUrl); - if (parsedUri.exchangePub && exchange.masterPub != parsedUri.exchangePub) { - throw Error("mismatch of exchange master public key (URI vs actual)"); - } if (parsedUri.amount) { const amt = Amounts.parseOrThrow(parsedUri.amount); if (amt.currency !== exchange.currency) { @@ -818,9 +821,7 @@ async function dispatchRequestInternal( } case WalletApiOperation.AddExchange: { const req = codecForAddExchangeRequest().decode(payload); - await fetchFreshExchange(wex, req.exchangeBaseUrl, { - expectedMasterPub: req.masterPub, - }); + await fetchFreshExchange(wex, req.exchangeBaseUrl, {}); return {}; } case WalletApiOperation.TestingPing: { @@ -905,9 +906,36 @@ async function dispatchRequestInternal( } case WalletApiOperation.GetWithdrawalDetailsForUri: { const req = codecForGetWithdrawalDetailsForUri().decode(payload); - return await getWithdrawalDetailsForUri(wex, req.talerWithdrawUri, { - restrictAge: req.restrictAge, + return await getWithdrawalDetailsForUri(wex, req.talerWithdrawUri); + } + case WalletApiOperation.TestingGetReserveHistory: { + const req = codecForTestingGetReserveHistoryRequest().decode(payload); + const reserve = await wex.db.runReadOnlyTx( + { storeNames: ["reserves"] }, + async (tx) => { + return tx.reserves.indexes.byReservePub.get(req.reservePub); + }, + ); + if (!reserve) { + throw Error("no reserve pub found"); + } + const sigResp = await wex.cryptoApi.signReserveHistoryReq({ + reservePriv: reserve.reservePriv, + startOffset: 0, }); + const exchangeBaseUrl = req.exchangeBaseUrl; + const url = new URL( + `reserves/${req.reservePub}/history`, + exchangeBaseUrl, + ); + const resp = await wex.http.fetch(url.href, { + headers: { ["Taler-Reserve-History-Signature"]: sigResp.sig }, + }); + const historyJson = await readSuccessResponseJsonOrThrow( + resp, + codecForAny(), + ); + return historyJson; } case WalletApiOperation.AcceptManualWithdrawal: { const req = codecForAcceptManualWithdrawalRequest().decode(payload); @@ -972,16 +1000,14 @@ async function dispatchRequestInternal( } case WalletApiOperation.ConfirmWithdrawal: { const req = codecForConfirmWithdrawalRequestRequest().decode(payload); - return confirmWithdrawal(wex, req.transactionId); + return confirmWithdrawal(wex, req); } case WalletApiOperation.PrepareBankIntegratedWithdrawal: { const req = codecForPrepareBankIntegratedWithdrawalRequest().decode(payload); return prepareBankIntegratedWithdrawal(wex, { - selectedExchange: req.exchangeBaseUrl, talerWithdrawUri: req.talerWithdrawUri, - forcedDenomSel: req.forcedDenomSel, - restrictAge: req.restrictAge, + selectedExchange: req.selectedExchange, }); } case WalletApiOperation.GetExchangeTos: { @@ -1028,6 +1054,10 @@ 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; @@ -1595,6 +1625,14 @@ async function handleCoreApiRequest( id: string, payload: unknown, ): Promise<CoreApiResponse> { + if (operation !== WalletApiOperation.InitWallet) { + if (!ws.initCalled) { + throw Error("init must be called first"); + } + // Might be lazily initialized! + await ws.taskScheduler.ensureRunning(); + } + let wex: WalletExecutionContext; let oc: ObservabilityContext; @@ -1683,6 +1721,7 @@ export function applyRunConfigDefaults( skipDefaults: wcp?.testing?.skipDefaults ?? false, emitObservabilityEvents: wcp?.testing?.emitObservabilityEvents ?? false, }, + lazyTaskLoop: wcp?.lazyTaskLoop ?? false, }; } diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts @@ -36,6 +36,7 @@ import { BankWithdrawDetails, CancellationToken, CoinStatus, + ConfirmWithdrawalRequest, CurrencySpecification, DenomKeyType, DenomSelItem, @@ -194,6 +195,15 @@ async function updateWithdrawalTransaction( let transactionItem: Transaction; + if ( + !wgRecord.instructedAmount || + !wgRecord.denomsSel || + !wgRecord.exchangeBaseUrl + ) { + // withdrawal group is in preparation, nothing to update + return; + } + if (wgRecord.wgInfo.withdrawalType === WithdrawalRecordType.BankIntegrated) { const txState = computeWithdrawalTransactionStatus(wgRecord); transactionItem = { @@ -224,6 +234,14 @@ async function updateWithdrawalTransaction( } else if ( wgRecord.wgInfo.withdrawalType === WithdrawalRecordType.BankManual ) { + checkDbInvariant( + wgRecord.instructedAmount !== undefined, + "manual withdrawal without amount can't be created", + ); + checkDbInvariant( + wgRecord.denomsSel !== undefined, + "manual withdrawal without denoms can't be created", + ); const exchangeDetails = await getExchangeWireDetailsInTx( tx, wgRecord.exchangeBaseUrl, @@ -839,8 +857,6 @@ export async function getBankWithdrawalInfo( } const { body: status } = resp; - logger.info(`bank withdrawal operation status: ${j2s(status)}`); - return { operationId: uriResult.withdrawalOperationId, apiBaseUrl: uriResult.bankIntegrationApiBaseUrl, @@ -897,6 +913,11 @@ async function processPlanchetGenerate( withdrawalGroup: WithdrawalGroupRecord, coinIdx: number, ): Promise<void> { + checkDbInvariant( + withdrawalGroup.denomsSel !== undefined, + "can't process uninitialized exchange", + ); + const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl; let planchet = await wex.db.runReadOnlyTx( { storeNames: ["planchets"] }, async (tx) => { @@ -934,12 +955,7 @@ async function processPlanchetGenerate( const denom = await wex.db.runReadOnlyTx( { storeNames: ["denominations"] }, async (tx) => { - return getDenomInfo( - wex, - tx, - withdrawalGroup.exchangeBaseUrl, - denomPubHash, - ); + return getDenomInfo(wex, tx, exchangeBaseUrl, denomPubHash); }, ); checkDbInvariant(!!denom); @@ -1060,7 +1076,7 @@ async function handleKycRequired( return TransitionResult.stay(); } for (let i = startIdx; i < requestCoinIdxs.length; i++) { - let planchet = await tx.planchets.indexes.byGroupAndIndex.get([ + const planchet = await tx.planchets.indexes.byGroupAndIndex.get([ withdrawalGroup.withdrawalGroupId, requestCoinIdxs[i], ]); @@ -1105,6 +1121,7 @@ async function processPlanchetExchangeBatchRequest( logger.info( `processing planchet exchange batch request ${withdrawalGroup.withdrawalGroupId}, start=${args.coinStartIndex}, len=${args.batchSize}`, ); + const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl; const batchReq: ExchangeBatchWithdrawRequest = { planchets: [] }; // Indices of coins that are included in the batch request @@ -1119,7 +1136,7 @@ async function processPlanchetExchangeBatchRequest( coinIdx < wgContext.numPlanchets; coinIdx++ ) { - let planchet = await tx.planchets.indexes.byGroupAndIndex.get([ + const planchet = await tx.planchets.indexes.byGroupAndIndex.get([ withdrawalGroup.withdrawalGroupId, coinIdx, ]); @@ -1136,7 +1153,7 @@ async function processPlanchetExchangeBatchRequest( const denom = await getDenomInfo( wex, tx, - withdrawalGroup.exchangeBaseUrl, + exchangeBaseUrl, planchet.denomPubHash, ); @@ -1170,7 +1187,7 @@ async function processPlanchetExchangeBatchRequest( ): Promise<void> { logger.trace(`withdrawal request failed: ${j2s(errDetail)}`); await wex.db.runReadWriteTx({ storeNames: ["planchets"] }, async (tx) => { - let planchet = await tx.planchets.indexes.byGroupAndIndex.get([ + const planchet = await tx.planchets.indexes.byGroupAndIndex.get([ withdrawalGroup.withdrawalGroupId, coinIdx, ]); @@ -1239,11 +1256,13 @@ async function processPlanchetVerifyAndStoreCoin( resp: ExchangeWithdrawResponse, ): Promise<void> { const withdrawalGroup = wgContext.wgRecord; + const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl; + logger.trace(`checking and storing planchet idx=${coinIdx}`); const d = await wex.db.runReadOnlyTx( { storeNames: ["planchets", "denominations"] }, async (tx) => { - let planchet = await tx.planchets.indexes.byGroupAndIndex.get([ + const planchet = await tx.planchets.indexes.byGroupAndIndex.get([ withdrawalGroup.withdrawalGroupId, coinIdx, ]); @@ -1257,7 +1276,7 @@ async function processPlanchetVerifyAndStoreCoin( const denomInfo = await getDenomInfo( wex, tx, - withdrawalGroup.exchangeBaseUrl, + exchangeBaseUrl, planchet.denomPubHash, ); if (!denomInfo) { @@ -1266,7 +1285,7 @@ async function processPlanchetVerifyAndStoreCoin( return { planchet, denomInfo, - exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl, + exchangeBaseUrl: exchangeBaseUrl, }; }, ); @@ -1287,7 +1306,7 @@ async function processPlanchetVerifyAndStoreCoin( throw Error(`cipher (${planchetDenomPub.cipher}) not supported`); } - let evSig = resp.ev_sig; + const evSig = resp.ev_sig; if (!(evSig.cipher === DenomKeyType.Rsa)) { throw Error("unsupported cipher"); } @@ -1306,7 +1325,7 @@ async function processPlanchetVerifyAndStoreCoin( if (!isValid) { await wex.db.runReadWriteTx({ storeNames: ["planchets"] }, async (tx) => { - let planchet = await tx.planchets.indexes.byGroupAndIndex.get([ + const planchet = await tx.planchets.indexes.byGroupAndIndex.get([ withdrawalGroup.withdrawalGroupId, coinIdx, ]); @@ -1485,6 +1504,15 @@ async function processQueryReserve( if (withdrawalGroup.status !== WithdrawalGroupStatus.PendingQueryingStatus) { return TaskRunResult.backoff(); } + checkDbInvariant( + withdrawalGroup.denomsSel !== undefined, + "can't process uninitialized exchange", + ); + checkDbInvariant( + withdrawalGroup.instructedAmount !== undefined, + "can't process uninitialized exchange", + ); + const reservePub = withdrawalGroup.reservePub; const reserveUrl = new URL( @@ -1520,15 +1548,52 @@ async function processQueryReserve( logger.trace(`got reserve status ${j2s(result.response)}`); - const transitionResult = await ctx.transition({}, async (wg) => { - if (!wg) { - logger.warn(`withdrawal group ${withdrawalGroupId} not found`); - return TransitionResult.stay(); - } - wg.status = WithdrawalGroupStatus.PendingReady; - wg.reserveBalanceAmount = Amounts.stringify(result.response.balance); - return TransitionResult.transition(wg); - }); + let amountChanged = false; + if ( + Amounts.cmp( + result.response.balance, + withdrawalGroup.denomsSel.totalWithdrawCost, + ) === -1 + ) { + amountChanged = true; + } + console.log(`amount change ${j2s(result.response)}`); + console.log( + `amount change ${j2s(withdrawalGroup.denomsSel.totalWithdrawCost)}`, + ); + + const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl; + const currency = Amounts.currencyOf(withdrawalGroup.instructedAmount); + + const transitionResult = await ctx.transition( + { + extraStores: ["denominations"], + }, + async (wg, tx) => { + if (!wg) { + logger.warn(`withdrawal group ${withdrawalGroupId} not found`); + return TransitionResult.stay(); + } + if (wg.status !== WithdrawalGroupStatus.PendingQueryingStatus) { + return TransitionResult.stay(); + } + if (amountChanged) { + const candidates = await getCandidateWithdrawalDenomsTx( + wex, + tx, + exchangeBaseUrl, + currency, + ); + wg.denomsSel = selectWithdrawalDenominations( + Amounts.parseOrThrow(result.response.balance), + candidates, + ); + } + wg.status = WithdrawalGroupStatus.PendingReady; + wg.reserveBalanceAmount = Amounts.stringify(result.response.balance); + return TransitionResult.transition(wg); + }, + ); if (transitionResult) { return TaskRunResult.progress(); @@ -1674,6 +1739,10 @@ async function redenominateWithdrawal( if (!wg) { return; } + checkDbInvariant( + wg.denomsSel !== undefined, + "can't process uninitialized exchange", + ); const currency = Amounts.currencyOf(wg.denomsSel.totalWithdrawCost); const exchangeBaseUrl = wg.exchangeBaseUrl; @@ -1690,13 +1759,13 @@ async function redenominateWithdrawal( logger.trace(`old denom sel: ${j2s(oldSel)}`); } - let zero = Amount.zeroOfCurrency(currency); + const zero = Amount.zeroOfCurrency(currency); let amountRemaining = zero; let prevTotalCoinValue = zero; let prevTotalWithdrawalCost = zero; let prevHasDenomWithAgeRestriction = false; let prevEarliestDepositExpiration = AbsoluteTime.never(); - let prevDenoms: DenomSelItem[] = []; + const prevDenoms: DenomSelItem[] = []; let coinIndex = 0; for (let i = 0; i < oldSel.selectedDenoms.length; i++) { const sel = wg.denomsSel.selectedDenoms[i]; @@ -1708,7 +1777,7 @@ async function redenominateWithdrawal( throw Error("denom in use but not not found"); } // FIXME: Also check planchet if there was a different error or planchet already withdrawn - let denomOkay = isWithdrawableDenom( + const denomOkay = isWithdrawableDenom( denom, wex.ws.config.testing.denomselAllowLate, ); @@ -1809,8 +1878,11 @@ async function processWithdrawalGroupPendingReady( const { withdrawalGroupId } = withdrawalGroup; const ctx = new WithdrawTransactionContext(wex, withdrawalGroupId); + checkDbInvariant( + withdrawalGroup.denomsSel !== undefined, + "can't process uninitialized exchange", + ); const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl; - await fetchFreshExchange(wex, withdrawalGroup.exchangeBaseUrl); if (withdrawalGroup.denomsSel.selectedDenoms.length === 0) { @@ -1912,7 +1984,6 @@ async function processWithdrawalGroupPendingReady( const errorsPerCoin: Record<number, TalerErrorDetail> = {}; let numPlanchetErrors = 0; let numActive = 0; - let numDone = 0; const maxReportedErrors = 5; const res = await ctx.transition( @@ -1933,7 +2004,6 @@ async function processWithdrawalGroupPendingReady( numActive++; break; case PlanchetStatus.WithdrawalDone: - numDone++; break; } if (x.lastError) { @@ -2047,9 +2117,7 @@ export async function getExchangeWithdrawalInfo( ageRestricted: number | undefined, ): Promise<ExchangeWithdrawalDetails> { logger.trace("updating exchange"); - const exchange = await fetchFreshExchange(wex, exchangeBaseUrl, { - cancellationToken: wex.cancellationToken, - }); + const exchange = await fetchFreshExchange(wex, exchangeBaseUrl, {}); wex.cancellationToken.throwIfCancelled(); @@ -2176,7 +2244,6 @@ export interface GetWithdrawalDetailsForUriOpts { export async function getWithdrawalDetailsForUri( wex: WalletExecutionContext, talerWithdrawUri: string, - opts: GetWithdrawalDetailsForUriOpts = {}, ): Promise<WithdrawUriInfoResponse> { logger.trace(`getting withdrawal details for URI ${talerWithdrawUri}`); const info = await getBankWithdrawalInfo(wex.http, talerWithdrawUri); @@ -2224,7 +2291,7 @@ export function augmentPaytoUrisForWithdrawal( return plainPaytoUris.map((x) => addPaytoQueryParams(x, { amount: Amounts.stringify(instructedAmount), - message: `Taler Withdrawal ${reservePub}`, + message: `Taler ${reservePub}`, }), ); } @@ -2240,6 +2307,10 @@ export async function getFundingPaytoUris( ): Promise<string[]> { const withdrawalGroup = await tx.withdrawalGroups.get(withdrawalGroupId); checkDbInvariant(!!withdrawalGroup); + checkDbInvariant( + withdrawalGroup.instructedAmount !== undefined, + "can't get funding uri from uninitialized wg", + ); const exchangeDetails = await getExchangeWireDetailsInTx( tx, withdrawalGroup.exchangeBaseUrl, @@ -2550,11 +2621,40 @@ export interface PrepareCreateWithdrawalGroupResult { }; } +async function getInitialDenomsSelection( + wex: WalletExecutionContext, + exchange: string, + amount: AmountJson, + forcedDenoms: ForcedDenomSel | undefined, +): Promise<DenomSelectionState> { + const currency = Amounts.currencyOf(amount); + await updateWithdrawalDenoms(wex, exchange); + const denoms = await getCandidateWithdrawalDenoms(wex, exchange, currency); + + if (forcedDenoms) { + logger.warn("using forced denom selection"); + const initialDenomSel = selectForcedWithdrawalDenominations( + amount, + denoms, + forcedDenoms, + wex.ws.config.testing.denomselAllowLate, + ); + return initialDenomSel; + } else { + const initialDenomSel = selectWithdrawalDenominations( + amount, + denoms, + wex.ws.config.testing.denomselAllowLate, + ); + return initialDenomSel; + } +} + export async function internalPrepareCreateWithdrawalGroup( wex: WalletExecutionContext, args: { reserveStatus: WithdrawalGroupStatus; - amount: AmountJson; + amount?: AmountJson; exchangeBaseUrl: string; forcedWithdrawalGroupId?: string; forcedDenomSel?: ForcedDenomSel; @@ -2569,9 +2669,8 @@ export async function internalPrepareCreateWithdrawalGroup( const secretSeed = encodeCrock(getRandomBytes(32)); const exchangeBaseUrl = args.exchangeBaseUrl; const amount = args.amount; - const currency = Amounts.currencyOf(amount); - let withdrawalGroupId; + let withdrawalGroupId: string; if (args.forcedWithdrawalGroupId) { withdrawalGroupId = args.forcedWithdrawalGroupId; @@ -2594,39 +2693,29 @@ export async function internalPrepareCreateWithdrawalGroup( withdrawalGroupId = encodeCrock(getRandomBytes(32)); } - await updateWithdrawalDenoms(wex, exchangeBaseUrl); - const denoms = await getCandidateWithdrawalDenoms( - wex, - exchangeBaseUrl, - currency, - ); - - let initialDenomSel: DenomSelectionState; + let initialDenomSel: DenomSelectionState | undefined; const denomSelUid = encodeCrock(getRandomBytes(16)); - if (args.forcedDenomSel) { - logger.warn("using forced denom selection"); - initialDenomSel = selectForcedWithdrawalDenominations( + + if (amount !== undefined) { + initialDenomSel = await getInitialDenomsSelection( + wex, + exchangeBaseUrl, amount, - denoms, args.forcedDenomSel, - wex.ws.config.testing.denomselAllowLate, - ); - } else { - initialDenomSel = selectWithdrawalDenominations( - amount, - denoms, - wex.ws.config.testing.denomselAllowLate, ); } const withdrawalGroup: WithdrawalGroupRecord = { denomSelUid, + // next fields will be undefined if exchange or amount is not specified denomsSel: initialDenomSel, exchangeBaseUrl: exchangeBaseUrl, - instructedAmount: Amounts.stringify(amount), + instructedAmount: + amount === undefined ? undefined : Amounts.stringify(amount), + rawWithdrawalAmount: initialDenomSel?.totalWithdrawCost, + effectiveWithdrawalAmount: initialDenomSel?.totalCoinValue, + // end of optional fields timestampStart: timestampPreciseToDb(now), - rawWithdrawalAmount: initialDenomSel.totalWithdrawCost, - effectiveWithdrawalAmount: initialDenomSel.totalCoinValue, secretSeed, reservePriv: reserveKeyPair.priv, reservePub: reserveKeyPair.pub, @@ -2639,6 +2728,7 @@ export async function internalPrepareCreateWithdrawalGroup( }; await fetchFreshExchange(wex, exchangeBaseUrl); + const transactionId = constructTransactionIdentifier({ tag: TransactionType.Withdrawal, withdrawalGroupId: withdrawalGroup.withdrawalGroupId, @@ -2647,10 +2737,12 @@ export async function internalPrepareCreateWithdrawalGroup( return { withdrawalGroup, transactionId, - creationInfo: { - canonExchange: exchangeBaseUrl, - amount, - }, + creationInfo: !amount + ? undefined + : { + amount, + canonExchange: exchangeBaseUrl, + }, }; } @@ -2674,13 +2766,6 @@ export async function internalPerformCreateWithdrawalGroup( prep: PrepareCreateWithdrawalGroupResult, ): Promise<PerformCreateWithdrawalGroupResult> { const { withdrawalGroup } = prep; - if (!prep.creationInfo) { - return { - withdrawalGroup, - transitionInfo: undefined, - exchangeNotif: undefined, - }; - } const existingWg = await tx.withdrawalGroups.get( withdrawalGroup.withdrawalGroupId, ); @@ -2697,7 +2782,14 @@ export async function internalPerformCreateWithdrawalGroup( reservePriv: withdrawalGroup.reservePriv, }); - const exchange = await tx.exchanges.get(withdrawalGroup.exchangeBaseUrl); + if (!prep.creationInfo) { + return { + withdrawalGroup, + transitionInfo: undefined, + exchangeNotif: undefined, + }; + } + const exchange = await tx.exchanges.get(prep.creationInfo.canonExchange); if (exchange) { exchange.lastWithdrawal = timestampPreciseToDb(TalerPreciseTimestamp.now()); await tx.exchanges.put(exchange); @@ -2716,7 +2808,7 @@ export async function internalPerformCreateWithdrawalGroup( const exchangeUsedRes = await markExchangeUsed( wex, tx, - prep.withdrawalGroup.exchangeBaseUrl, + prep.creationInfo.canonExchange, ); const ctx = new WithdrawTransactionContext( @@ -2745,8 +2837,8 @@ export async function internalCreateWithdrawalGroup( wex: WalletExecutionContext, args: { reserveStatus: WithdrawalGroupStatus; - amount: AmountJson; exchangeBaseUrl: string; + amount?: AmountJson; forcedWithdrawalGroupId?: string; forcedDenomSel?: ForcedDenomSel; reserveKeyPair?: EddsaKeypair; @@ -2791,9 +2883,7 @@ export async function prepareBankIntegratedWithdrawal( wex: WalletExecutionContext, req: { talerWithdrawUri: string; - selectedExchange: string; - forcedDenomSel?: ForcedDenomSel; - restrictAge?: number; + selectedExchange?: string; }, ): Promise<PrepareBankIntegratedWithdrawalResponse> { const existingWithdrawalGroup = await wex.db.runReadOnlyTx( @@ -2806,59 +2896,47 @@ export async function prepareBankIntegratedWithdrawal( ); if (existingWithdrawalGroup) { - let url: string | undefined; - if ( - existingWithdrawalGroup.wgInfo.withdrawalType === - WithdrawalRecordType.BankIntegrated - ) { - url = existingWithdrawalGroup.wgInfo.bankInfo.confirmUrl; - } + const info = await getWithdrawalDetailsForUri(wex, req.talerWithdrawUri); return { transactionId: constructTransactionIdentifier({ tag: TransactionType.Withdrawal, withdrawalGroupId: existingWithdrawalGroup.withdrawalGroupId, }), + info, }; } - - const selectedExchange = req.selectedExchange; - 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, - }, - wex.cancellationToken, - ); + 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 + * choose. We are still tracking this object since the state + * can change from the bank side or another wallet with the + * same URI + */ const withdrawalGroup = await internalCreateWithdrawalGroup(wex, { - amount: withdrawInfo.amount, - exchangeBaseUrl: req.selectedExchange, + exchangeBaseUrl, wgInfo: { withdrawalType: WithdrawalRecordType.BankIntegrated, - exchangeCreditAccounts: withdrawalAccountList, bankInfo: { - exchangePaytoUri, talerWithdrawUri: req.talerWithdrawUri, confirmUrl: withdrawInfo.confirmTransferUrl, timestampBankConfirmed: undefined, timestampReserveInfoPosted: undefined, + wireTypes: withdrawInfo.wireTypes, }, }, - restrictAge: req.restrictAge, - forcedDenomSel: req.forcedDenomSel, reserveStatus: WithdrawalGroupStatus.DialogProposed, }); @@ -2870,14 +2948,15 @@ export async function prepareBankIntegratedWithdrawal( return { transactionId: ctx.transactionId, + info, }; } export async function confirmWithdrawal( wex: WalletExecutionContext, - transactionId: string, + req: ConfirmWithdrawalRequest, ): Promise<void> { - const parsedTx = parseTransactionIdentifier(transactionId); + const parsedTx = parseTransactionIdentifier(req.transactionId); if (parsedTx?.tag !== TransactionType.Withdrawal) { throw Error("invalid withdrawal transaction ID"); } @@ -2892,16 +2971,86 @@ export async function confirmWithdrawal( throw Error("withdrawal group not found"); } + if ( + withdrawalGroup.wgInfo.withdrawalType !== + WithdrawalRecordType.BankIntegrated + ) { + throw Error("not a bank integrated withdrawal"); + } + + const selectedExchange = req.exchangeBaseUrl; + const exchange = await fetchFreshExchange(wex, selectedExchange); + + 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 + */ + let wtypes: string[]; + if (withdrawalGroup.wgInfo.bankInfo.wireTypes === undefined) { + const withdrawInfo = await getBankWithdrawalInfo( + wex.http, + talerWithdrawUri, + ); + wtypes = withdrawInfo.wireTypes; + } else { + wtypes = withdrawalGroup.wgInfo.bankInfo.wireTypes; + } + + const exchangePaytoUri = await getExchangePaytoUri( + wex, + selectedExchange, + wtypes, + ); + + const withdrawalAccountList = await fetchWithdrawalAccountInfo( + wex, + { + exchange, + instructedAmount: Amounts.parseOrThrow(req.amount), + }, + wex.cancellationToken, + ); + const ctx = new WithdrawTransactionContext( wex, withdrawalGroup.withdrawalGroupId, ); + const initalDenoms = await getInitialDenomsSelection( + wex, + req.exchangeBaseUrl, + Amounts.parseOrThrow(req.amount), + req.forcedDenomSel, + ); + ctx.transition({}, async (rec) => { if (!rec) { return TransitionResult.stay(); } switch (rec.status) { case WithdrawalGroupStatus.DialogProposed: { + rec.exchangeBaseUrl = req.exchangeBaseUrl; + rec.instructedAmount = req.amount; + rec.denomsSel = initalDenoms; + rec.rawWithdrawalAmount = initalDenoms.totalWithdrawCost; + rec.effectiveWithdrawalAmount = initalDenoms.totalCoinValue; + rec.restrictAge = req.restrictAge; + + rec.wgInfo = { + withdrawalType: WithdrawalRecordType.BankIntegrated, + exchangeCreditAccounts: withdrawalAccountList, + bankInfo: { + exchangePaytoUri, + talerWithdrawUri, + confirmUrl: confirmUrl, + timestampBankConfirmed: undefined, + timestampReserveInfoPosted: undefined, + wireTypes: wtypes, + }, + }; + rec.status = WithdrawalGroupStatus.PendingRegisteringBank; return TransitionResult.transition(rec); } @@ -2996,6 +3145,7 @@ export async function acceptWithdrawalFromUri( confirmUrl: withdrawInfo.confirmTransferUrl, timestampBankConfirmed: undefined, timestampReserveInfoPosted: undefined, + wireTypes: withdrawInfo.wireTypes, }, }, restrictAge: req.restrictAge, @@ -3166,7 +3316,7 @@ async function fetchAccount( }); if (reservePub != null) { paytoUri = addPaytoQueryParams(paytoUri, { - message: `Taler Withdrawal ${reservePub}`, + message: `Taler ${reservePub}`, }); } const acctInfo: WithdrawalExchangeAccountDetails = { diff --git a/packages/taler-wallet-embedded/src/wallet-qjs.ts b/packages/taler-wallet-embedded/src/wallet-qjs.ts @@ -240,8 +240,7 @@ export function installNativeWalletListener(): void { operation: "testing-dangerously-eval", id: msg.id, }; - } - { + } else { respMsg = await handler.handleMessage(operation, id, msg.args ?? {}); } } catch (e) { diff --git a/packages/taler-wallet-webextension/src/components/Part.tsx b/packages/taler-wallet-webextension/src/components/Part.tsx @@ -19,14 +19,15 @@ import { stringifyPaytoUri, TranslatedString, } from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { styled } from "@linaria/react"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; +import arrowDown from "../svg/chevron-down.inline.svg"; import { ExtraLargeText, LargeText, - SmallBoldText, - SmallLightText, + SmallBoldText } from "./styled/index.js"; export type Kind = "positive" | "negative" | "neutral"; @@ -96,11 +97,8 @@ const CollasibleBox = styled.div` } } `; -import arrowDown from "../svg/chevron-down.inline.svg"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -export function PartCollapsible({ text, title, big, showSign }: Props): VNode { - const Text = big ? ExtraLargeText : LargeText; +export function PartCollapsible({ text, title }: Props): VNode { const [collapsed, setCollapsed] = useState(true); return ( diff --git a/packages/taler-wallet-webextension/src/components/WalletActivity.tsx b/packages/taler-wallet-webextension/src/components/WalletActivity.tsx @@ -22,42 +22,80 @@ 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"; -import { Fragment, JSX, VNode, h } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useEffect, useState } from "preact/hooks"; import { Pages } from "../NavigationBar.js"; import { useBackendContext } from "../context/backend.js"; import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; import { useSettings } from "../hooks/useSettings.js"; import { Button } from "../mui/Button.js"; +import { TextField } from "../mui/TextField.js"; +import { SafeHandler } from "../mui/handlers.js"; import { WxApiType } from "../wxApi.js"; +import { WalletActivityTrack } from "../wxBackend.js"; import { Modal } from "./Modal.js"; import { Time } from "./Time.js"; -interface Props extends JSX.HTMLAttributes {} +const OPEN_ACTIVITY_HEIGHT_PX = 250; +const CLOSE_ACTIVITY_HEIGHT_PX = 40; -export function WalletActivity({}: Props): VNode { +export function WalletActivity(): VNode { const { i18n } = useTranslationContext(); - const [settings, updateSettings] = useSettings(); - const api = useBackendContext(); + const [, updateSettings] = useSettings(); + + const [collapsed, setCollcapsed] = useState(true); + useEffect(() => { - document.body.style.marginBottom = "250px"; + document.body.style.marginBottom = `${ + collapsed ? CLOSE_ACTIVITY_HEIGHT_PX : OPEN_ACTIVITY_HEIGHT_PX + }px`; return () => { document.body.style.marginBottom = "0px"; }; - }); - const [table, setTable] = useState<"tasks" | "events">("tasks"); + }, [collapsed]); + + const [table, setTable] = useState<"tasks" | "events">("events"); + if (collapsed) { + return ( + <div + style={{ + position: "fixed", + bottom: 0, + background: "lightgrey", + zIndex: 1, + height: CLOSE_ACTIVITY_HEIGHT_PX, + overflowY: "scroll", + width: "100%", + }} + onClick={() => { + setCollcapsed(!collapsed); + }} + > + <div + style={{ + display: "flex", + justifyContent: "space-around", + marginTop: 10, + cursor: "pointer", + }} + > + click here to open + </div> + </div> + ); + } return ( <div style={{ position: "fixed", bottom: 0, - background: "white", + background: "lightgrey", zIndex: 1, - height: 250, + height: OPEN_ACTIVITY_HEIGHT_PX, overflowY: "scroll", width: "100%", }} @@ -65,23 +103,22 @@ export function WalletActivity({}: Props): VNode { <div style={{ display: "flex", - justifyContent: "space-between", - float: "right", + justifyContent: "space-around", + cursor: "pointer", + }} + onClick={() => { + setCollcapsed(!collapsed); }} > - <div /> - <div> - <div - style={{ padding: 4, margin: 2, border: "solid 1px black" }} - onClick={() => { - updateSettings("showWalletActivity", false); - }} - > - close - </div> - </div> - </div> - <div style={{ display: "flex", justifyContent: "space-around" }}> + <Button + variant={table === "events" ? "contained" : "outlined"} + style={{ margin: 4 }} + onClick={async () => { + setTable("events"); + }} + > + <i18n.Translate>Events</i18n.Translate> + </Button> <Button variant={table === "tasks" ? "contained" : "outlined"} style={{ margin: 4 }} @@ -89,31 +126,38 @@ export function WalletActivity({}: Props): VNode { setTable("tasks"); }} > - <i18n.Translate>Tasks</i18n.Translate> + <i18n.Translate>Active tasks</i18n.Translate> </Button> + <Button - variant={table === "events" ? "contained" : "outlined"} + variant="outlined" style={{ margin: 4 }} onClick={async () => { - setTable("events"); + updateSettings("showWalletActivity", false); }} > - <i18n.Translate>Events</i18n.Translate> + <i18n.Translate>Close</i18n.Translate> </Button> </div> - {(function (): VNode { - switch (table) { - case "events": { - return <ObservabilityEventsTable />; - } - case "tasks": { - return <ActiveTasksTable />; - } - default: { - assertUnreachable(table); + <div + style={{ + backgroundColor: "white", + }} + > + {(function (): VNode { + switch (table) { + case "events": { + return <ObservabilityEventsTable />; + } + case "tasks": { + return <ActiveTasksTable />; + } + default: { + assertUnreachable(table); + } } - } - })()} + })()} + </div> </div> ); } @@ -122,21 +166,6 @@ interface MoreInfoPRops { events: (WalletNotification & { when: AbsoluteTime })[]; onClick: (content: VNode) => void; } -type Notif = { - id: string; - events: (WalletNotification & { when: AbsoluteTime })[]; - description: string; - start: AbsoluteTime; - end: AbsoluteTime; - reference: - | { - eventType: NotificationType; - referenceType: "task" | "transaction" | "operation" | "exchange"; - id: string; - } - | undefined; - MoreInfo: (p: MoreInfoPRops) => VNode; -}; function ShowBalanceChange({ events }: MoreInfoPRops): VNode { if (!events.length) return <Fragment />; @@ -267,10 +296,7 @@ function ShowTransactionStateTransition({ </Fragment> ); } -function ShowExchangeStateTransition({ - events, - onClick, -}: MoreInfoPRops): VNode { +function ShowExchangeStateTransition({ events }: MoreInfoPRops): VNode { if (!events.length) return <Fragment />; const not = events[0]; if (not.type !== NotificationType.ExchangeStateTransition) @@ -323,7 +349,7 @@ type ObservaNotifWithTime = ( }; function ShowObservabilityEvent({ events, onClick }: MoreInfoPRops): VNode { // let prev: ObservaNotifWithTime; - const asd = events.map((not) => { + const asd = events.map((not, idx) => { if ( not.type !== NotificationType.RequestObservabilityEvent && not.type !== NotificationType.TaskObservabilityEvent @@ -364,7 +390,12 @@ function ShowObservabilityEvent({ events, onClick }: MoreInfoPRops): VNode { })(); return ( - <ShowObervavilityDetails title={title} notif={not} onClick={onClick} /> + <ShowObervavilityDetails + key={idx} + title={title} + notif={not} + onClick={onClick} + /> ); }); return ( @@ -673,235 +704,64 @@ function ShowObervavilityDetails({ } } -function getNotificationFor( - id: string, - event: WalletNotification, - start: AbsoluteTime, - list: Notif[], -): Notif | undefined { - const eventWithTime = { ...event, when: start }; - switch (event.type) { - case NotificationType.BalanceChange: { - return { - id, - events: [eventWithTime], - reference: { - eventType: event.type, - referenceType: "transaction", - id: event.hintTransactionId, - }, - description: "Balance change", - start, - end: AbsoluteTime.never(), - MoreInfo: ShowBalanceChange, - }; - } - case NotificationType.BackupOperationError: { - return { - id, - events: [eventWithTime], - reference: undefined, - description: "Backup error", - start, - end: AbsoluteTime.never(), - MoreInfo: ShowBackupOperationError, - }; - } - case NotificationType.TransactionStateTransition: { - const found = list.find( - (a) => - a.reference?.eventType === event.type && - a.reference.id === event.transactionId, - ); - if (found) { - found.end = start; - found.events.unshift(eventWithTime); - return undefined; - } - return { - id, - events: [eventWithTime], - reference: { - eventType: event.type, - referenceType: "transaction", - id: event.transactionId, - }, - description: event.type, - start, - end: AbsoluteTime.never(), - MoreInfo: ShowTransactionStateTransition, - }; - } - case NotificationType.ExchangeStateTransition: { - const found = list.find( - (a) => - a.reference?.eventType === event.type && - a.reference.id === event.exchangeBaseUrl, - ); - if (found) { - found.end = start; - found.events.unshift(eventWithTime); - return undefined; - } - return { - id, - events: [eventWithTime], - description: "Exchange update", - reference: { - eventType: event.type, - referenceType: "exchange", - id: event.exchangeBaseUrl, - }, - start, - end: AbsoluteTime.never(), - MoreInfo: ShowExchangeStateTransition, - }; - } - case NotificationType.TaskObservabilityEvent: { - const found = list.find( - (a) => - a.reference?.eventType === event.type && - a.reference.id === event.taskId, - ); - if (found) { - found.end = start; - found.events.unshift(eventWithTime); - return undefined; - } - return { - id, - events: [eventWithTime], - reference: { - eventType: event.type, - referenceType: "task", - id: event.taskId, - }, - description: `Task update ${event.taskId}`, - start, - end: AbsoluteTime.never(), - MoreInfo: ShowObservabilityEvent, - }; - } - case NotificationType.WithdrawalOperationTransition: { - const found = list.find( - (a) => - a.reference?.eventType === event.type && a.reference.id === event.uri, - ); - if (found) { - found.end = start; - found.events.unshift(eventWithTime); - return undefined; - } - return { - id, - events: [eventWithTime], - reference: { - eventType: event.type, - referenceType: "task", - id: event.uri, - }, - description: `Withdrawal operation updated`, - start, - end: AbsoluteTime.never(), - MoreInfo: ShowObservabilityEvent, - }; - } - case NotificationType.RequestObservabilityEvent: { - const found = list.find( - (a) => - a.reference?.eventType === event.type && - a.reference.id === event.requestId, - ); - if (found) { - found.end = start; - found.events.unshift(eventWithTime); - return undefined; - } - return { - id, - events: [eventWithTime], - reference: { - eventType: event.type, - referenceType: "operation", - id: event.requestId, - }, - description: `wallet.${event.operation}(${event.requestId})`, - start, - end: AbsoluteTime.never(), - MoreInfo: ShowObservabilityEvent, - }; - } - case NotificationType.Idle: - return undefined; - default: { - assertUnreachable(event); - } - } -} - -function refresh(api: WxApiType, onUpdate: (list: Notif[]) => void) { +function refresh( + api: WxApiType, + onUpdate: (list: WalletActivityTrack[]) => void, + filter: string, +) { api.background - .call("getNotifications", undefined) + .call("getNotifications", { filter }) .then((notif) => { - const list: Notif[] = []; - for (const n of notif) { - if ( - n.notification.type === NotificationType.RequestObservabilityEvent && - n.notification.operation === "getActiveTasks" - ) { - //ignore monitor request - continue; - } - const event = getNotificationFor( - String(list.length), - n.notification, - n.when, - list, - ); - // pepe. - if (event) { - list.unshift(event); - } - } - onUpdate(list); + onUpdate(notif); }) .catch((error) => { console.log(error); }); } -export function ObservabilityEventsTable({}: {}): VNode { +export function ObservabilityEventsTable(): VNode { const { i18n } = useTranslationContext(); const api = useBackendContext(); - const [notifications, setNotifications] = useState<Notif[]>([]); + const [notifications, setNotifications] = useState<WalletActivityTrack[]>([]); const [showDetails, setShowDetails] = useState<VNode>(); + const [filter, onChangeFilter] = useState(""); useEffect(() => { let lastTimeout: ReturnType<typeof setTimeout>; function periodicRefresh() { - refresh(api, setNotifications); + refresh(api, setNotifications, filter); lastTimeout = setTimeout(() => { periodicRefresh(); }, 1000); - //clear on unload return () => { clearTimeout(lastTimeout); }; } return periodicRefresh(); - }, [1]); + }, [filter]); return ( <div> <div style={{ display: "flex", justifyContent: "space-between" }}> + <TextField + label="Filter" + variant="outlined" + value={filter} + onChange={onChangeFilter} + /> <div - style={{ padding: 4, margin: 2, border: "solid 1px black" }} + style={{ + padding: 4, + margin: 2, + border: "solid 1px black", + alignSelf: "center", + }} onClick={() => { - api.background.call("clearNotifications", undefined).then((d) => { - refresh(api, setNotifications); + api.background.call("clearNotifications", undefined).then(() => { + refresh(api, setNotifications, filter); }); }} > @@ -914,7 +774,7 @@ export function ObservabilityEventsTable({}: {}): VNode { onClose={{ onClick: (async () => { setShowDetails(undefined); - }) as any, + }) as SafeHandler<void>, }} > {showDetails} @@ -932,7 +792,40 @@ export function ObservabilityEventsTable({}: {}): VNode { padding: 4, }} > - <div style={{ padding: 4 }}>{not.description}</div> + <div style={{ padding: 4 }}> + {(() => { + switch (not.type) { + case NotificationType.BalanceChange: + return i18n.str`Balance change`; + case NotificationType.BackupOperationError: + return i18n.str`Backup failed`; + case NotificationType.TransactionStateTransition: + return i18n.str`Transaction updated`; + case NotificationType.ExchangeStateTransition: + return i18n.str`Exchange updated`; + case NotificationType.Idle: + return i18n.str`Idle`; + case NotificationType.TaskObservabilityEvent: + return i18n.str`task.${ + (not.events[0] as TaskProgressNotification).taskId + }`; + case NotificationType.RequestObservabilityEvent: + return i18n.str`wallet.${ + (not.events[0] as RequestProgressNotification) + .operation + }(${ + (not.events[0] as RequestProgressNotification) + .requestId + })`; + case NotificationType.WithdrawalOperationTransition: { + return `---`; + } + default: { + assertUnreachable(not.type); + } + } + })()} + </div> <div style={{ padding: 4 }}> <Time timestamp={not.start} format="yyyy/MM/dd HH:mm:ss" /> </div> @@ -941,12 +834,76 @@ export function ObservabilityEventsTable({}: {}): VNode { </div> </div> </summary> - <not.MoreInfo - events={not.events} - onClick={(details) => { - setShowDetails(details); - }} - /> + {(() => { + switch (not.type) { + case NotificationType.BalanceChange: { + return ( + <ShowBalanceChange + events={not.events} + onClick={(details) => { + setShowDetails(details); + }} + /> + ); + } + case NotificationType.BackupOperationError: { + return ( + <ShowBackupOperationError + events={not.events} + onClick={(details) => { + setShowDetails(details); + }} + /> + ); + } + case NotificationType.TransactionStateTransition: { + return ( + <ShowTransactionStateTransition + events={not.events} + onClick={(details) => { + setShowDetails(details); + }} + /> + ); + } + case NotificationType.ExchangeStateTransition: { + return ( + <ShowExchangeStateTransition + events={not.events} + onClick={(details) => { + setShowDetails(details); + }} + /> + ); + } + case NotificationType.Idle: { + return <div>not implemented</div>; + } + case NotificationType.TaskObservabilityEvent: { + return ( + <ShowObservabilityEvent + events={not.events} + onClick={(details) => { + setShowDetails(details); + }} + /> + ); + } + case NotificationType.RequestObservabilityEvent: { + return ( + <ShowObservabilityEvent + events={not.events} + onClick={(details) => { + setShowDetails(details); + }} + /> + ); + } + case NotificationType.WithdrawalOperationTransition: { + return <div>not implemented</div>; + } + } + })()} </details> ); })} @@ -965,7 +922,7 @@ function ErroDetailModal({ <Modal title="Full detail" onClose={{ - onClick: onClose as any, + onClick: onClose as SafeHandler<void>, }} > <dl> @@ -987,7 +944,7 @@ function ErroDetailModal({ ); } -export function ActiveTasksTable({}: {}): VNode { +export function ActiveTasksTable(): VNode { const { i18n } = useTranslationContext(); const api = useBackendContext(); const state = useAsyncAsHook(() => { @@ -1006,13 +963,6 @@ export function ActiveTasksTable({}: {}): VNode { }; }, [tasks]); - // const listenAllEvents = Array.from<NotificationType>({ length: 1 }); - // listenAllEvents.includes = () => true - // useEffect(() => { - // return api.listener.onUpdateNotification(listenAllEvents, (notif) => { - // state?.retry() - // }); - // }); return ( <Fragment> {showError && ( @@ -1051,7 +1001,7 @@ export function ActiveTasksTable({}: {}): VNode { {tasks.map((task) => { const [type, id] = task.taskId.split(":"); return ( - <tr> + <tr key={id}> <td>{type}</td> <td title={id}>{id.substring(0, 10)}</td> <td> diff --git a/packages/taler-wallet-webextension/src/components/styled/index.tsx b/packages/taler-wallet-webextension/src/components/styled/index.tsx @@ -690,6 +690,16 @@ export const SmallBoldText = styled.div` font-weight: bold; `; +export const AgeSign = styled.div<{size:number}>` + display: inline-block; + border: red solid 1px; + border-radius: 100%; + width: ${({ size }: {size:number}) => (`${size}px`)}; + height: ${({ size }: {size:number}) => (`${size}px`)}; + line-height: ${({ size }: {size:number}) => (`${size}px`)}; + padding: 3px; +`; + export const LargeText = styled.div` font-size: large; `; diff --git a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx @@ -27,7 +27,11 @@ import { Part } from "../../components/Part.js"; import { PaymentButtons } from "../../components/PaymentButtons.js"; import { ShowFullContractTermPopup } from "../../components/ShowFullContractTermPopup.js"; import { Time } from "../../components/Time.js"; -import { SuccessBox, WarningBox } from "../../components/styled/index.js"; +import { + AgeSign, + SuccessBox, + WarningBox, +} from "../../components/styled/index.js"; import { MerchantDetails } from "../../wallet/Transaction.js"; import { State } from "./index.js"; import { EnabledBySettings } from "../../components/EnabledBySettings.js"; @@ -56,7 +60,17 @@ export function BaseView(state: SupportedStates): VNode { <section style={{ textAlign: "left" }}> <Part - title={i18n.str`Purchase`} + title={ + contractTerms.minimum_age ? ( + <Fragment> + <i18n.Translate>Purchase</i18n.Translate> + + <AgeSign size={20} title={i18n.str`This purchase is age restricted.`}>{contractTerms.minimum_age}+</AgeSign> + </Fragment> + ) : ( + <i18n.Translate>Purchase</i18n.Translate> + ) + } text={contractTerms.summary as TranslatedString} kind="neutral" /> diff --git a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/index.ts b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/index.ts @@ -53,9 +53,9 @@ export namespace State { export interface FillTemplate { status: "fill-template"; error: undefined; - currency: string; amount?: AmountFieldHandler; summary?: TextFieldHandler; + minAge: number; onCreate: ButtonHandler; } diff --git a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts @@ -16,12 +16,13 @@ import { Amounts, PreparePayResult } 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 { AmountFieldHandler, TextFieldHandler } from "../../mui/handlers.js"; +import { RecursiveState } from "../../utils/index.js"; import { Props, State } from "./index.js"; export function useComponentState({ @@ -29,43 +30,32 @@ export function useComponentState({ cancel, goToWalletManualWithdraw, onSuccess, -}: Props): State { +}: Props): RecursiveState<State> { const api = useBackendContext(); const { i18n } = useTranslationContext(); const { safely } = useAlertContext(); - const url = talerTemplateUri ? new URL(talerTemplateUri) : undefined; - - const amountParam = !url - ? undefined - : url.searchParams.get("amount") ?? undefined; - const summaryParam = !url - ? undefined - : url.searchParams.get("summary") ?? undefined; + // const url = talerTemplateUri ? new URL(talerTemplateUri) : undefined; + // const parsedAmount = !amountParam ? undefined : Amounts.parse(amountParam); + // const currency = parsedAmount ? parsedAmount.currency : amountParam; - const parsedAmount = !amountParam ? undefined : Amounts.parse(amountParam); - const currency = parsedAmount ? parsedAmount.currency : amountParam; + // const initialAmount = + // parsedAmount ?? (currency ? Amounts.zeroOfCurrency(currency) : undefined); - const initialAmount = - parsedAmount ?? (currency ? Amounts.zeroOfCurrency(currency) : undefined); - const [amount, setAmount] = useState(initialAmount); - const [summary, setSummary] = useState(summaryParam); const [newOrder, setNewOrder] = useState(""); const hook = useAsyncAsHook(async () => { if (!talerTemplateUri) throw Error("ERROR_NO-URI-FOR-PAYMENT-TEMPLATE"); + const templateP = await api.wallet.call( + WalletApiOperation.CheckPayForTemplate, { talerPayTemplateUri: talerTemplateUri }, + ); + const requireMoreInfo = !templateP.templateDetails.template_contract.amount || !templateP.templateDetails.template_contract.summary; let payStatus: PreparePayResult | undefined = undefined; - if (!amountParam && !summaryParam) { - payStatus = await api.wallet.call( - WalletApiOperation.PreparePayForTemplate, - { - talerPayTemplateUri: talerTemplateUri, - templateParams: {}, - }, - ); + if (!requireMoreInfo) { + payStatus = await api.wallet.call(WalletApiOperation.PreparePayForTemplate, { talerPayTemplateUri: talerTemplateUri }); } const balance = await api.wallet.call(WalletApiOperation.GetBalances, {}); - return { payStatus, balance, uri: talerTemplateUri }; + return { payStatus, balance, uri: talerTemplateUri, templateP }; }, []); if (!hook) { @@ -108,61 +98,83 @@ export function useComponentState({ }; } - async function createOrder() { - try { - const templateParams: Record<string, string> = {}; - if (amount) { - templateParams["amount"] = Amounts.stringify(amount); + return () => { + 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 ?? ""); + + async function createOrder() { + try { + const templateParams: Record<string, string> = {}; + if (amount && !fixedAmount) { + templateParams["amount"] = Amounts.stringify(amount); + } + if (summary && !fixedSummary) { + templateParams["summary"] = summary; + } + const payStatus = await api.wallet.call( + WalletApiOperation.PreparePayForTemplate, + { + talerPayTemplateUri: talerTemplateUri, + templateParams, + }, + ); + setNewOrder(payStatus.talerUri!); + } catch (e) { + console.error(e); } - if (summary) { - templateParams["summary"] = summary; - } - const payStatus = await api.wallet.call( - WalletApiOperation.PreparePayForTemplate, - { - talerPayTemplateUri: talerTemplateUri, - templateParams, - }, - ); - setNewOrder(payStatus.talerUri!); - } catch (e) { - console.error(e); } - } - const errors = undefinedIfEmpty({ - amount: amount && Amounts.isZero(amount) ? i18n.str`required` : undefined, - summary: summary !== undefined && !summary ? i18n.str`required` : undefined, - }); - return { - status: "fill-template", - error: undefined, - currency: currency!, //currency is always not null - amount: - amount !== undefined - ? ({ + + 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, + }); + 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: - summary !== undefined - ? ({ + : undefined, + summary: + fixedSummary === undefined + ? ({ onInput: (t) => { setSummary(t); }, value: summary, error: errors?.summary, } as TextFieldHandler) - : undefined, - onCreate: { - onClick: errors - ? undefined - : safely("create order for pay template", createOrder), - }, - }; + : undefined, + 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 @@ -14,17 +14,17 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { AmountField } from "../../components/AmountField.js"; -import { Part } from "../../components/Part.js"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Button } from "../../mui/Button.js"; import { TextField } from "../../mui/TextField.js"; import { State } from "./index.js"; +import { AgeSign } from "../../components/styled/index.js"; export function ReadyView({ - currency, amount, + minAge, summary, onCreate, }: State.FillTemplate): VNode { @@ -67,6 +67,12 @@ export function ReadyView({ </p> )} </section> + {minAge && ( + <section> + <AgeSign size={25}>{minAge}+</AgeSign> + <i18n.Translate>This purchase is age restricted.</i18n.Translate> + </section> + )} <section> <Button onClick={onCreate.onClick} variant="contained" color="success"> <i18n.Translate>Review order</i18n.Translate> diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts @@ -18,8 +18,7 @@ import { AmountJson, AmountString, CurrencySpecification, - ExchangeListItem, - WithdrawalExchangeAccountDetails, + ExchangeListItem } from "@gnu-taler/taler-util"; import { Loading } from "../../components/Loading.js"; import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js"; @@ -84,6 +83,8 @@ export namespace State { export interface AlreadyCompleted { status: "already-completed"; operationState: "confirmed" | "aborted" | "selected"; + thisWallet: boolean; + redirectToTx: () => void; confirmTransferUrl?: string, error: undefined; } diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts @@ -16,11 +16,13 @@ import { AmountJson, + AmountString, Amounts, ExchangeFullDetails, ExchangeListItem, NotificationType, - parseWithdrawExchangeUri + TransactionMajorState, + parseWithdrawExchangeUri, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; @@ -42,6 +44,7 @@ export function useComponentStateFromParams({ const api = useBackendContext(); const { i18n } = useTranslationContext(); const paramsAmount = amount ? Amounts.parse(amount) : undefined; + const [updatedExchangeByUser, setUpdatedExchangeByUser] = useState<string>(); const uriInfoHook = useAsyncAsHook(async () => { const exchanges = await api.wallet.call( WalletApiOperation.ListExchanges, @@ -50,12 +53,12 @@ export function useComponentStateFromParams({ const uri = maybeTalerUri ? parseWithdrawExchangeUri(maybeTalerUri) : undefined; - const exchangeByTalerUri = uri?.exchangeBaseUrl; + const exchangeByTalerUri = updatedExchangeByUser ?? uri?.exchangeBaseUrl; + let ex: ExchangeFullDetails | undefined; if (exchangeByTalerUri) { await api.wallet.call(WalletApiOperation.AddExchange, { exchangeBaseUrl: exchangeByTalerUri, - masterPub: uri.exchangePub, }); const info = await api.wallet.call( WalletApiOperation.GetExchangeDetailedInfo, @@ -139,8 +142,8 @@ export function useComponentStateFromParams({ confirm: { onClick: isValid ? pushAlertOnError(async () => { - onAmountChanged(Amounts.stringify(amount)); - }) + onAmountChanged(Amounts.stringify(amount)); + }) : undefined, }, amount: { @@ -157,6 +160,7 @@ export function useComponentStateFromParams({ async function doManualWithdraw( exchange: string, ageRestricted: number | undefined, + amount: AmountString, ): Promise<{ transactionId: string; confirmTransferUrl: string | undefined; @@ -165,7 +169,7 @@ export function useComponentStateFromParams({ WalletApiOperation.AcceptManualWithdrawal, { exchangeBaseUrl: exchange, - amount: Amounts.stringify(chosenAmount), + amount, restrictAge: ageRestricted, }, ); @@ -184,6 +188,7 @@ export function useComponentStateFromParams({ chosenAmount, exchangeList, exchangeByTalerUri, + setUpdatedExchangeByUser, ); } @@ -194,6 +199,8 @@ export function useComponentStateFromURI({ }: PropsFromURI): RecursiveState<State> { const api = useBackendContext(); const { i18n } = useTranslationContext(); + + const [updatedExchangeByUser, setUpdatedExchangeByUser] = useState<string>(); /** * Ask the wallet about the withdraw URI */ @@ -204,29 +211,30 @@ export function useComponentStateFromURI({ : maybeTalerUri; const uriInfo = await api.wallet.call( - WalletApiOperation.GetWithdrawalDetailsForUri, + WalletApiOperation.PrepareBankIntegratedWithdrawal, { talerWithdrawUri, - // notifyChangeFromPendingTimeoutMs: 30 * 1000, + selectedExchange: updatedExchangeByUser, }, ); const { amount, defaultExchangeBaseUrl, possibleExchanges, - operationId, confirmTransferUrl, status, - } = uriInfo; - const transaction = await api.wallet.call( - WalletApiOperation.GetWithdrawalTransactionByUri, - { talerWithdrawUri }, - ); + } = uriInfo.info; + const txInfo = + uriInfo.transactionId === undefined + ? undefined + : await api.wallet.call(WalletApiOperation.GetTransactionById, { + transactionId: uriInfo.transactionId, + }); return { talerWithdrawUri, - operationId, status, - transaction, + transactionId: uriInfo.transactionId, + txInfo: txInfo, confirmTransferUrl, amount: Amounts.parseOrThrow(amount), thisExchange: defaultExchangeBaseUrl, @@ -237,12 +245,21 @@ export function useComponentStateFromURI({ const readyToListen = uriInfoHook && !uriInfoHook.hasError; useEffect(() => { - if (!uriInfoHook) { + if (!uriInfoHook || uriInfoHook.hasError) { return; } + const txId = uriInfoHook.response.transactionId; + return api.listener.onUpdateNotification( - [NotificationType.WithdrawalOperationTransition], - uriInfoHook.retry, + [NotificationType.TransactionStateTransition], + (notif) => { + if ( + notif.type === NotificationType.TransactionStateTransition && + notif.transactionId === txId + ) { + uriInfoHook.retry(); + } + }, ); }, [readyToListen]); @@ -260,6 +277,7 @@ 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; @@ -267,32 +285,34 @@ export function useComponentStateFromURI({ async function doManagedWithdraw( exchange: string, ageRestricted: number | undefined, + amount: AmountString, ): Promise<{ transactionId: string; confirmTransferUrl: string | undefined; }> { - const res = await api.wallet.call( - WalletApiOperation.AcceptBankIntegratedWithdrawal, - { - exchangeBaseUrl: exchange, - talerWithdrawUri: uri, - restrictAge: ageRestricted, - }, - ); + if (!txId) { + throw Error("can't confirm transaction"); + } + const res = await api.wallet.call(WalletApiOperation.ConfirmWithdrawal, { + exchangeBaseUrl: exchange, + amount, + restrictAge: ageRestricted, + transactionId: txId, + }); return { confirmTransferUrl: res.confirmTransferUrl, transactionId: res.transactionId, }; } - if (uriInfoHook.response.status !== "pending") { - if (uriInfoHook.response.transaction) { - onSuccess(uriInfoHook.response.transaction.transactionId); - } + 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, + thisWallet: info.txState.major === TransactionMajorState.Pending, + redirectToTx: () => onSuccess(info.transactionId), error: undefined, }; } @@ -306,6 +326,7 @@ export function useComponentStateFromURI({ chosenAmount, exchangeList, defaultExchange, + setUpdatedExchangeByUser, ); }, []); } @@ -313,6 +334,7 @@ export function useComponentStateFromURI({ type ManualOrManagedWithdrawFunction = ( exchange: string, ageRestricted: number | undefined, + amount: AmountString, ) => Promise<{ transactionId: string; confirmTransferUrl: string | undefined }>; function exchangeSelectionState( @@ -323,6 +345,7 @@ function exchangeSelectionState( chosenAmount: AmountJson, exchangeList: ExchangeListItem[], exchangeSuggestedByTheBank: string | undefined, + onExchangeUpdated: (ex: string) => void, ): RecursiveState<State> { const api = useBackendContext(); const selectedExchange = useSelectedExchange({ @@ -331,6 +354,16 @@ function exchangeSelectionState( list: exchangeList, }); + const current = + selectedExchange.status !== "ready" + ? undefined + : selectedExchange.selected.exchangeBaseUrl; + useEffect(() => { + if (current) { + onExchangeUpdated(current); + } + }, [current]); + if (selectedExchange.status !== "ready") { return selectedExchange; } @@ -381,6 +414,7 @@ function exchangeSelectionState( const res = await doWithdraw( currentExchange.exchangeBaseUrl, !ageRestricted ? undefined : ageRestricted, + Amounts.stringify(chosenAmount), ); if (res.confirmTransferUrl) { document.location.href = res.confirmTransferUrl; @@ -432,12 +466,12 @@ function exchangeSelectionState( //TODO: calculate based on exchange info const ageRestriction = ageRestrictionEnabled ? { - list: ageRestrictionOptions, - value: String(ageRestricted), - onChange: pushAlertOnError(async (v: string) => - setAgeRestricted(parseInt(v, 10)), - ), - } + list: ageRestrictionOptions, + value: String(ageRestricted), + onChange: pushAlertOnError(async (v: string) => + setAgeRestricted(parseInt(v, 10)), + ), + } : undefined; const altCurrencies = amountHook.response.accounts @@ -457,9 +491,9 @@ function exchangeSelectionState( const conversionInfo = !convAccount ? undefined : { - spec: convAccount.currencySpecification!, - amount: Amounts.parseOrThrow(convAccount.transferAmount!), - }; + spec: convAccount.currencySpecification!, + amount: Amounts.parseOrThrow(convAccount.transferAmount!), + }; return { status: "success", diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts @@ -99,7 +99,7 @@ describe("Withdraw CTA states", () => { expect(handler.getCallingQueueState()).eq("empty"); }); - it("should tell the user that there is not known exchange", async () => { + it.skip("should tell the user that there is not known exchange", async () => { const { handler, TestingContext } = createWalletApiMock(); const props = { talerWithdrawUri: "taler-withdraw://", @@ -108,22 +108,18 @@ describe("Withdraw CTA states", () => { }; handler.addWalletCallResponse( - WalletApiOperation.GetWithdrawalDetailsForUri, + WalletApiOperation.PrepareBankIntegratedWithdrawal, undefined, { - status: "pending", - operationId: "123", - amount: "EUR:2" as AmountString, - possibleExchanges: [], + transactionId: "123", + info: { + status: "pending", + operationId: "123", + amount: "EUR:2" as AmountString, + possibleExchanges: [], + } }, ); - handler.addWalletCallResponse( - WalletApiOperation.GetWithdrawalTransactionByUri, - undefined, - { - transactionId: "123" - } as any, - ); const hookBehavior = await tests.hookBehaveLikeThis( useComponentStateFromURI, @@ -144,7 +140,7 @@ describe("Withdraw CTA states", () => { expect(handler.getCallingQueueState()).eq("empty"); }); - it("should be able to withdraw if tos are ok", async () => { + it.skip("should be able to withdraw if tos are ok", async () => { const { handler, TestingContext } = createWalletApiMock(); const props = { talerWithdrawUri: "taler-withdraw://", @@ -153,24 +149,20 @@ describe("Withdraw CTA states", () => { }; handler.addWalletCallResponse( - WalletApiOperation.GetWithdrawalDetailsForUri, + WalletApiOperation.PrepareBankIntegratedWithdrawal, undefined, { - status: "pending", - operationId: "123", - amount: "ARS:2" as AmountString, - possibleExchanges: exchanges, - defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl, + transactionId: "123", + info: { + status: "pending", + operationId: "123", + amount: "ARS:2" as AmountString, + possibleExchanges: exchanges, + defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl, + } }, ); handler.addWalletCallResponse( - WalletApiOperation.GetWithdrawalTransactionByUri, - undefined, - { - transactionId: "123" - } as any, - ); - handler.addWalletCallResponse( WalletApiOperation.GetWithdrawalDetailsForAmount, undefined, { diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx @@ -23,7 +23,12 @@ import { Part } from "../../components/Part.js"; import { QR } from "../../components/QR.js"; import { SelectList } from "../../components/SelectList.js"; import { TermsOfService } from "../../components/TermsOfService/index.js"; -import { Input, LinkSuccess, SvgIcon, WarningBox } from "../../components/styled/index.js"; +import { + Input, + LinkSuccess, + SvgIcon, + WarningBox, +} from "../../components/styled/index.js"; import { Button } from "../../mui/Button.js"; import { Grid } from "../../mui/Grid.js"; import editIcon from "../../svg/edit_24px.inline.svg"; @@ -37,28 +42,102 @@ import { EnabledBySettings } from "../../components/EnabledBySettings.js"; export function FinalStateOperation(state: State.AlreadyCompleted): VNode { const { i18n } = useTranslationContext(); + // document.location.href = res.confirmTransferUrl + if (state.thisWallet) { + switch (state.operationState) { + case "confirmed": { + state.redirectToTx(); + return ( + <WarningBox> + <div style={{ justifyContent: "center", lineHeight: "25px" }}> + <i18n.Translate> + This operation has already been completed. + </i18n.Translate> + </div> + </WarningBox> + ); + } + case "aborted": { + state.redirectToTx(); + return ( + <WarningBox> + <div style={{ justifyContent: "center", lineHeight: "25px" }}> + <i18n.Translate> + This operation has already been aborted + </i18n.Translate> + </div> + </WarningBox> + ); + } + case "selected": { + if (state.confirmTransferUrl) { + document.location.href = state.confirmTransferUrl; + } + return ( + <WarningBox> + <div style={{ justifyContent: "center", lineHeight: "25px" }}> + <i18n.Translate> + This operation has started and should be completed in the bank. + </i18n.Translate> + </div> + {state.confirmTransferUrl && ( + <div style={{ justifyContent: "center", lineHeight: "25px" }}> + <i18n.Translate> + You can confirm the operation in + </i18n.Translate> + + <a + target="_bank" + rel="noreferrer" + href={state.confirmTransferUrl} + > + <i18n.Translate>this page</i18n.Translate> + </a> + </div> + )} + </WarningBox> + ); + } + } + } switch (state.operationState) { - case "confirmed": return <WarningBox> - <div style={{ justifyContent: "center", lineHeight: "25px" }}> - <i18n.Translate>This operation has already been completed by another wallet.</i18n.Translate> - </div> - </WarningBox> - case "aborted": return <WarningBox> - <div style={{ justifyContent: "center", lineHeight: "25px" }}> - <i18n.Translate>This operation has already been aborted</i18n.Translate> - </div> - </WarningBox> - case "selected": return <WarningBox> - <div style={{ justifyContent: "center", lineHeight: "25px" }}> - <i18n.Translate>This operation has already been used by another wallet.</i18n.Translate> - </div> - <div style={{ justifyContent: "center", lineHeight: "25px" }}> - <i18n.Translate>It can be confirmed in</i18n.Translate> <a target="_bank" rel="noreferrer" href={state.confirmTransferUrl}> - <i18n.Translate>this page</i18n.Translate> - </a> - </div> - </WarningBox> + case "confirmed": + return ( + <WarningBox> + <div style={{ justifyContent: "center", lineHeight: "25px" }}> + <i18n.Translate> + This operation has already been completed by another wallet. + </i18n.Translate> + </div> + </WarningBox> + ); + case "aborted": + return ( + <WarningBox> + <div style={{ justifyContent: "center", lineHeight: "25px" }}> + <i18n.Translate> + This operation has already been aborted + </i18n.Translate> + </div> + </WarningBox> + ); + case "selected": + return ( + <WarningBox> + <div style={{ justifyContent: "center", lineHeight: "25px" }}> + <i18n.Translate> + This operation has already been used by another wallet. + </i18n.Translate> + </div> + <div style={{ justifyContent: "center", lineHeight: "25px" }}> + <i18n.Translate>It can be confirmed in</i18n.Translate> + <a target="_bank" rel="noreferrer" href={state.confirmTransferUrl}> + <i18n.Translate>this page</i18n.Translate> + </a> + </div> + </WarningBox> + ); } } @@ -95,21 +174,31 @@ export function SuccessView(state: State.Success): VNode { kind="neutral" big /> - {state.chooseCurrencies.length > 0 ? + {state.chooseCurrencies.length > 0 ? ( <Fragment> <p> - {state.chooseCurrencies.map(currency => { - return <Button variant={currency === state.selectedCurrency ? "contained" : "outlined"} - onClick={async () => { - state.changeCurrency(currency) - }} - > - {currency} - </Button> + {state.chooseCurrencies.map((currency) => { + return ( + <Button + key={currency} + variant={ + currency === state.selectedCurrency + ? "contained" + : "outlined" + } + onClick={async () => { + state.changeCurrency(currency); + }} + > + {currency} + </Button> + ); })} </p> </Fragment> - : <Fragment />} + ) : ( + <Fragment /> + )} <Part title={i18n.str`Details`} @@ -202,7 +291,6 @@ function WithdrawWithMobile({ } export function SelectAmountView({ - currency, amount, exchangeBaseUrl, confirm, diff --git a/packages/taler-wallet-webextension/src/i18n/de.po b/packages/taler-wallet-webextension/src/i18n/de.po @@ -17,7 +17,7 @@ msgstr "" "Project-Id-Version: Taler Wallet\n" "Report-Msgid-Bugs-To: languages@taler.net\n" "POT-Creation-Date: 2016-11-23 00:00+0100\n" -"PO-Revision-Date: 2023-11-25 17:24+0000\n" +"PO-Revision-Date: 2024-05-07 14:32+0000\n" "Last-Translator: Stefan Kügel <skuegel@web.de>\n" "Language-Team: German <https://weblate.taler.net/projects/gnu-taler/" "webextensions/de/>\n" @@ -26,7 +26,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.2.1\n" +"X-Generator: Weblate 5.4.3\n" #: src/NavigationBar.tsx:139 #, c-format @@ -56,7 +56,7 @@ msgstr "Dev" #: src/mui/Typography.tsx:122 #, c-format msgid "%1$s" -msgstr "" +msgstr "%1$s" #: src/components/PendingTransactions.tsx:74 #, c-format @@ -215,7 +215,7 @@ msgstr "" #: src/wallet/AddNewActionView.tsx:57 #, c-format msgid "Cancel" -msgstr "Abbrechen" +msgstr "Zurück" #: src/wallet/AddNewActionView.tsx:68 #, c-format @@ -325,7 +325,7 @@ msgstr "" #: src/components/ShowFullContractTermPopup.tsx:189 #, c-format msgid "Summary" -msgstr "" +msgstr "Zusammenfassung" #: src/components/ShowFullContractTermPopup.tsx:195 #, c-format @@ -370,7 +370,7 @@ msgstr "" #: src/components/ShowFullContractTermPopup.tsx:256 #, c-format msgid "Delivery date" -msgstr "" +msgstr "Lieferdatum" #: src/components/ShowFullContractTermPopup.tsx:271 #, c-format @@ -405,7 +405,7 @@ msgstr "" #: src/components/ShowFullContractTermPopup.tsx:354 #, c-format msgid "Fulfillment URL" -msgstr "" +msgstr "Adresse digitaler Dienstleistung (Fulfillment-URL)" #: src/components/ShowFullContractTermPopup.tsx:360 #, c-format @@ -1061,7 +1061,7 @@ msgstr "Konnte die Umsatzanzeige nicht laden" #: src/wallet/ExchangeSelection/views.tsx:131 #, c-format msgid "Close" -msgstr "" +msgstr "Schließen" #: src/wallet/ExchangeSelection/views.tsx:160 #, fuzzy, c-format diff --git a/packages/taler-wallet-webextension/src/i18n/ru.po b/packages/taler-wallet-webextension/src/i18n/ru.po @@ -0,0 +1,1977 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: languages@taler.net\n" +"POT-Creation-Date: 2016-11-23 00:00+0100\n" +"PO-Revision-Date: 2024-05-10 00:13+0000\n" +"Last-Translator: Lily Ponomareva <lilyponomareva2017@gmail.com>\n" +"Language-Team: Russian <https://weblate.taler.net/projects/gnu-taler/" +"webextensions/ru/>\n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"X-Generator: Weblate 5.4.3\n" + +#: src/NavigationBar.tsx:139 +#, c-format +msgid "Balance" +msgstr "Баланс" + +#: src/NavigationBar.tsx:142 +#, c-format +msgid "Backup" +msgstr "Резервная копия" + +#: src/NavigationBar.tsx:147 +#, c-format +msgid "QR Reader and Taler URI" +msgstr "Считыватель QR-кодов и URI Taler" + +#: src/NavigationBar.tsx:154 +#, c-format +msgid "Settings" +msgstr "Настройки" + +#: src/NavigationBar.tsx:184 +#, c-format +msgid "Dev" +msgstr "Dev" + +#: src/mui/Typography.tsx:122 +#, c-format +msgid "%1$s" +msgstr "%1$s" + +#: src/components/PendingTransactions.tsx:74 +#, c-format +msgid "PENDING OPERATIONS" +msgstr "ОЖИДАЮЩИЕ ОПЕРАЦИИ" + +#: src/components/Loading.tsx:36 +#, c-format +msgid "Loading" +msgstr "Загружаются" + +#: src/wallet/BackupPage.tsx:123 +#, c-format +msgid "Could not load backup providers" +msgstr "Не удалось загрузить поставщиков резервного копирования" + +#: src/wallet/BackupPage.tsx:202 +#, c-format +msgid "No backup providers configured" +msgstr "Поставщики резервного копирования не настроены" + +#: src/wallet/BackupPage.tsx:205 +#, c-format +msgid "Add provider" +msgstr "Добавить сервис" + +#: src/wallet/BackupPage.tsx:219 +#, c-format +msgid "Sync all backups" +msgstr "Синхронизация всех резервных копий" + +#: src/wallet/BackupPage.tsx:221 +#, c-format +msgid "Sync now" +msgstr "Синхронизировать сейчас" + +#: src/wallet/BackupPage.tsx:264 +#, c-format +msgid "Last synced" +msgstr "Последняя синхронизация" + +#: src/wallet/BackupPage.tsx:269 +#, c-format +msgid "Not synced" +msgstr "Не синхронизировано" + +#: src/wallet/BackupPage.tsx:289 +#, c-format +msgid "Expires in" +msgstr "Срок действия истекает в" + +#: src/wallet/ProviderDetailPage.tsx:60 +#, c-format +msgid "There was an error loading the provider detail for " %1$s"" +msgstr "" +"Произошла ошибка при загрузке сведений о поставщике для " %1$s"" + +#: src/wallet/ProviderDetailPage.tsx:108 +#, c-format +msgid "There is not known provider with url "%1$s"." +msgstr "Нет провайдера с url "%1$s"." + +#: src/wallet/ProviderDetailPage.tsx:115 +#, c-format +msgid "See providers" +msgstr "Посмотреть провайдеров" + +#: src/wallet/ProviderDetailPage.tsx:143 +#, c-format +msgid "Last backup" +msgstr "Последняя резервная копия" + +#: src/wallet/ProviderDetailPage.tsx:148 +#, c-format +msgid "Back up" +msgstr "Создать резервную копию" + +#: src/wallet/ProviderDetailPage.tsx:154 +#, c-format +msgid "Provider fee" +msgstr "Комиссия провайдера" + +#: src/wallet/ProviderDetailPage.tsx:157 +#, c-format +msgid "per year" +msgstr "в год" + +#: src/wallet/ProviderDetailPage.tsx:163 +#, c-format +msgid "Extend" +msgstr "Расширить" + +#: src/wallet/ProviderDetailPage.tsx:169 +#, c-format +msgid "" +"terms has changed, extending the service will imply accepting the new terms of " +"service" +msgstr "" +"изменились условия, продление сервиса будет означать принятие новых условий " +"предоставления услуг" + +#: src/wallet/ProviderDetailPage.tsx:179 +#, c-format +msgid "old" +msgstr "старый" + +#: src/wallet/ProviderDetailPage.tsx:183 +#, c-format +msgid "new" +msgstr "новый" + +#: src/wallet/ProviderDetailPage.tsx:190 +#, c-format +msgid "fee" +msgstr "комиссия" + +#: src/wallet/ProviderDetailPage.tsx:198 +#, c-format +msgid "storage" +msgstr "хранение" + +#: src/wallet/ProviderDetailPage.tsx:215 +#, c-format +msgid "Remove provider" +msgstr "Удалить провадер" + +#: src/wallet/ProviderDetailPage.tsx:228 +#, c-format +msgid "This provider has reported an error" +msgstr "Этот провайдер сообщил об ошибке" + +#: src/wallet/ProviderDetailPage.tsx:242 +#, c-format +msgid "There is conflict with another backup from %1$s" +msgstr "Возник конфликт с другой резервной копией из %1$s" + +#: src/wallet/ProviderDetailPage.tsx:253 +#, c-format +msgid "Backup is not readable" +msgstr "Резервная копия не читается" + +#: src/wallet/ProviderDetailPage.tsx:261 +#, c-format +msgid "Unknown backup problem: %1$s" +msgstr "Неизвестная проблема резервного копирования: %1$s" + +#: src/wallet/ProviderDetailPage.tsx:283 +#, c-format +msgid "service paid" +msgstr "Услуга платная" + +#: src/wallet/ProviderDetailPage.tsx:290 +#, c-format +msgid "Backup valid until" +msgstr "Резервная копия действительна до" + +#: src/wallet/AddNewActionView.tsx:57 +#, c-format +msgid "Cancel" +msgstr "Отмена" + +#: src/wallet/AddNewActionView.tsx:68 +#, c-format +msgid "Open reserve page" +msgstr "Открыть резервную страницу" + +#: src/wallet/AddNewActionView.tsx:70 +#, c-format +msgid "Open pay page" +msgstr "Открыть страницу оплаты" + +#: src/wallet/AddNewActionView.tsx:72 +#, c-format +msgid "Open refund page" +msgstr "Открыть страницу возврата средств" + +#: src/wallet/AddNewActionView.tsx:74 +#, c-format +msgid "Open tip page" +msgstr "Открыть страницу чаевых" + +#: src/wallet/AddNewActionView.tsx:76 +#, c-format +msgid "Open withdraw page" +msgstr "Открыть страницу вывода средств" + +#: src/popup/NoBalanceHelp.tsx:43 +#, c-format +msgid "Get digital cash" +msgstr "Получите цифровую наличность" + +#: src/popup/BalancePage.tsx:138 +#, c-format +msgid "Could not load balance page" +msgstr "Не удалось загрузить страницу баланса" + +#: src/popup/BalancePage.tsx:175 +#, c-format +msgid "Add" +msgstr "Добавить" + +#: src/popup/BalancePage.tsx:179 +#, c-format +msgid "Send %1$s" +msgstr "Отправить %1$s" + +#: src/popup/TalerActionFound.tsx:44 +#, c-format +msgid "Taler Action" +msgstr "Действие Талер" + +#: src/popup/TalerActionFound.tsx:49 +#, c-format +msgid "This page has pay action." +msgstr "На этой странице есть платное действие." + +#: src/popup/TalerActionFound.tsx:63 +#, c-format +msgid "This page has a withdrawal action." +msgstr "На этой странице есть действие по выводу средств." + +#: src/popup/TalerActionFound.tsx:79 +#, c-format +msgid "This page has a tip action." +msgstr "На этой странице есть действие чаевых." + +#: src/popup/TalerActionFound.tsx:93 +#, c-format +msgid "This page has a notify reserve action." +msgstr "На этой странице есть действие уведомить о резерве." + +#: src/popup/TalerActionFound.tsx:102 +#, c-format +msgid "Notify" +msgstr "Уведомить" + +#: src/popup/TalerActionFound.tsx:109 +#, c-format +msgid "This page has a refund action." +msgstr "На этой странице есть действие по возврату средств." + +#: src/popup/TalerActionFound.tsx:123 +#, c-format +msgid "This page has a malformed taler uri." +msgstr "На этой странице неправильно сформирован Taler URI." + +#: src/popup/TalerActionFound.tsx:134 +#, c-format +msgid "Dismiss" +msgstr "Закрыть" + +#: src/popup/Application.tsx:177 +#, c-format +msgid "this popup is being closed and you are being redirected to %1$s" +msgstr "Это всплывающее окно закрывается и вы перенаправляетесь на %1$s" + +#: src/components/ShowFullContractTermPopup.tsx:158 +#, c-format +msgid "Could not load purchase proposal details" +msgstr "Не удалось загрузить сведения о предложении покупки" + +#: src/components/ShowFullContractTermPopup.tsx:183 +#, c-format +msgid "Order Id" +msgstr "Номер заказа" + +#: src/components/ShowFullContractTermPopup.tsx:189 +#, c-format +msgid "Summary" +msgstr "Вкратце" + +#: src/components/ShowFullContractTermPopup.tsx:195 +#, c-format +msgid "Amount" +msgstr "Сумма" + +#: src/components/ShowFullContractTermPopup.tsx:203 +#, c-format +msgid "Merchant name" +msgstr "Название продавца" + +#: src/components/ShowFullContractTermPopup.tsx:209 +#, c-format +msgid "Merchant jurisdiction" +msgstr "Юрисдикция продавца" + +#: src/components/ShowFullContractTermPopup.tsx:215 +#, c-format +msgid "Merchant address" +msgstr "Адрес продавца" + +#: src/components/ShowFullContractTermPopup.tsx:221 +#, c-format +msgid "Merchant logo" +msgstr "Логотип продавца" + +#: src/components/ShowFullContractTermPopup.tsx:234 +#, c-format +msgid "Merchant website" +msgstr "Сайт продавца" + +#: src/components/ShowFullContractTermPopup.tsx:240 +#, c-format +msgid "Merchant email" +msgstr "Email продавца" + +#: src/components/ShowFullContractTermPopup.tsx:246 +#, c-format +msgid "Merchant public key" +msgstr "Публичный ключ продавца" + +#: src/components/ShowFullContractTermPopup.tsx:256 +#, c-format +msgid "Delivery date" +msgstr "Дата поставки" + +#: src/components/ShowFullContractTermPopup.tsx:271 +#, c-format +msgid "Delivery location" +msgstr "Адрес доставки" + +#: src/components/ShowFullContractTermPopup.tsx:277 +#, c-format +msgid "Products" +msgstr "Продукты" + +#: src/components/ShowFullContractTermPopup.tsx:289 +#, c-format +msgid "Created at" +msgstr "Создано в" + +#: src/components/ShowFullContractTermPopup.tsx:304 +#, c-format +msgid "Refund deadline" +msgstr "Крайний срок возврата средств" + +#: src/components/ShowFullContractTermPopup.tsx:319 +#, c-format +msgid "Auto refund" +msgstr "Автоматический возврат средств" + +#: src/components/ShowFullContractTermPopup.tsx:339 +#, c-format +msgid "Pay deadline" +msgstr "Крайний срок оплаты" + +#: src/components/ShowFullContractTermPopup.tsx:354 +#, c-format +msgid "Fulfillment URL" +msgstr "URL-адрес выполнения" + +#: src/components/ShowFullContractTermPopup.tsx:360 +#, c-format +msgid "Fulfillment message" +msgstr "Сообщение о выполнении" + +#: src/components/ShowFullContractTermPopup.tsx:370 +#, c-format +msgid "Max deposit fee" +msgstr "Максимальная комиссия за депозит" + +#: src/components/ShowFullContractTermPopup.tsx:378 +#, c-format +msgid "Max fee" +msgstr "максимальная комиссия" + +#: src/components/ShowFullContractTermPopup.tsx:386 +#, c-format +msgid "Minimum age" +msgstr "Минимальный возраст" + +#: src/components/ShowFullContractTermPopup.tsx:398 +#, c-format +msgid "Wire fee amortization" +msgstr "Комиссия за банковский перевод" + +#: src/components/ShowFullContractTermPopup.tsx:404 +#, c-format +msgid "Auditors" +msgstr "Аудиторы" + +#: src/components/ShowFullContractTermPopup.tsx:419 +#, c-format +msgid "Exchanges" +msgstr "Обменники" + +#: src/components/Part.tsx:148 +#, c-format +msgid "Bank account" +msgstr "Баковский счёт" + +#: src/components/Part.tsx:160 +#, c-format +msgid "Bitcoin address" +msgstr "Биткоин адрес" + +#: src/components/Part.tsx:163 +#, c-format +msgid "IBAN" +msgstr "IBAN" + +#: src/cta/Deposit/views.tsx:38 +#, c-format +msgid "Could not load deposit status" +msgstr "Не удалось загрузить статус депозита" + +#: src/cta/Deposit/views.tsx:52 +#, c-format +msgid "Digital cash deposit" +msgstr "Депозит цифровой налички" + +#: src/cta/Deposit/views.tsx:58 +#, c-format +msgid "Cost" +msgstr "Стоимость" + +#: src/cta/Deposit/views.tsx:66 +#, c-format +msgid "Fee" +msgstr "Комиссия" + +#: src/cta/Deposit/views.tsx:73 +#, c-format +msgid "To be received" +msgstr "К получению" + +#: src/cta/Deposit/views.tsx:84 +#, c-format +msgid "Send %1$s" +msgstr "Отправить %1$s" + +#: src/components/BankDetailsByPaytoType.tsx:63 +#, c-format +msgid "Bitcoin transfer details" +msgstr "Подробности перевода биткоина" + +#: src/components/BankDetailsByPaytoType.tsx:66 +#, c-format +msgid "" +"The exchange need a transaction with 3 output, one output is the exchange " +"account and the other two are segwit fake address for metadata with an minimum " +"amount." +msgstr "" +"Обменнику нужна транзакция с 3 выходами, один выход - это счёт обменника, а " +"два других - это сегвит фейк адрес для метаданных с минимальной суммой." + +#: src/components/BankDetailsByPaytoType.tsx:74 +#, c-format +msgid "" +"In bitcoincore wallet use 'Add Recipient' button to add two additional " +"recipient and copy addresses and amounts" +msgstr "" + +#: src/components/BankDetailsByPaytoType.tsx:98 +#, c-format +msgid "Make sure the amount show %1$s BTC, else you have to change the base unit to BTC" +msgstr "" +"Убедитесь что сумма показывает %1$s BTC, в противном случае вам придётся " +"изменить базовую единицу на BTC" + +#: src/components/BankDetailsByPaytoType.tsx:110 +#, c-format +msgid "Account" +msgstr "Счёт" + +#: src/components/BankDetailsByPaytoType.tsx:116 +#, c-format +msgid "Bank host" +msgstr "Хост банка" + +#: src/components/BankDetailsByPaytoType.tsx:139 +#, c-format +msgid "Bank transfer details" +msgstr "Подробности банковского перевода" + +#: src/components/BankDetailsByPaytoType.tsx:148 +#, c-format +msgid "Subject" +msgstr "Причина" + +#: src/components/BankDetailsByPaytoType.tsx:154 +#, c-format +msgid "Receiver name" +msgstr "Имя получателя" + +#: src/wallet/Transaction.tsx:98 +#, c-format +msgid "Could not load the transaction information" +msgstr "Не удалось загрузить информацию о транзакции" + +#: src/wallet/Transaction.tsx:191 +#, c-format +msgid "There was an error trying to complete the transaction" +msgstr "При попытке завершить транзакцию произошла ошибка" + +#: src/wallet/Transaction.tsx:200 +#, c-format +msgid "This transaction is not completed" +msgstr "Эта транзакция не завершена" + +#: src/wallet/Transaction.tsx:209 +#, c-format +msgid "Send" +msgstr "Отправить" + +#: src/wallet/Transaction.tsx:216 +#, c-format +msgid "Retry" +msgstr "Повторить попытку" + +#: src/wallet/Transaction.tsx:224 +#, c-format +msgid "Forget" +msgstr "Забыть" + +#: src/wallet/Transaction.tsx:241 +#, c-format +msgid "Caution!" +msgstr "Внимание!" + +#: src/wallet/Transaction.tsx:244 +#, c-format +msgid "" +"If you have already wired money to the exchange you will loose the chance to get " +"the coins form it." +msgstr "" +"Если вы уже перевели деньги на обменник вы потеряете шанс получить монеты с " +"нее." + +#: src/wallet/Transaction.tsx:259 +#, c-format +msgid "Confirm" +msgstr "Подтвердить" + +#: src/wallet/Transaction.tsx:267 +#, c-format +msgid "Withdrawal" +msgstr "Вывод" + +#: src/wallet/Transaction.tsx:286 +#, c-format +msgid "" +"Make sure to use the correct subject, otherwise the money will not arrive in " +"this wallet." +msgstr "" +"Убедитесь что вы указали правильное назначение, иначе деньги не поступят на " +"этот кошелек." + +#: src/wallet/Transaction.tsx:298 +#, c-format +msgid "" +"The bank did not yet confirmed the wire transfer. Go to the %1$s %2$s and check " +"there is no pending step." +msgstr "" +"Банк пока не подтвердил перевод. Перейдите к %1$s %2$s и проверьте нет ли " +"ожидающих шагов." + +#: src/wallet/Transaction.tsx:316 +#, c-format +msgid "Bank has confirmed the wire transfer. Waiting for the exchange to send the coins" +msgstr "Банк подтвердил перевод. Ожидание пока обменик отправит монеты" + +#: src/wallet/Transaction.tsx:325 +#, c-format +msgid "Details" +msgstr "Подробности" + +#: src/wallet/Transaction.tsx:360 +#, c-format +msgid "Payment" +msgstr "Платёж" + +#: src/wallet/Transaction.tsx:378 +#, c-format +msgid "Refunds" +msgstr "Возвраты" + +#: src/wallet/Transaction.tsx:385 +#, c-format +msgid "%1$s %2$s on %3$s" +msgstr "%1$s %2$s на %3$s" + +#: src/wallet/Transaction.tsx:415 +#, c-format +msgid "Merchant created a refund for this order but was not automatically picked up." +msgstr "" +"Продавец создал возврат средств за этот заказ, но не был автоматически " +"забран." + +#: src/wallet/Transaction.tsx:420 +#, c-format +msgid "Offer" +msgstr "Предложение" + +#: src/wallet/Transaction.tsx:431 +#, c-format +msgid "Accept" +msgstr "Принять" + +#: src/wallet/Transaction.tsx:438 +#, c-format +msgid "Merchant" +msgstr "Продавец" + +#: src/wallet/Transaction.tsx:443 +#, c-format +msgid "Invoice ID" +msgstr "№ счёта-фактуры" + +#: src/wallet/Transaction.tsx:470 +#, c-format +msgid "Deposit" +msgstr "Депозит" + +#: src/wallet/Transaction.tsx:496 +#, c-format +msgid "Refresh" +msgstr "Обновить" + +#: src/wallet/Transaction.tsx:517 +#, c-format +msgid "Tip" +msgstr "Чаевые" + +#: src/wallet/Transaction.tsx:542 +#, c-format +msgid "Refund" +msgstr "Возврат" + +#: src/wallet/Transaction.tsx:555 +#, c-format +msgid "Original order ID" +msgstr "№ исходного заказа" + +#: src/wallet/Transaction.tsx:568 +#, c-format +msgid "Purchase summary" +msgstr "Сводка о покупке" + +#: src/wallet/Transaction.tsx:593 +#, c-format +msgid "copy" +msgstr "копировать" + +#: src/wallet/Transaction.tsx:596 +#, c-format +msgid "hide qr" +msgstr "спрятать qr" + +#: src/wallet/Transaction.tsx:608 +#, c-format +msgid "show qr" +msgstr "показать qr" + +#: src/wallet/Transaction.tsx:620 +#, c-format +msgid "Credit" +msgstr "Кредит" + +#: src/wallet/Transaction.tsx:624 +#, c-format +msgid "Invoice" +msgstr "Счёт-фактура" + +#: src/wallet/Transaction.tsx:635 +#, c-format +msgid "Exchange" +msgstr "Обменник" + +#: src/wallet/Transaction.tsx:641 +#, c-format +msgid "URI" +msgstr "URI" + +#: src/wallet/Transaction.tsx:667 +#, c-format +msgid "Debit" +msgstr "Дебит" + +#: src/wallet/Transaction.tsx:710 +#, c-format +msgid "Transfer" +msgstr "Перевести" + +#: src/wallet/Transaction.tsx:844 +#, c-format +msgid "Country" +msgstr "Страна" + +#: src/wallet/Transaction.tsx:852 +#, c-format +msgid "Address lines" +msgstr "Строки адреса" + +#: src/wallet/Transaction.tsx:860 +#, c-format +msgid "Building number" +msgstr "Номер дома" + +#: src/wallet/Transaction.tsx:868 +#, c-format +msgid "Building name" +msgstr "Название дома" + +#: src/wallet/Transaction.tsx:876 +#, c-format +msgid "Street" +msgstr "Улица" + +#: src/wallet/Transaction.tsx:884 +#, c-format +msgid "Post code" +msgstr "Почтовый индекс" + +#: src/wallet/Transaction.tsx:892 +#, c-format +msgid "Town location" +msgstr "Область города" + +#: src/wallet/Transaction.tsx:900 +#, c-format +msgid "Town" +msgstr "Город" + +#: src/wallet/Transaction.tsx:908 +#, c-format +msgid "District" +msgstr "Район" + +#: src/wallet/Transaction.tsx:916 +#, c-format +msgid "Country subdivision" +msgstr "Регион страны" + +#: src/wallet/Transaction.tsx:935 +#, c-format +msgid "Date" +msgstr "Дата" + +#: src/wallet/Transaction.tsx:990 +#, c-format +msgid "Transaction fees" +msgstr "Комиссия транзакции" + +#: src/wallet/Transaction.tsx:1004 +#, c-format +msgid "Total" +msgstr "Всего" + +#: src/wallet/Transaction.tsx:1074 +#, c-format +msgid "Withdraw" +msgstr "Снять средства" + +#: src/wallet/Transaction.tsx:1146 +#, c-format +msgid "Price" +msgstr "Цена" + +#: src/wallet/Transaction.tsx:1156 +#, c-format +msgid "Refunded" +msgstr "Возвращено на счёт" + +#: src/wallet/Transaction.tsx:1220 +#, c-format +msgid "Delivery" +msgstr "Поставка" + +#: src/wallet/Transaction.tsx:1335 +#, c-format +msgid "Total transfer" +msgstr "Итого перевод" + +#: src/cta/Payment/views.tsx:57 +#, c-format +msgid "Could not load pay status" +msgstr "Не удалось загрузить статус оплаты" + +#: src/cta/Payment/views.tsx:87 +#, c-format +msgid "Digital cash payment" +msgstr "Оплата цифровой наличкой" + +#: src/cta/Payment/views.tsx:119 +#, c-format +msgid "Purchase" +msgstr "Покупка" + +#: src/cta/Payment/views.tsx:149 +#, c-format +msgid "Receipt" +msgstr "Чек" + +#: src/cta/Payment/views.tsx:156 +#, c-format +msgid "Valid until" +msgstr "Действительно до" + +#: src/cta/Payment/views.tsx:191 +#, c-format +msgid "List of products" +msgstr "Список продуктов" + +#: src/cta/Payment/views.tsx:242 +#, c-format +msgid "free" +msgstr "комиссия" + +#: src/cta/Payment/views.tsx:263 +#, c-format +msgid "Already paid, you are going to be redirected to %1$s" +msgstr "Уже оплачено, вы будете перенаправлены на %1$s" + +#: src/cta/Payment/views.tsx:274 +#, c-format +msgid "Already paid" +msgstr "Уже оплачено" + +#: src/cta/Payment/views.tsx:280 +#, c-format +msgid "Already claimed" +msgstr "Уже заявлено" + +#: src/cta/Payment/views.tsx:296 +#, c-format +msgid "Pay with a mobile phone" +msgstr "Оплата с помощью мобильного телефона" + +#: src/cta/Payment/views.tsx:298 +#, c-format +msgid "Hide QR" +msgstr "Скрыть QR" + +#: src/cta/Payment/views.tsx:305 +#, c-format +msgid "Scan the QR code or %1$s" +msgstr "Отсканируйте QR код или %1$s" + +#: src/cta/Payment/views.tsx:346 +#, c-format +msgid "Pay %1$s" +msgstr "Заплатить %1$s" + +#: src/cta/Payment/views.tsx:360 +#, c-format +msgid "You have no balance for this currency. Withdraw digital cash first." +msgstr "У вас нет баланса в этой валюте. Сначала снимите цифровые деньги." + +#: src/cta/Payment/views.tsx:364 +#, c-format +msgid "" +"Could not find enough coins to pay. Even if you have enough %1$s some " +"restriction may apply." +msgstr "" +"Не удалось найти достаточно монет для оплаты. Даже если у вас достаточно %1$" +"s, могут применяться некоторые ограничения." + +#: src/cta/Payment/views.tsx:366 +#, c-format +msgid "Your current balance is not enough." +msgstr "Недостаточно средств на балансе." + +#: src/cta/Payment/views.tsx:395 +#, c-format +msgid "Merchant message" +msgstr "Сообщение продавца" + +#: src/cta/Refund/views.tsx:34 +#, c-format +msgid "Could not load refund status" +msgstr "Не удалось загрузить статус возврата" + +#: src/cta/Refund/views.tsx:48 +#, c-format +msgid "Digital cash refund" +msgstr "Возврат цифровой налички" + +#: src/cta/Refund/views.tsx:52 +#, c-format +msgid "You've ignored the tip." +msgstr "Вы проигнорировали чаевые." + +#: src/cta/Refund/views.tsx:70 +#, c-format +msgid "The refund is in progress." +msgstr "Возврат средств в выполняется." + +#: src/cta/Refund/views.tsx:76 +#, c-format +msgid "Total to refund" +msgstr "Всего к возврату" + +#: src/cta/Refund/views.tsx:106 +#, c-format +msgid "The merchant "%1$s" is offering you a refund." +msgstr "Продавец «%1$s» предлагает вам возврат средств." + +#: src/cta/Refund/views.tsx:115 +#, c-format +msgid "Order amount" +msgstr "Сумма заказа" + +#: src/cta/Refund/views.tsx:122 +#, c-format +msgid "Already refunded" +msgstr "Уже возвращено" + +#: src/cta/Refund/views.tsx:129 +#, c-format +msgid "Refund offered" +msgstr "Предложен возврат средств" + +#: src/cta/Refund/views.tsx:145 +#, c-format +msgid "Accept %1$s" +msgstr "Принять %1$s" + +#: src/cta/Tip/views.tsx:32 +#, c-format +msgid "Could not load tip status" +msgstr "Не удалось загрузить статус чаевых" + +#: src/cta/Tip/views.tsx:45 +#, c-format +msgid "Digital cash tip" +msgstr "Чаевые цифровой налички" + +#: src/cta/Tip/views.tsx:66 +#, c-format +msgid "The merchant is offering you a tip" +msgstr "Продавец предлагает вам чаевые" + +#: src/cta/Tip/views.tsx:74 +#, c-format +msgid "Merchant URL" +msgstr "URL-адрес продавца" + +#: src/cta/Tip/views.tsx:90 +#, c-format +msgid "Receive %1$s" +msgstr "Получить %1$s" + +#: src/cta/Tip/views.tsx:114 +#, c-format +msgid "Tip from %1$s accepted. Check your transactions list for more details." +msgstr "Чаевые от %1$s приняты. Проверьте список транзакций для подробностей." + +#: src/components/SelectList.tsx:66 +#, c-format +msgid "Select one option" +msgstr "Выберете одну опцию" + +#: src/components/TermsOfService/views.tsx:39 +#, c-format +msgid "Could not load" +msgstr "Невозможно загрузить" + +#: src/components/TermsOfService/views.tsx:73 +#, c-format +msgid "Show terms of service" +msgstr "Показать Условия использования" + +#: src/components/TermsOfService/views.tsx:81 +#, c-format +msgid "I accept the exchange terms of service" +msgstr "Я принимаю эти Условия использования" + +#: src/components/TermsOfService/views.tsx:107 +#, c-format +msgid "Exchange doesn't have terms of service" +msgstr "Обменник не имеет условий использования" + +#: src/components/TermsOfService/views.tsx:135 +#, c-format +msgid "Review exchange terms of service" +msgstr "Ознакомиться с Условиями использования" + +#: src/components/TermsOfService/views.tsx:146 +#, c-format +msgid "Review new version of terms of service" +msgstr "Ознакомиться с новой версией Условий использования" + +#: src/components/TermsOfService/views.tsx:170 +#, c-format +msgid "The exchange reply with a empty terms of service" +msgstr "Биржа ответитила с пустыми условиями использования" + +#: src/components/TermsOfService/views.tsx:193 +#, c-format +msgid "Download Terms of Service" +msgstr "Скачать Условия использования" + +#: src/components/TermsOfService/views.tsx:204 +#, c-format +msgid "Hide terms of service" +msgstr "Скрыть Условия использования" + +#: src/wallet/ExchangeSelection/views.tsx:117 +#, c-format +msgid "Could not load exchange fees" +msgstr "Не удалось загрузить комиссию за обмен" + +#: src/wallet/ExchangeSelection/views.tsx:131 +#, c-format +msgid "Close" +msgstr "Закрыть" + +#: src/wallet/ExchangeSelection/views.tsx:160 +#, c-format +msgid "could not find any exchange" +msgstr "Не удалось найти ни одного обменника" + +#: src/wallet/ExchangeSelection/views.tsx:166 +#, c-format +msgid "could not find any exchange for the currency %1$s" +msgstr "Не удалось найти ни одного обменника для валюты %1$s" + +#: src/wallet/ExchangeSelection/views.tsx:186 +#, c-format +msgid "Service fee description" +msgstr "Описание комиссии за услугу" + +#: src/wallet/ExchangeSelection/views.tsx:201 +#, c-format +msgid "Select %1$s exchange" +msgstr "Выберите %1$s обменник" + +#: src/wallet/ExchangeSelection/views.tsx:215 +#, c-format +msgid "Reset" +msgstr "Сбросить" + +#: src/wallet/ExchangeSelection/views.tsx:218 +#, c-format +msgid "Use this exchange" +msgstr "Использовать этот обменник" + +#: src/wallet/ExchangeSelection/views.tsx:230 +#, c-format +msgid "Doesn't have auditors" +msgstr "Не имеет аудиторов" + +#: src/wallet/ExchangeSelection/views.tsx:241 +#, c-format +msgid "currency" +msgstr "валюта" + +#: src/wallet/ExchangeSelection/views.tsx:249 +#, c-format +msgid "Operations" +msgstr "Операции" + +#: src/wallet/ExchangeSelection/views.tsx:252 +#, c-format +msgid "Deposits" +msgstr "Депозиты" + +#: src/wallet/ExchangeSelection/views.tsx:259 +#, c-format +msgid "Denomination" +msgstr "Деноминация" + +#: src/wallet/ExchangeSelection/views.tsx:265 +#, c-format +msgid "Until" +msgstr "до" + +#: src/wallet/ExchangeSelection/views.tsx:274 +#, c-format +msgid "Withdrawals" +msgstr "Выводы средств" + +#: src/wallet/ExchangeSelection/views.tsx:423 +#, c-format +msgid "Currency" +msgstr "Валюта" + +#: src/wallet/ExchangeSelection/views.tsx:433 +#, c-format +msgid "Coin operations" +msgstr "Операции моент" + +#: src/wallet/ExchangeSelection/views.tsx:436 +#, c-format +msgid "" +"Every operation in this section may be different by denomination value and is " +"valid for a period of time. The exchange will charge the indicated amount every " +"time a coin is used in such operation." +msgstr "" +"Каждая операция в этом разделе может отличаться номиналом и действительна в " +"течение определенного периода времени. Биржа будет взимать указанную сумму " +"каждый раз, когда монета используется в такой операции." + +#: src/wallet/ExchangeSelection/views.tsx:545 +#, c-format +msgid "Transfer operations" +msgstr "Операции переводов" + +#: src/wallet/ExchangeSelection/views.tsx:548 +#, c-format +msgid "" +"Every operation in this section may be different by transfer type and is valid " +"for a period of time. The exchange will charge the indicated amount every time a " +"transfer is made." +msgstr "" +"Каждая операция в этом разделе может отличаться в зависимости от типа " +"перевода и действительна в течение определенного периода времени. Обменник " +"будет взимать указанную сумму каждый раз при совершении перевода." + +#: src/wallet/ExchangeSelection/views.tsx:563 +#, c-format +msgid "Operation" +msgstr "Операция" + +#: src/wallet/ExchangeSelection/views.tsx:583 +#, c-format +msgid "Wallet operations" +msgstr "Операции кошелька" + +#: src/wallet/ExchangeSelection/views.tsx:597 +#, c-format +msgid "Feature" +msgstr "Возможность" + +#: src/cta/Withdraw/views.tsx:47 +#, c-format +msgid "Could not get the info from the URI" +msgstr "Не удалось получить информацию из URI" + +#: src/cta/Withdraw/views.tsx:60 +#, c-format +msgid "Could not get info of withdrawal" +msgstr "Не удалось получить информацию о выводе средств" + +#: src/cta/Withdraw/views.tsx:74 +#, c-format +msgid "Digital cash withdrawal" +msgstr "Вывод цифровых наличных" + +#: src/cta/Withdraw/views.tsx:79 +#, c-format +msgid "Could not finish the withdrawal operation" +msgstr "" + +#: src/cta/Withdraw/views.tsx:127 +#, c-format +msgid "Age restriction" +msgstr "Ограничения возраста" + +#: src/cta/Withdraw/views.tsx:145 +#, c-format +msgid "Withdraw %1$s" +msgstr "Вывести %1$s" + +#: src/cta/Withdraw/views.tsx:179 +#, c-format +msgid "Withdraw to a mobile phone" +msgstr "Вывести на мобильный телефон" + +#: src/cta/InvoiceCreate/views.tsx:65 +#, c-format +msgid "Digital invoice" +msgstr "Цифровой счёт-фактура" + +#: src/cta/InvoiceCreate/views.tsx:69 +#, c-format +msgid "Could not finish the invoice creation" +msgstr "" + +#: src/cta/InvoiceCreate/views.tsx:130 +#, c-format +msgid "Create" +msgstr "Создать" + +#: src/cta/InvoicePay/views.tsx:63 +#, c-format +msgid "Could not finish the payment operation" +msgstr "" + +#: src/cta/TransferCreate/views.tsx:55 +#, c-format +msgid "Digital cash transfer" +msgstr "" + +#: src/cta/TransferCreate/views.tsx:59 +#, c-format +msgid "Could not finish the transfer creation" +msgstr "" + +#: src/cta/TransferPickup/views.tsx:57 +#, c-format +msgid "Could not finish the pickup operation" +msgstr "" + +#: src/wallet/CreateManualWithdraw.tsx:149 +#, c-format +msgid "Manual Withdrawal for %1$s" +msgstr "" + +#: src/wallet/CreateManualWithdraw.tsx:154 +#, c-format +msgid "" +"Choose a exchange from where the coins will be withdrawn. The exchange will send " +"the coins to this wallet after receiving a wire transfer with the correct " +"subject." +msgstr "" + +#: src/wallet/CreateManualWithdraw.tsx:162 +#, c-format +msgid "No exchange found for %1$s" +msgstr "" + +#: src/wallet/CreateManualWithdraw.tsx:170 +#, c-format +msgid "Add Exchange" +msgstr "Добавить Обменник" + +#: src/wallet/CreateManualWithdraw.tsx:192 +#, c-format +msgid "No exchange configured" +msgstr "" + +#: src/wallet/CreateManualWithdraw.tsx:210 +#, c-format +msgid "Can't create the reserve" +msgstr "" + +#: src/wallet/CreateManualWithdraw.tsx:277 +#, c-format +msgid "Start withdrawal" +msgstr "Начать вывод" + +#: src/wallet/DepositPage/views.tsx:38 +#, c-format +msgid "Could not load deposit balance" +msgstr "" + +#: src/wallet/DepositPage/views.tsx:51 +#, c-format +msgid "A currency or an amount should be indicated" +msgstr "" + +#: src/wallet/DepositPage/views.tsx:67 +#, c-format +msgid "There is no enough balance to make a deposit for currency %1$s" +msgstr "" + +#: src/wallet/DepositPage/views.tsx:117 +#, c-format +msgid "Send %1$s to your account" +msgstr "" + +#: src/wallet/DepositPage/views.tsx:121 +#, c-format +msgid "There is no account to make a deposit for currency %1$s" +msgstr "" + +#: src/wallet/DepositPage/views.tsx:127 +#, c-format +msgid "Add account" +msgstr "Добавить Счёт" + +#: src/wallet/DepositPage/views.tsx:151 +#, c-format +msgid "Select account" +msgstr "Выберете счёт" + +#: src/wallet/DepositPage/views.tsx:163 +#, c-format +msgid "Add another account" +msgstr "Добавить другой счёт" + +#: src/wallet/DepositPage/views.tsx:191 +#, c-format +msgid "Deposit fee" +msgstr "Комиссия депозита" + +#: src/wallet/DepositPage/views.tsx:205 +#, c-format +msgid "Total deposit" +msgstr "Всего к депозиту" + +#: src/wallet/DepositPage/views.tsx:233 +#, c-format +msgid "Deposit %1$s %2$s" +msgstr "" + +#: src/wallet/AddAccount/views.tsx:56 +#, c-format +msgid "Add bank account for %1$s" +msgstr "" + +#: src/wallet/AddAccount/views.tsx:59 +#, c-format +msgid "Enter the URL of an exchange you trust." +msgstr "" + +#: src/wallet/AddAccount/views.tsx:66 +#, c-format +msgid "Unable add this account" +msgstr "" + +#: src/wallet/AddAccount/views.tsx:73 +#, c-format +msgid "Select account type" +msgstr "Выберете тип счёта" + +#: src/wallet/ExchangeAddConfirm.tsx:42 +#, c-format +msgid "Review terms of service" +msgstr "" + +#: src/wallet/ExchangeAddConfirm.tsx:45 +#, c-format +msgid "Exchange URL" +msgstr "URL обменника" + +#: src/wallet/ExchangeAddConfirm.tsx:70 +#, c-format +msgid "Add exchange" +msgstr "Добавить Обменник" + +#: src/wallet/ExchangeSetUrl.tsx:112 +#, c-format +msgid "Add new exchange" +msgstr "Добавить новый Обменник" + +#: src/wallet/ExchangeSetUrl.tsx:116 +#, c-format +msgid "Add exchange for %1$s" +msgstr "" + +#: src/wallet/ExchangeSetUrl.tsx:128 +#, c-format +msgid "An exchange has been found! Review the information and click next" +msgstr "" + +#: src/wallet/ExchangeSetUrl.tsx:135 +#, c-format +msgid "This exchange doesn't match the expected currency %1$s" +msgstr "" + +#: src/wallet/ExchangeSetUrl.tsx:143 +#, c-format +msgid "Unable to verify this exchange" +msgstr "" + +#: src/wallet/ExchangeSetUrl.tsx:151 +#, c-format +msgid "Unable to add this exchange" +msgstr "" + +#: src/wallet/ExchangeSetUrl.tsx:167 +#, c-format +msgid "loading" +msgstr "загрузка" + +#: src/wallet/ExchangeSetUrl.tsx:174 +#, c-format +msgid "Version" +msgstr "Версия" + +#: src/wallet/ExchangeSetUrl.tsx:206 +#, c-format +msgid "Next" +msgstr "Далее" + +#: src/components/TransactionItem.tsx:201 +#, c-format +msgid "Waiting for confirmation" +msgstr "Ожидание подтверждения" + +#: src/components/TransactionItem.tsx:266 +#, c-format +msgid "PENDING" +msgstr "ОЖИДАЕТ" + +#: src/wallet/History.tsx:75 +#, c-format +msgid "Could not load the list of transactions" +msgstr "" + +#: src/wallet/History.tsx:233 +#, c-format +msgid "Your transaction history is empty for this currency." +msgstr "" + +#: src/wallet/ProviderAddPage.tsx:127 +#, c-format +msgid "Add backup provider" +msgstr "Добавить провайдера резервной копии" + +#: src/wallet/ProviderAddPage.tsx:131 +#, c-format +msgid "Could not get provider information" +msgstr "" + +#: src/wallet/ProviderAddPage.tsx:140 +#, c-format +msgid "Backup providers may charge for their service" +msgstr "" + +#: src/wallet/ProviderAddPage.tsx:147 +#, c-format +msgid "URL" +msgstr "URL" + +#: src/wallet/ProviderAddPage.tsx:158 +#, c-format +msgid "Name" +msgstr "Название" + +#: src/wallet/ProviderAddPage.tsx:212 +#, c-format +msgid "Provider URL" +msgstr "URL провайдера" + +#: src/wallet/ProviderAddPage.tsx:218 +#, c-format +msgid "Please review and accept this provider's terms of service" +msgstr "" + +#: src/wallet/ProviderAddPage.tsx:223 +#, c-format +msgid "Pricing" +msgstr "" + +#: src/wallet/ProviderAddPage.tsx:226 +#, c-format +msgid "free of charge" +msgstr "комиссия за пополнение" + +#: src/wallet/ProviderAddPage.tsx:228 +#, c-format +msgid "%1$s per year of service" +msgstr "" + +#: src/wallet/ProviderAddPage.tsx:235 +#, c-format +msgid "Storage" +msgstr "Хранилище" + +#: src/wallet/ProviderAddPage.tsx:238 +#, c-format +msgid "%1$s megabytes of storage per year of service" +msgstr "" + +#: src/wallet/ProviderAddPage.tsx:244 +#, c-format +msgid "Accept terms of service" +msgstr "" + +#: src/wallet/ReserveCreated.tsx:44 +#, c-format +msgid "Could not parse the payto URI" +msgstr "" + +#: src/wallet/ReserveCreated.tsx:45 +#, c-format +msgid "Please check the uri" +msgstr "" + +#: src/wallet/ReserveCreated.tsx:75 +#, c-format +msgid "Exchange is ready for withdrawal" +msgstr "" + +#: src/wallet/ReserveCreated.tsx:78 +#, c-format +msgid "To complete the process you need to wire%1$s %2$s to the exchange bank account" +msgstr "" + +#: src/wallet/ReserveCreated.tsx:87 +#, c-format +msgid "" +"Alternative, you can also scan this QR code or open %1$s if you have a banking " +"app installed that supports RFC 8905" +msgstr "" + +#: src/wallet/ReserveCreated.tsx:98 +#, c-format +msgid "Cancel withdrawal" +msgstr "" + +#: src/wallet/Settings.tsx:115 +#, c-format +msgid "Could not toggle auto-open" +msgstr "" + +#: src/wallet/Settings.tsx:121 +#, c-format +msgid "Could not toggle clipboard" +msgstr "" + +#: src/wallet/Settings.tsx:126 +#, c-format +msgid "Navigator" +msgstr "Навигатор" + +#: src/wallet/Settings.tsx:129 +#, c-format +msgid "Automatically open wallet based on page content" +msgstr "" + +#: src/wallet/Settings.tsx:135 +#, c-format +msgid "" +"Enabling this option below will make using the wallet faster, but requires more " +"permissions from your browser." +msgstr "" + +#: src/wallet/Settings.tsx:145 +#, c-format +msgid "Automatically check clipboard for Taler URI" +msgstr "" + +#: src/wallet/Settings.tsx:162 +#, c-format +msgid "Trust" +msgstr "Доверять" + +#: src/wallet/Settings.tsx:166 +#, c-format +msgid "No exchange yet" +msgstr "" + +#: src/wallet/Settings.tsx:180 +#, c-format +msgid "Term of Service" +msgstr "Условия использования" + +#: src/wallet/Settings.tsx:191 +#, c-format +msgid "ok" +msgstr "ok" + +#: src/wallet/Settings.tsx:197 +#, c-format +msgid "changed" +msgstr "изменено" + +#: src/wallet/Settings.tsx:204 +#, c-format +msgid "not accepted" +msgstr "не принято" + +#: src/wallet/Settings.tsx:210 +#, c-format +msgid "unknown (exchange status should be updated)" +msgstr "" + +#: src/wallet/Settings.tsx:236 +#, c-format +msgid "Add an exchange" +msgstr "" + +#: src/wallet/Settings.tsx:241 +#, c-format +msgid "Troubleshooting" +msgstr "Исправление проблем" + +#: src/wallet/Settings.tsx:244 +#, c-format +msgid "Developer mode" +msgstr "Режим разработчика" + +#: src/wallet/Settings.tsx:246 +#, c-format +msgid "More options and information useful for debugging" +msgstr "" + +#: src/wallet/Settings.tsx:257 +#, c-format +msgid "Display" +msgstr "Отбражение" + +#: src/wallet/Settings.tsx:261 +#, c-format +msgid "Current Language" +msgstr "" + +#: src/wallet/Settings.tsx:274 +#, c-format +msgid "Wallet Core" +msgstr "" + +#: src/wallet/Settings.tsx:284 +#, c-format +msgid "Web Extension" +msgstr "Расширение браузера" + +#: src/wallet/Settings.tsx:295 +#, c-format +msgid "Exchange compatibility" +msgstr "" + +#: src/wallet/Settings.tsx:299 +#, c-format +msgid "Merchant compatibility" +msgstr "" + +#: src/wallet/Settings.tsx:303 +#, c-format +msgid "Bank compatibility" +msgstr "" + +#: src/wallet/Welcome.tsx:59 +#, c-format +msgid "Browser Extension Installed!" +msgstr "" + +#: src/wallet/Welcome.tsx:63 +#, c-format +msgid "You can open the GNU Taler Wallet using the combination %1$s ." +msgstr "" + +#: src/wallet/Welcome.tsx:72 +#, c-format +msgid "" +"Also pinning the GNU Taler Wallet to your Chrome browser allows you to quick " +"access without keyboard:" +msgstr "" + +#: src/wallet/Welcome.tsx:79 +#, c-format +msgid "Click the puzzle icon" +msgstr "" + +#: src/wallet/Welcome.tsx:82 +#, c-format +msgid "Search for GNU Taler Wallet" +msgstr "" + +#: src/wallet/Welcome.tsx:85 +#, c-format +msgid "Click the pin icon" +msgstr "" + +#: src/wallet/Welcome.tsx:91 +#, c-format +msgid "Permissions" +msgstr "Разрешения" + +#: src/wallet/Welcome.tsx:100 +#, c-format +msgid "" +"(Enabling this option below will make using the wallet faster, but requires more " +"permissions from your browser.)" +msgstr "" + +#: src/wallet/Welcome.tsx:110 +#, c-format +msgid "Next Steps" +msgstr "Следующий шаг" + +#: src/wallet/Welcome.tsx:113 +#, c-format +msgid "Try the demo" +msgstr "Попробовать демо" + +#: src/wallet/Welcome.tsx:116 +#, c-format +msgid "Learn how to top up your wallet balance" +msgstr "Узнайте как пополнить ваш баланс на кошельке" + +#: src/components/Diagnostics.tsx:31 +#, c-format +msgid "Diagnostics timed out. Could not talk to the wallet backend." +msgstr "" + +#: src/components/Diagnostics.tsx:52 +#, c-format +msgid "Problems detected:" +msgstr "" + +#: src/components/Diagnostics.tsx:61 +#, c-format +msgid "" +"Please check in your %1$s settings that you have IndexedDB enabled (check the " +"preference name %2$s)." +msgstr "" + +#: src/components/Diagnostics.tsx:70 +#, c-format +msgid "" +"Your wallet database is outdated. Currently automatic migration is not " +"supported. Please go %1$s to reset the wallet database." +msgstr "" + +#: src/components/Diagnostics.tsx:83 +#, c-format +msgid "Running diagnostics" +msgstr "" + +#: src/wallet/DeveloperPage.tsx:163 +#, c-format +msgid "Debug tools" +msgstr "Инструменты отладки" + +#: src/wallet/DeveloperPage.tsx:170 +#, c-format +msgid "" +"Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE ALL " +"YOUR COINS?" +msgstr "" + +#: src/wallet/DeveloperPage.tsx:176 +#, c-format +msgid "reset" +msgstr "сбросить" + +#: src/wallet/DeveloperPage.tsx:183 +#, c-format +msgid "TESTING: This may delete all your coin, proceed with caution" +msgstr "" + +#: src/wallet/DeveloperPage.tsx:189 +#, c-format +msgid "run gc" +msgstr "" + +#: src/wallet/DeveloperPage.tsx:197 +#, c-format +msgid "import database" +msgstr "импортировать базу данных" + +#: src/wallet/DeveloperPage.tsx:219 +#, c-format +msgid "export database" +msgstr "экспортировать базу данных" + +#: src/wallet/DeveloperPage.tsx:225 +#, c-format +msgid "Database exported at %1$s %2$s to download" +msgstr "" + +#: src/wallet/DeveloperPage.tsx:248 +#, c-format +msgid "Coins" +msgstr "Монеты" + +#: src/wallet/DeveloperPage.tsx:282 +#, c-format +msgid "Pending operations" +msgstr "" + +#: src/wallet/DeveloperPage.tsx:328 +#, c-format +msgid "usable coins" +msgstr "" + +#: src/wallet/DeveloperPage.tsx:337 +#, c-format +msgid "id" +msgstr "" + +#: src/wallet/DeveloperPage.tsx:340 +#, c-format +msgid "denom" +msgstr "" + +#: src/wallet/DeveloperPage.tsx:343 +#, c-format +msgid "value" +msgstr "значение" + +#: src/wallet/DeveloperPage.tsx:346 +#, c-format +msgid "status" +msgstr "статус" + +#: src/wallet/DeveloperPage.tsx:349 +#, c-format +msgid "from refresh?" +msgstr "" + +#: src/wallet/DeveloperPage.tsx:352 +#, c-format +msgid "age key count" +msgstr "" + +#: src/wallet/DeveloperPage.tsx:369 +#, c-format +msgid "spent coins" +msgstr "" + +#: src/wallet/DeveloperPage.tsx:373 +#, c-format +msgid "click to show" +msgstr "кликите чтобы показать" + +#: src/wallet/QrReader.tsx:108 +#, c-format +msgid "Scan a QR code or enter taler:// URI below" +msgstr "" + +#: src/wallet/QrReader.tsx:122 +#, c-format +msgid "Open" +msgstr "Открыть" + +#: src/wallet/QrReader.tsx:128 +#, c-format +msgid "URI is not valid. Taler URI should start with `taler://`" +msgstr "" + +#: src/wallet/QrReader.tsx:133 +#, c-format +msgid "Try another" +msgstr "" + +#: src/wallet/DestinationSelection.tsx:183 +#, c-format +msgid "Could not load list of exchange" +msgstr "" + +#: src/wallet/DestinationSelection.tsx:209 +#, c-format +msgid "Choose a currency to proceed or add another exchange" +msgstr "" + +#: src/wallet/DestinationSelection.tsx:217 +#, c-format +msgid "Known currencies" +msgstr "" + +#: src/wallet/DestinationSelection.tsx:318 +#, c-format +msgid "Specify the amount and the origin" +msgstr "" + +#: src/wallet/DestinationSelection.tsx:336 +#, c-format +msgid "Change currency" +msgstr "" + +#: src/wallet/DestinationSelection.tsx:344 +#, c-format +msgid "Use previous origins:" +msgstr "" + +#: src/wallet/DestinationSelection.tsx:364 +#, c-format +msgid "Or specify the origin of the money" +msgstr "" + +#: src/wallet/DestinationSelection.tsx:372 +#, c-format +msgid "Specify the origin of the money" +msgstr "" + +#: src/wallet/DestinationSelection.tsx:380 +#, c-format +msgid "From my bank account" +msgstr "Из моего банковского счёта" + +#: src/wallet/DestinationSelection.tsx:395 +#, c-format +msgid "From another wallet" +msgstr "" + +#: src/wallet/DestinationSelection.tsx:449 +#, c-format +msgid "currency not provided" +msgstr "" + +#: src/wallet/DestinationSelection.tsx:459 +#, c-format +msgid "Specify the amount and the destination" +msgstr "" + +#: src/wallet/DestinationSelection.tsx:483 +#, c-format +msgid "Use previous destinations:" +msgstr "" + +#: src/wallet/DestinationSelection.tsx:503 +#, c-format +msgid "Or specify the destination of the money" +msgstr "" + +#: src/wallet/DestinationSelection.tsx:511 +#, c-format +msgid "Specify the destination of the money" +msgstr "" + +#: src/wallet/DestinationSelection.tsx:521 +#, c-format +msgid "To my bank account" +msgstr "На мой банковский счёт" + +#: src/wallet/DestinationSelection.tsx:534 +#, c-format +msgid "To another wallet" +msgstr "На другой кошелек" + +#: src/cta/Recovery/views.tsx:30 +#, c-format +msgid "Could not load backup recovery information" +msgstr "" + +#: src/cta/Recovery/views.tsx:47 +#, c-format +msgid "Digital wallet recovery" +msgstr "" + +#: src/cta/Recovery/views.tsx:52 +#, c-format +msgid "Import backup, show info" +msgstr "Импорт резервной копии, отображение информации" + +#: src/wallet/Application.tsx:189 +#, c-format +msgid "All done, your transaction is in progress" +msgstr "Все готово, ваша транзакция выполняется" + +#: src/components/EditableText.tsx:45 +#, c-format +msgid "Edit" +msgstr "Изменить" + +#: src/wallet/ManualWithdrawPage.tsx:102 +#, c-format +msgid "Could not load the list of known exchanges" +msgstr "Не удалось загрузить список известных обменников" diff --git a/packages/taler-wallet-webextension/src/mui/Button.tsx b/packages/taler-wallet-webextension/src/mui/Button.tsx @@ -371,7 +371,11 @@ function ButtonBase({ ); } return ( - <button onClick={doClick} class={classNames} {...rest}> + <button onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + doClick(); + }} class={classNames} {...rest}> {children} </button> ); diff --git a/packages/taler-wallet-webextension/src/wallet/Application.tsx b/packages/taler-wallet-webextension/src/wallet/Application.tsx @@ -157,7 +157,7 @@ export function Application(): VNode { )} /> -<Route + <Route path={Pages.balanceHistory.pattern} component={({ currency }: { currency?: string }) => ( <WalletTemplate path="balance" goToTransaction={redirectToTxInfo} goToURL={redirectToURL}> diff --git a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx @@ -19,12 +19,10 @@ import { Amounts, CoinDumpJson, CoinStatus, - ExchangeListItem, ExchangeTosStatus, LogLevel, NotificationType, ScopeType, - parseWithdrawUri, stringifyWithdrawExchange, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; @@ -32,10 +30,19 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; import { Fragment, VNode, h } from "preact"; import { useEffect, useRef, useState } from "preact/hooks"; +import { Pages } from "../NavigationBar.js"; import { Checkbox } from "../components/Checkbox.js"; import { SelectList } from "../components/SelectList.js"; import { Time } from "../components/Time.js"; -import { DestructiveText, LinkPrimary, NotifyUpdateFadeOut, SubTitle, SuccessText, WarningText } from "../components/styled/index.js"; +import { ActiveTasksTable } from "../components/WalletActivity.js"; +import { + DestructiveText, + LinkPrimary, + NotifyUpdateFadeOut, + SubTitle, + SuccessText, + WarningText, +} from "../components/styled/index.js"; import { useAlertContext } from "../context/alert.js"; import { useBackendContext } from "../context/backend.js"; import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; @@ -44,9 +51,6 @@ import { Button } from "../mui/Button.js"; import { Grid } from "../mui/Grid.js"; import { Paper } from "../mui/Paper.js"; import { TextField } from "../mui/TextField.js"; -import { Pages } from "../NavigationBar.js"; -import { CoinInfo } from "@gnu-taler/taler-wallet-core/dbless"; -import { ActiveTasksTable } from "../components/WalletActivity.js"; type CoinsInfo = CoinDumpJson["coins"]; type CalculatedCoinfInfo = { @@ -72,7 +76,7 @@ function hashObjectId(o: any): string { return JSON.stringify(o); } -export function DeveloperPage({ }: Props): VNode { +export function DeveloperPage({}: Props): VNode { const { i18n } = useTranslationContext(); const [downloadedDatabase, setDownloadedDatabase] = useState< { time: Date; content: string } | undefined @@ -110,8 +114,8 @@ export function DeveloperPage({ }: Props): VNode { useEffect(() => { return api.listener.onUpdateNotification(listenAllEvents, (ev) => { - console.log("event", ev) - return hook?.retry() + console.log("event", ev); + return hook?.retry(); }); }); @@ -275,7 +279,6 @@ export function DeveloperPage({ }: Props): VNode { })} /> - <SubTitle> <i18n.Translate>Exchange Entries</i18n.Translate> </SubTitle> @@ -336,19 +339,31 @@ export function DeveloperPage({ }: Props): VNode { ); } } - const uri = !e.masterPub ? undefined : stringifyWithdrawExchange({ - exchangeBaseUrl: e.exchangeBaseUrl, - exchangePub: e.masterPub, - }); + const uri = !e.masterPub + ? undefined + : stringifyWithdrawExchange({ + exchangeBaseUrl: e.exchangeBaseUrl, + }); return ( <tr key={idx}> <td> <a href={!uri ? undefined : Pages.defaultCta({ uri })}> - {e.scopeInfo ? `${e.scopeInfo.currency} (${e.scopeInfo.type === ScopeType.Global ? "global" : "regional"})` : e.currency} + {e.scopeInfo + ? `${e.scopeInfo.currency} (${ + e.scopeInfo.type === ScopeType.Global + ? "global" + : "regional" + })` + : e.currency} </a> </td> <td> - <a href={new URL(`/keys`, e.exchangeBaseUrl).href} target="_blank">{e.exchangeBaseUrl}</a> + <a + href={new URL(`/keys`, e.exchangeBaseUrl).href} + target="_blank" + > + {e.exchangeBaseUrl} + </a> </td> <td> {e.exchangeEntryStatus} / {e.exchangeUpdateStatus} @@ -359,10 +374,10 @@ export function DeveloperPage({ }: Props): VNode { <td> {e.lastUpdateTimestamp ? AbsoluteTime.toIsoString( - AbsoluteTime.fromPreciseTimestamp( - e.lastUpdateTimestamp, - ), - ) + AbsoluteTime.fromPreciseTimestamp( + e.lastUpdateTimestamp, + ), + ) : "never"} </td> <td> @@ -381,31 +396,25 @@ export function DeveloperPage({ }: Props): VNode { </button> <button onClick={() => { - api.wallet.call( - WalletApiOperation.DeleteExchange, - { - exchangeBaseUrl: e.exchangeBaseUrl, - }, - ); + api.wallet.call(WalletApiOperation.DeleteExchange, { + exchangeBaseUrl: e.exchangeBaseUrl, + }); }} > Delete </button> <button onClick={() => { - api.wallet.call( - WalletApiOperation.DeleteExchange, - { - exchangeBaseUrl: e.exchangeBaseUrl, - purge: true, - }, - ); + api.wallet.call(WalletApiOperation.DeleteExchange, { + exchangeBaseUrl: e.exchangeBaseUrl, + purge: true, + }); }} > Purge </button> - {e.scopeInfo && e.masterPub && e.currency ? - (e.scopeInfo.type === ScopeType.Global ? + {e.scopeInfo && e.masterPub && e.currency ? ( + e.scopeInfo.type === ScopeType.Global ? ( <button onClick={() => { api.wallet.call( @@ -418,30 +427,27 @@ export function DeveloperPage({ }: Props): VNode { ); }} > - Make regional </button> - : e.scopeInfo.type === ScopeType.Auditor ? - undefined - - : e.scopeInfo.type === ScopeType.Exchange ? - <button - onClick={() => { - api.wallet.call( - WalletApiOperation.AddGlobalCurrencyExchange, - { - exchangeBaseUrl: e.exchangeBaseUrl, - currency: e.currency!, - exchangeMasterPub: e.masterPub!, - }, - ); - }} - > - - Make global - </button> - : undefined) : undefined - } + ) : e.scopeInfo.type === + ScopeType.Auditor ? undefined : e.scopeInfo.type === + ScopeType.Exchange ? ( + <button + onClick={() => { + api.wallet.call( + WalletApiOperation.AddGlobalCurrencyExchange, + { + exchangeBaseUrl: e.exchangeBaseUrl, + currency: e.currency!, + exchangeMasterPub: e.masterPub!, + }, + ); + }} + > + Make global + </button> + ) : undefined + ) : undefined} <button onClick={() => { api.wallet.call( @@ -469,7 +475,6 @@ export function DeveloperPage({ }: Props): VNode { </LinkPrimary> </div> - <Paper style={{ padding: 10, margin: 10 }}> <h3>Logging</h3> <div> diff --git a/packages/taler-wallet-webextension/src/wxApi.ts b/packages/taler-wallet-webextension/src/wxApi.ts @@ -46,6 +46,7 @@ import { MessageFromFrontendWallet, } from "./platform/api.js"; import { platform } from "./platform/foreground.js"; +import { WalletActivityTrack } from "./wxBackend.js"; /** * @@ -74,8 +75,10 @@ export interface BackgroundOperations { response: void; }; getNotifications: { - request: void; - response: WalletEvent[]; + request: { + filter: string; + }; + response: WalletActivityTrack[]; }; clearNotifications: { request: void; diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts @@ -25,7 +25,6 @@ */ import { AbsoluteTime, - BalanceFlag, LogLevel, Logger, NotificationType, @@ -34,14 +33,13 @@ import { TalerError, TalerErrorCode, TalerErrorDetail, - TransactionMajorState, TransactionMinorState, WalletNotification, getErrorDetailFromException, makeErrorDetail, openPromise, setGlobalLogLevelFromString, - setLogLevelFromString, + setLogLevelFromString } from "@gnu-taler/taler-util"; import { HttpRequestLibrary } from "@gnu-taler/taler-util/http"; import { @@ -55,11 +53,11 @@ import { exportDb, importDb, } from "@gnu-taler/taler-wallet-core"; +import { BrowserFetchHttpLib } from "@gnu-taler/web-util/browser"; import { MessageFromFrontend, MessageResponse } from "./platform/api.js"; import { platform } from "./platform/background.js"; import { ExtensionOperations } from "./taler-wallet-interaction-loader.js"; -import { BackgroundOperations, WalletEvent } from "./wxApi.js"; -import { BrowserFetchHttpLib } from "@gnu-taler/web-util/browser"; +import { BackgroundOperations } from "./wxApi.js"; /** * Currently active wallet instance. Might be unloaded and @@ -92,14 +90,162 @@ async function resetDb(): Promise<void> { await reinitWallet(); } +export type WalletActivityTrack = { + id: number; + events: (WalletNotification & {when: AbsoluteTime})[]; + start: AbsoluteTime; + type: NotificationType; + end: AbsoluteTime; + groupId: string; +}; + +let counter = 0; +function getUniqueId(): number { + return counter++; +} + //FIXME: maybe circular buffer -const notifications: WalletEvent[] = []; -async function getNotifications(): Promise<WalletEvent[]> { - return notifications; +const activity: WalletActivityTrack[] = []; + +function addNewWalletActivityNotification(list: WalletActivityTrack[], n: WalletNotification) { + const start = AbsoluteTime.now(); + const ev = {...n, when:start}; + switch (n.type) { + case NotificationType.BalanceChange: { + const groupId = `${n.type}:${n.hintTransactionId}`; + const found = list.find((a)=>a.groupId === groupId) + if (found) { + found.end = start; + found.events.unshift(ev) + return; + } + list.push({ + id: getUniqueId(), + type: n.type, + start, + end: AbsoluteTime.never(), + events: [ev], + groupId, + }); + return; + } + case NotificationType.BackupOperationError: { + const groupId = ""; + list.push({ + id: getUniqueId(), + type: n.type, + start, + end: AbsoluteTime.never(), + events: [ev], + groupId, + }); + return; + } + case NotificationType.TransactionStateTransition: { + const groupId = `${n.type}:${n.transactionId}`; + const found = list.find((a)=>a.groupId === groupId) + if (found) { + found.end = start; + found.events.unshift(ev) + return; + } + list.push({ + id: getUniqueId(), + type: n.type, + start, + end: AbsoluteTime.never(), + events: [ev], + groupId, + }); + return; + } + case NotificationType.WithdrawalOperationTransition: { + return; + } + case NotificationType.ExchangeStateTransition: { + const groupId = `${n.type}:${n.exchangeBaseUrl}`; + const found = list.find((a)=>a.groupId === groupId) + if (found) { + found.end = start; + found.events.unshift(ev) + return; + } + list.push({ + id: getUniqueId(), + type: n.type, + start, + end: AbsoluteTime.never(), + events: [ev], + groupId, + }); + return; + } + case NotificationType.Idle: { + const groupId = ""; + list.push({ + id: getUniqueId(), + type: n.type, + start, + end: AbsoluteTime.never(), + events: [ev], + groupId, + }); + return; + } + case NotificationType.TaskObservabilityEvent: { + const groupId = `${n.type}:${n.taskId}`; + const found = list.find((a)=>a.groupId === groupId) + if (found) { + found.end = start; + found.events.unshift(ev) + return; + } + list.push({ + id: getUniqueId(), + type: n.type, + start, + end: AbsoluteTime.never(), + events: [ev], + groupId, + }); + return; + } + case NotificationType.RequestObservabilityEvent: { + const groupId = `${n.type}:${n.operation}:${n.requestId}`; + const found = list.find((a)=>a.groupId === groupId) + if (found) { + found.end = start; + found.events.unshift(ev) + return; + } + list.push({ + id: getUniqueId(), + type: n.type, + start, + end: AbsoluteTime.never(), + events: [ev], + groupId, + }); + return; + } + } +} + +async function getNotifications({ + filter, +}: { + filter: string; +}): Promise<WalletActivityTrack[]> { + if (!filter) return activity; + + const rg = new RegExp(`.*${filter}.*`); + return activity.filter((event) => { + return rg.test(event.groupId.toLowerCase()); + }); } async function clearNotifications(): Promise<void> { - notifications.splice(0, notifications.length); + activity.splice(0, activity.length); } async function runGarbageCollector(): Promise<void> { @@ -327,10 +473,7 @@ async function reinitWallet(): Promise<void> { } wallet.addNotificationListener((message) => { if (settings.showWalletActivity) { - notifications.push({ - notification: message, - when: AbsoluteTime.now(), - }); + addNewWalletActivityNotification(activity, message); } processWalletNotification(message); @@ -394,7 +537,7 @@ async function updateIconBasedOnBalance() { let showAlert = false; for (const b of balance.balances) { if (b.flags.length > 0) { - console.log("b.flags", JSON.stringify(b.flags)) + console.log("b.flags", JSON.stringify(b.flags)); showAlert = true; break; } diff --git a/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx b/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx @@ -47,7 +47,7 @@ const form: FlexibleForm_Deprecated<TargetObject> = { design: [{ title: "this is a simple form" as TranslatedString, fields: [{ - type: "absoluteTime", + type: "absoluteTimeText", properties: { label: "label of the field" as TranslatedString, name: "today", diff --git a/packages/web-util/src/forms/InputAmount.tsx b/packages/web-util/src/forms/InputAmount.tsx @@ -18,25 +18,26 @@ export function InputAmount<T extends object, K extends keyof T>( : (value as any).currency; return ( <InputLine<T, K> + {...props} type="text" before={{ type: "text", text: currency as TranslatedString, }} - //@ts-ignore - converter={ props.converter ?? { - - fromStringUI: (v): AmountJson => { - return ( - Amounts.parse(`${currency}:${v}`) ?? - Amounts.zeroOfCurrency(currency) - ); - }, - toStringUI: (v: AmountJson) => { - return v === undefined ? "" : Amounts.stringifyValue(v); - }, - }} - {...props} + //@ts-ignore + converter={ + props.converter ?? { + fromStringUI: (v): AmountJson => { + return ( + Amounts.parse(`${currency}:${v}`) ?? + Amounts.zeroOfCurrency(currency) + ); + }, + toStringUI: (v: AmountJson) => { + return v === undefined ? "" : Amounts.stringifyValue(v); + }, + } + } /> ); } diff --git a/packages/web-util/src/forms/InputArray.tsx b/packages/web-util/src/forms/InputArray.tsx @@ -99,7 +99,7 @@ export function InputArray<T extends object, K extends keyof T>( const [selectedIndex, setSelected] = useState<number | undefined>(undefined); const selected = selectedIndex === undefined ? undefined : list[selectedIndex]; - + return ( <div class="sm:col-span-6"> <LabelWithTooltipMaybeRequired @@ -110,9 +110,10 @@ export function InputArray<T extends object, K extends keyof T>( <div class="-space-y-px rounded-md bg-white "> {list.map((v, idx) => { + const label = getValueDeeper(v, labelField.split(".")) return ( <Option - label={v[labelField] as TranslatedString} + label={label as TranslatedString} key={idx} isSelected={selectedIndex === idx} isLast={idx === list.length - 1} @@ -158,7 +159,7 @@ export function InputArray<T extends object, K extends keyof T>( // elements should be present in the state object since this is expected to be an array //@ts-ignore // return state.elements[selectedIndex]; - return {} + return {}; }} onSubmit={(v) => { const newValue = [...list]; @@ -202,3 +203,24 @@ export function InputArray<T extends object, K extends keyof T>( </div> ); } + + + +export function getValueDeeper( + object: Record<string, any>, + names: string[], +): string { + if (names.length === 0) { + return object as any as string; + } + const [head, ...rest] = names; + if (!head) { + return getValueDeeper(object, rest); + } + if (object === undefined) { + return "" + } + return getValueDeeper(object[head], rest); +} + + diff --git a/packages/web-util/src/forms/InputChoiceHorizontal.tsx b/packages/web-util/src/forms/InputChoiceHorizontal.tsx @@ -34,11 +34,12 @@ export function InputChoiceHorizontal<T extends object, K extends keyof T>( <fieldset class="mt-2"> <div class="isolate inline-flex rounded-md shadow-sm"> {choices.map((choice, idx) => { + const convertedValue = converter?.fromStringUI(choice.value as any) const isFirst = idx === 0; const isLast = idx === choices.length - 1; let clazz = "relative inline-flex items-center px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 focus:z-10"; - if (converter?.fromStringUI(choice.value as any) === value) { + if (convertedValue !== undefined && convertedValue === value) { clazz += " text-white bg-indigo-600 hover:bg-indigo-500 ring-2 ring-indigo-600 hover:ring-indigo-500"; } else { @@ -61,7 +62,7 @@ export function InputChoiceHorizontal<T extends object, K extends keyof T>( class={clazz} onClick={(e) => { onChange( - (value === choice.value ? undefined : converter?.fromStringUI(choice.value as any)) as any, + (value === choice.value ? undefined : convertedValue) as any, ); }} > diff --git a/packages/web-util/src/forms/converter.ts b/packages/web-util/src/forms/converter.ts @@ -53,6 +53,15 @@ function parseAmlState(s: string | undefined): TalerExchangeApi.AmlState { } } +const nullConverter: StringConverter<string> = { + fromStringUI(v: string | undefined): string { + return v ?? ""; + }, + toStringUI(v: unknown): string { + return v as string; + }, +}; + function amountConverter(config: any): StringConverter<AmountJson> { const currency = config["currency"]; if (!currency || typeof currency !== "string") { @@ -61,7 +70,9 @@ function amountConverter(config: any): StringConverter<AmountJson> { return { fromStringUI(v: string | undefined): AmountJson { // FIXME: requires currency - return Amounts.parse(`${currency}:${v}`) ?? Amounts.zeroOfCurrency(currency); + return ( + Amounts.parse(`${currency}:${v}`) ?? Amounts.zeroOfCurrency(currency) + ); }, toStringUI(v: unknown): string { return v === undefined ? "" : Amounts.stringifyValue(v as AmountJson); @@ -82,7 +93,7 @@ function absTimeConverter(config: any): StringConverter<AbsoluteTime> { try { const time = parse(v, pattern, new Date()); return AbsoluteTime.fromMilliseconds(time.getTime()); - } catch(e) { + } catch (e) { return AbsoluteTime.never(); } }, @@ -91,9 +102,9 @@ function absTimeConverter(config: any): StringConverter<AbsoluteTime> { const d = v as AbsoluteTime; if (d.t_ms === "never") return "never"; try { - return format(d.t_ms, pattern) + return format(d.t_ms, pattern); } catch (e) { - return "" + return ""; } }, }; @@ -115,5 +126,5 @@ export function getConverterById( // @ts-expect-error check this return amlStateConverter; } - return undefined!; + return nullConverter as StringConverter<unknown>; } diff --git a/packages/web-util/src/forms/forms.ts b/packages/web-util/src/forms/forms.ts @@ -14,9 +14,9 @@ import { InputText } from "./InputText.js"; import { InputTextArea } from "./InputTextArea.js"; import { InputToggle } from "./InputToggle.js"; import { Addon, StringConverter, UIFieldHandler } from "./FormProvider.js"; -import { InternationalizationAPI, UIFieldBaseDescription } from "../index.browser.js"; +import { InternationalizationAPI, UIFieldElementDescription } from "../index.browser.js"; import { assertUnreachable, TranslatedString } from "@gnu-taler/taler-util"; -import {UIFormFieldBaseConfig, UIFormFieldConfig} from "./ui-form.js"; +import {UIFormFieldBaseConfig, UIFormElementConfig} from "./ui-form.js"; /** * Constrain the type with the ui props */ @@ -31,7 +31,7 @@ type FieldType<T extends object = any, K extends keyof T = any> = { textArea: Parameters<typeof InputTextArea<T, K>>[0]; choiceStacked: Parameters<typeof InputChoiceStacked<T, K>>[0]; choiceHorizontal: Parameters<typeof InputChoiceHorizontal<T, K>>[0]; - absoluteTime: Parameters<typeof InputAbsoluteTime<T, K>>[0]; + absoluteTimeText: Parameters<typeof InputAbsoluteTime<T, K>>[0]; integer: Parameters<typeof InputInteger<T, K>>[0]; toggle: Parameters<typeof InputToggle<T, K>>[0]; amount: Parameters<typeof InputAmount<T, K>>[0]; @@ -64,8 +64,8 @@ export type UIFormField = | { type: "integer"; properties: FieldType["integer"] } | { type: "toggle"; properties: FieldType["toggle"] } | { - type: "absoluteTime"; - properties: FieldType["absoluteTime"]; + type: "absoluteTimeText"; + properties: FieldType["absoluteTimeText"]; }; type FieldComponentFunction<key extends keyof FieldType> = ( @@ -89,7 +89,7 @@ const UIFormConfiguration: UIFormFieldMap = { file: InputFile, textArea: InputTextArea, //@ts-ignore - absoluteTime: InputAbsoluteTime, + absoluteTimeText: InputAbsoluteTime, //@ts-ignore choiceStacked: InputChoiceStacked, //@ts-ignore @@ -156,7 +156,7 @@ export function RenderAllFieldsByUiConfig({ */ export function convertUiField( i18n_: InternationalizationAPI, - fieldConfig: UIFormFieldConfig[], + fieldConfig: UIFormElementConfig[], form: object, getConverterById: GetConverterById, ): UIFormField[] { @@ -166,7 +166,7 @@ export function convertUiField( case "caption": { const resp: UIFormField = { type: config.type, - properties: converBaseFieldsProps(i18n_, config.properties), + properties: converBaseFieldsProps(i18n_, config), }; return resp; } @@ -174,8 +174,8 @@ export function convertUiField( const resp: UIFormField = { type: config.type, properties: { - ...converBaseFieldsProps(i18n_, config.properties), - fields: convertUiField(i18n_, config.properties.fields, form, getConverterById), + ...converBaseFieldsProps(i18n_, config), + fields: convertUiField(i18n_, config.fields, form, getConverterById), }, }; return resp; @@ -187,19 +187,19 @@ export function convertUiField( return { type: "array", properties: { - ...converBaseFieldsProps(i18n_, config.properties), - ...converInputFieldsProps(form, config.properties, getConverterById), - labelField: config.properties.labelFieldId, - fields: convertUiField(i18n_, config.properties.fields, form, getConverterById), + ...converBaseFieldsProps(i18n_, config), + ...converInputFieldsProps(form, config, getConverterById), + labelField: config.labelFieldId, + fields: convertUiField(i18n_, config.fields, form, getConverterById), }, } as UIFormField; } - case "absoluteTime": { + case "absoluteTimeText": { return { - type: "absoluteTime", + type: "absoluteTimeText", properties: { - ...converBaseFieldsProps(i18n_, config.properties), - ...converInputFieldsProps(form, config.properties, getConverterById), + ...converBaseFieldsProps(i18n_, config), + ...converInputFieldsProps(form, config, getConverterById), }, } as UIFormField; } @@ -207,8 +207,9 @@ export function convertUiField( return { type: "amount", properties: { - ...converBaseFieldsProps(i18n_, config.properties), - ...converInputFieldsProps(form, config.properties, getConverterById), + ...converBaseFieldsProps(i18n_, config), + ...converInputFieldsProps(form, config, getConverterById), + currency: config.currency, }, } as UIFormField; } @@ -216,9 +217,9 @@ export function convertUiField( return { type: "choiceHorizontal", properties: { - ...converBaseFieldsProps(i18n_, config.properties), - ...converInputFieldsProps(form, config.properties, getConverterById), - choices: config.properties.choices, + ...converBaseFieldsProps(i18n_, config), + ...converInputFieldsProps(form, config, getConverterById), + choices: config.choices, }, } as UIFormField; } @@ -226,9 +227,9 @@ export function convertUiField( return { type: "choiceStacked", properties: { - ...converBaseFieldsProps(i18n_, config.properties), - ...converInputFieldsProps(form, config.properties, getConverterById), - choices: config.properties.choices, + ...converBaseFieldsProps(i18n_, config), + ...converInputFieldsProps(form, config, getConverterById), + choices: config.choices, }, }as UIFormField; @@ -237,10 +238,10 @@ export function convertUiField( return { type: "file", properties: { - ...converBaseFieldsProps(i18n_, config.properties), - ...converInputFieldsProps(form, config.properties, getConverterById), - accept: config.properties.accept, - maxBites: config.properties.maxBytes, + ...converBaseFieldsProps(i18n_, config), + ...converInputFieldsProps(form, config, getConverterById), + accept: config.accept, + maxBites: config.maxBytes, }, } as UIFormField; } @@ -248,8 +249,8 @@ export function convertUiField( return { type: "integer", properties: { - ...converBaseFieldsProps(i18n_, config.properties), - ...converInputFieldsProps(form, config.properties, getConverterById), + ...converBaseFieldsProps(i18n_, config), + ...converInputFieldsProps(form, config, getConverterById), }, } as UIFormField; } @@ -257,9 +258,9 @@ export function convertUiField( return { type: "selectMultiple", properties: { - ...converBaseFieldsProps(i18n_, config.properties), - ...converInputFieldsProps(form, config.properties, getConverterById), - choices: config.properties.choices, + ...converBaseFieldsProps(i18n_, config), + ...converInputFieldsProps(form, config, getConverterById), + choices: config.choices, }, } as UIFormField; } @@ -267,9 +268,9 @@ export function convertUiField( return { type: "selectOne", properties: { - ...converBaseFieldsProps(i18n_, config.properties), - ...converInputFieldsProps(form, config.properties, getConverterById), - choices: config.properties.choices, + ...converBaseFieldsProps(i18n_, config), + ...converInputFieldsProps(form, config, getConverterById), + choices: config.choices, }, } as UIFormField; } @@ -277,8 +278,8 @@ export function convertUiField( return { type: "text", properties: { - ...converBaseFieldsProps(i18n_, config.properties), - ...converInputFieldsProps(form, config.properties, getConverterById), + ...converBaseFieldsProps(i18n_, config), + ...converInputFieldsProps(form, config, getConverterById), }, } as UIFormField; } @@ -286,8 +287,8 @@ export function convertUiField( return { type: "text", properties: { - ...converBaseFieldsProps(i18n_, config.properties), - ...converInputFieldsProps(form, config.properties, getConverterById), + ...converBaseFieldsProps(i18n_, config), + ...converInputFieldsProps(form, config, getConverterById), }, } as UIFormField; } @@ -295,8 +296,8 @@ export function convertUiField( return { type: "toggle", properties: { - ...converBaseFieldsProps(i18n_, config.properties), - ...converInputFieldsProps(form, config.properties, getConverterById), + ...converBaseFieldsProps(i18n_, config), + ...converInputFieldsProps(form, config, getConverterById), }, } as UIFormField; } @@ -340,7 +341,7 @@ function converInputFieldsProps( function converBaseFieldsProps( i18n_: InternationalizationAPI, - p: UIFieldBaseDescription, + p: UIFieldElementDescription, ) { return { after: getAddonById(p.addonAfterId), @@ -353,7 +354,7 @@ function converBaseFieldsProps( }; } -function getValueDeeper2( +export function getValueDeeper2( object: Record<string, any>, names: string[], ): UIFieldHandler { diff --git a/packages/web-util/src/forms/ui-form.ts b/packages/web-util/src/forms/ui-form.ts @@ -14,18 +14,18 @@ import { TalerProtocolTimestamp, } from "@gnu-taler/taler-util"; -export type FlexibleForm = DoubleColumnForm; +export type FormConfiguration = DoubleColumnForm; -export interface DoubleColumnForm { +export type DoubleColumnForm = { type: "double-column"; - design: Array<DoubleColumnFormSection>; + design: DoubleColumnFormSection[]; // behavior?: (form: Partial<T>) => FormState<T>; -} +}; export type DoubleColumnFormSection = { title: string; description?: string; - fields: UIFormFieldConfig[]; + fields: UIFormElementConfig[]; }; // export interface BaseForm { @@ -33,92 +33,74 @@ export type DoubleColumnFormSection = { // threshold: AmountJson; // } -export type UIFormFieldConfig = - | UIFormFieldConfigAbsoluteTime - | UIFormFieldConfigAmount - | UIFormFieldConfigArray - | UIFormFieldConfigCaption - | UIFormFieldConfigChoiseHorizontal - | UIFormFieldConfigChoiseStacked - | UIFormFieldConfigFile - | UIFormFieldConfigGroup - | UIFormFieldConfigInteger - | UIFormFieldConfigSelectMultiple - | UIFormFieldConfigSelectOne - | UIFormFieldConfigText - | UIFormFieldConfigTextArea - | UIFormFieldConfigToggle; - -type UIFormFieldConfigAbsoluteTime = { - type: "absoluteTime"; - properties: UIFormFieldBaseConfig & { - max?: TalerProtocolTimestamp; - min?: TalerProtocolTimestamp; - pattern: string; - }; -}; - -type UIFormFieldConfigAmount = { +export type UIFormElementConfig = + | UIFormElementGroup + | UIFormElementCaption + | UIFormFieldAbsoluteTime + | UIFormFieldAmount + | UIFormFieldArray + | UIFormFieldChoiseHorizontal + | UIFormFieldChoiseStacked + | UIFormFieldFile + | UIFormFieldInteger + | UIFormFieldSelectMultiple + | UIFormFieldSelectOne + | UIFormFieldText + | UIFormFieldTextArea + | UIFormFieldToggle; + +type UIFormFieldAbsoluteTime = { + type: "absoluteTimeText"; + max?: TalerProtocolTimestamp; + min?: TalerProtocolTimestamp; + pattern: string; +} & UIFormFieldBaseConfig; + +type UIFormFieldAmount = { type: "amount"; - properties: UIFormFieldBaseConfig & { - max?: Integer; - min?: Integer; - currency: string; - }; -}; + max?: Integer; + min?: Integer; + currency: string; +} & UIFormFieldBaseConfig; -type UIFormFieldConfigArray = { +type UIFormFieldArray = { type: "array"; - properties: UIFormFieldBaseConfig & { - // id of the field shown when the array is collapsed - labelFieldId: UIHandlerId; - fields: UIFormFieldConfig[]; - }; -}; + // id of the field shown when the array is collapsed + labelFieldId: UIHandlerId; + fields: UIFormElementConfig[]; +} & UIFormFieldBaseConfig; -type UIFormFieldConfigCaption = { - type: "caption"; - properties: UIFieldBaseDescription; -}; +type UIFormElementCaption = { type: "caption" } & UIFieldElementDescription; -type UIFormFieldConfigGroup = { +type UIFormElementGroup = { type: "group"; - properties: UIFieldBaseDescription & { - fields: UIFormFieldConfig[]; - }; -}; + fields: UIFormElementConfig[]; +} & UIFieldElementDescription; -type UIFormFieldConfigChoiseHorizontal = { +type UIFormFieldChoiseHorizontal = { type: "choiceHorizontal"; - properties: UIFormFieldBaseConfig & { - choices: Array<SelectUiChoice>; - }; -}; + choices: Array<SelectUiChoice>; +} & UIFormFieldBaseConfig; -type UIFormFieldConfigChoiseStacked = { +type UIFormFieldChoiseStacked = { type: "choiceStacked"; - properties: UIFormFieldBaseConfig & { - choices: Array<SelectUiChoice>; - }; -}; + choices: Array<SelectUiChoice>; +} & UIFormFieldBaseConfig; -type UIFormFieldConfigFile = { +type UIFormFieldFile = { type: "file"; - properties: UIFormFieldBaseConfig & { - maxBytes?: Integer; - minBytes?: Integer; - // comma-separated list of one or more file types - // https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept#unique_file_type_specifiers - accept?: string; - }; -}; -type UIFormFieldConfigInteger = { + maxBytes?: Integer; + minBytes?: Integer; + // comma-separated list of one or more file types + // https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept#unique_file_type_specifiers + accept?: string; +} & UIFormFieldBaseConfig; + +type UIFormFieldInteger = { type: "integer"; - properties: UIFormFieldBaseConfig & { - max?: Integer; - min?: Integer; - }; -}; + max?: Integer; + min?: Integer; +} & UIFormFieldBaseConfig; interface SelectUiChoice { label: string; @@ -126,41 +108,30 @@ interface SelectUiChoice { value: string; } -type UIFormFieldConfigSelectMultiple = { +type UIFormFieldSelectMultiple = { type: "selectMultiple"; - properties: UIFormFieldBaseConfig & { - max?: Integer; - min?: Integer; - unique?: boolean; - choices: Array<SelectUiChoice>; - }; -}; -type UIFormFieldConfigSelectOne = { + max?: Integer; + min?: Integer; + unique?: boolean; + choices: Array<SelectUiChoice>; +} & UIFormFieldBaseConfig; + +type UIFormFieldSelectOne = { type: "selectOne"; - properties: UIFormFieldBaseConfig & { - choices: Array<SelectUiChoice>; - }; -}; -type UIFormFieldConfigText = { - type: "text"; - properties: UIFormFieldBaseConfig; -}; -type UIFormFieldConfigTextArea = { - type: "textArea"; - properties: UIFormFieldBaseConfig; -}; -type UIFormFieldConfigToggle = { - type: "toggle"; - properties: UIFormFieldBaseConfig; -}; + choices: Array<SelectUiChoice>; +} & UIFormFieldBaseConfig; +type UIFormFieldText = { type: "text" } & UIFormFieldBaseConfig; +type UIFormFieldTextArea = { type: "textArea" } & UIFormFieldBaseConfig; +type UIFormFieldToggle = { type: "toggle" } & UIFormFieldBaseConfig; -export type UIFieldBaseDescription = { +export type UIFieldElementDescription = { /* label if the field, visible for the user */ label: string; + /* long text to be shown on user demand */ tooltip?: string; - /* short text to be shown close to the field */ + /* short text to be shown close to the field, usually below and dimmer*/ help?: string; /* name of the field, useful for a11y */ @@ -168,13 +139,15 @@ export type UIFieldBaseDescription = { /* if the field should be initially hidden */ hidden?: boolean; + /* ui element to show before */ addonBeforeId?: string; + /* ui element to show after */ addonAfterId?: string; }; -export type UIFormFieldBaseConfig = UIFieldBaseDescription & { +export type UIFormFieldBaseConfig = UIFieldElementDescription & { /* example to be shown inside the field */ placeholder?: string; @@ -200,7 +173,7 @@ export type UIHandlerId = string & { [__handlerId]: true }; const codecForUiFieldId = codecForString as () => Codec<UIHandlerId>; const codecForUIFormFieldBaseDescriptionTemplate = < - T extends UIFieldBaseDescription, + T extends UIFieldElementDescription, >() => buildCodecForObject<T>() .property("addonAfterId", codecOptional(codecForString())) @@ -221,62 +194,35 @@ const codecForUIFormFieldBaseConfigTemplate = < .property("required", codecOptional(codecForBoolean())) .property("placeholder", codecOptional(codecForString())); -const codecForUIFormFieldBaseConfig = (): Codec<UIFormFieldBaseConfig> => - codecForUIFormFieldBaseConfigTemplate().build("UIFieldToggleProperties"); - -const codecForUIFormFieldAbsoluteTimeConfig = (): Codec< - UIFormFieldConfigAbsoluteTime["properties"] -> => - codecForUIFormFieldBaseConfigTemplate< - UIFormFieldConfigAbsoluteTime["properties"] - >() +const codecForUiFormFieldAbsoluteTime = (): Codec<UIFormFieldAbsoluteTime> => + codecForUIFormFieldBaseConfigTemplate<UIFormFieldAbsoluteTime>() + .property("type", codecForConstString("absoluteTimeText")) .property("pattern", codecForString()) .property("max", codecOptional(codecForTimestamp)) .property("min", codecOptional(codecForTimestamp)) - .build("UIFormFieldConfigAbsoluteTime.properties"); - -const codecForUiFormFieldAbsoluteTime = - (): Codec<UIFormFieldConfigAbsoluteTime> => - buildCodecForObject<UIFormFieldConfigAbsoluteTime>() - .property("type", codecForConstString("absoluteTime")) - .property("properties", codecForUIFormFieldAbsoluteTimeConfig()) - .build("UIFormFieldConfigAbsoluteTime"); - -const codecForUIFormFieldAmountConfig = (): Codec< - UIFormFieldConfigAmount["properties"] -> => - codecForUIFormFieldBaseConfigTemplate<UIFormFieldConfigAmount["properties"]>() + .build("UIFormFieldAbsoluteTime"); + +const codecForUiFormFieldAmount = (): Codec<UIFormFieldAmount> => + codecForUIFormFieldBaseConfigTemplate<UIFormFieldAmount>() + .property("type", codecForConstString("amount")) .property("currency", codecForString()) .property("max", codecOptional(codecForNumber())) .property("min", codecOptional(codecForNumber())) - .build("UIFormFieldConfigAmount.properties"); - -const codecForUiFormFieldAmount = (): Codec<UIFormFieldConfigAmount> => - buildCodecForObject<UIFormFieldConfigAmount>() - .property("type", codecForConstString("amount")) - .property("properties", codecForUIFormFieldAmountConfig()) - .build("UIFormFieldConfigAmount"); + .build("UIFormFieldAmount"); -const codecForUIFormFieldArrayConfig = (): Codec< - UIFormFieldConfigArray["properties"] -> => - codecForUIFormFieldBaseConfigTemplate<UIFormFieldConfigArray["properties"]>() +const codecForUiFormFieldArray = (): Codec<UIFormFieldArray> => + codecForUIFormFieldBaseConfigTemplate<UIFormFieldArray>() + .property("type", codecForConstString("array")) .property("labelFieldId", codecForUiFieldId()) + .property("tooltip", codecOptional(codecForString())) // eslint-disable-next-line @typescript-eslint/no-use-before-define .property("fields", codecForList(codecForUiFormField())) - .build("UIFormFieldConfigArray.properties"); - -const codecForUiFormFieldArray = (): Codec<UIFormFieldConfigArray> => - buildCodecForObject<UIFormFieldConfigArray>() - .property("type", codecForConstString("array")) - .property("properties", codecForUIFormFieldArrayConfig()) - .build("UIFormFieldConfigArray"); + .build("UIFormFieldArray"); -const codecForUiFormFieldCaption = (): Codec<UIFormFieldConfigCaption> => - buildCodecForObject<UIFormFieldConfigCaption>() +const codecForUiFormFieldCaption = (): Codec<UIFormElementCaption> => + codecForUIFormFieldBaseDescriptionTemplate<UIFormElementCaption>() .property("type", codecForConstString("caption")) - .property("properties", codecForUIFormFieldBaseConfig()) - .build("UIFormFieldConfigCaption"); + .build("UIFormFieldCaption"); const codecForUiFormSelectUiChoice = (): Codec<SelectUiChoice> => buildCodecForObject<SelectUiChoice>() @@ -285,115 +231,79 @@ const codecForUiFormSelectUiChoice = (): Codec<SelectUiChoice> => .property("value", codecForString()) .build("SelectUiChoice"); -const codecForUIFormFieldWithChoiseConfig = (): Codec< - UIFormFieldConfigChoiseHorizontal["properties"] -> => - codecForUIFormFieldBaseConfigTemplate< - UIFormFieldConfigChoiseHorizontal["properties"] - >() - .property("choices", codecForList(codecForUiFormSelectUiChoice())) - .build("UIFormFieldConfigChoiseHorizontal.properties"); - const codecForUiFormFieldChoiceHorizontal = - (): Codec<UIFormFieldConfigChoiseHorizontal> => - buildCodecForObject<UIFormFieldConfigChoiseHorizontal>() + (): Codec<UIFormFieldChoiseHorizontal> => + codecForUIFormFieldBaseConfigTemplate<UIFormFieldChoiseHorizontal>() .property("type", codecForConstString("choiceHorizontal")) - .property("properties", codecForUIFormFieldWithChoiseConfig()) - .build("UIFormFieldConfigChoiseHorizontal"); - -const codecForUiFormFieldChoiceStacked = - (): Codec<UIFormFieldConfigChoiseStacked> => - buildCodecForObject<UIFormFieldConfigChoiseStacked>() - .property("type", codecForConstString("choiceStacked")) - .property("properties", codecForUIFormFieldWithChoiseConfig()) - .build("UIFormFieldConfigChoiseStacked"); - -const codecForUIFormFieldFileConfig = (): Codec< - UIFormFieldConfigFile["properties"] -> => - codecForUIFormFieldBaseConfigTemplate<UIFormFieldConfigFile["properties"]>() + .property("choices", codecForList(codecForUiFormSelectUiChoice())) + .build("UIFormFieldChoiseHorizontal"); + +const codecForUiFormFieldChoiceStacked = (): Codec<UIFormFieldChoiseStacked> => + codecForUIFormFieldBaseConfigTemplate<UIFormFieldChoiseStacked>() + .property("type", codecForConstString("choiceStacked")) + .property("choices", codecForList(codecForUiFormSelectUiChoice())) + .build("UIFormFieldChoiseStacked"); + +const codecForUiFormFieldFile = (): Codec<UIFormFieldFile> => + codecForUIFormFieldBaseConfigTemplate<UIFormFieldFile>() + .property("type", codecForConstString("file")) .property("accept", codecOptional(codecForString())) .property("maxBytes", codecOptional(codecForNumber())) .property("minBytes", codecOptional(codecForNumber())) - .build("UIFormFieldConfigFile.properties"); + .build("UIFormFieldFile"); -const codecForUiFormFieldFile = (): Codec<UIFormFieldConfigFile> => - buildCodecForObject<UIFormFieldConfigFile>() - .property("type", codecForConstString("file")) - .property("properties", codecForUIFormFieldFileConfig()) - .build("UIFormFieldConfigFile"); - -const codecForUIFormFieldWithFieldsConfig = (): Codec< - UIFormFieldConfigGroup["properties"] -> => - codecForUIFormFieldBaseDescriptionTemplate< - UIFormFieldConfigGroup["properties"] - >() +const codecForUiFormFieldGroup = (): Codec<UIFormElementGroup> => + codecForUIFormFieldBaseDescriptionTemplate<UIFormElementGroup>() + .property("type", codecForConstString("group")) // eslint-disable-next-line @typescript-eslint/no-use-before-define .property("fields", codecForList(codecForUiFormField())) - .build("UIFormFieldConfigGroup.properties"); - -const codecForUiFormFieldGroup = (): Codec<UIFormFieldConfigGroup> => - buildCodecForObject<UIFormFieldConfigGroup>() - .property("type", codecForConstString("group")) - .property("properties", codecForUIFormFieldWithFieldsConfig()) .build("UiFormFieldGroup"); -const codecForUiFormFieldInteger = (): Codec<UIFormFieldConfigInteger> => - buildCodecForObject<UIFormFieldConfigInteger>() +const codecForUiFormFieldInteger = (): Codec<UIFormFieldInteger> => + codecForUIFormFieldBaseConfigTemplate<UIFormFieldInteger>() .property("type", codecForConstString("integer")) - .property("properties", codecForUIFormFieldBaseConfig()) - .build("UIFormFieldConfigInteger"); - -const codecForUIFormFieldSelectMultipleConfig = (): Codec< - UIFormFieldConfigSelectMultiple["properties"] -> => - codecForUIFormFieldBaseConfigTemplate< - UIFormFieldConfigSelectMultiple["properties"] - >() + // .property("properties", codecForUIFormFieldBaseConfig()) .property("max", codecOptional(codecForNumber())) .property("min", codecOptional(codecForNumber())) - .property("unique", codecOptional(codecForBoolean())) - .property("choices", codecForList(codecForUiFormSelectUiChoice())) - .build("UIFormFieldConfigSelectMultiple.properties"); + .build("UIFormFieldInteger"); const codecForUiFormFieldSelectMultiple = - (): Codec<UIFormFieldConfigSelectMultiple> => - buildCodecForObject<UIFormFieldConfigSelectMultiple>() + (): Codec<UIFormFieldSelectMultiple> => + codecForUIFormFieldBaseConfigTemplate<UIFormFieldSelectMultiple>() .property("type", codecForConstString("selectMultiple")) - .property("properties", codecForUIFormFieldSelectMultipleConfig()) + .property("max", codecOptional(codecForNumber())) + .property("min", codecOptional(codecForNumber())) + .property("unique", codecOptional(codecForBoolean())) + .property("choices", codecForList(codecForUiFormSelectUiChoice())) .build("UiFormFieldSelectMultiple"); -const codecForUiFormFieldSelectOne = (): Codec<UIFormFieldConfigSelectOne> => - buildCodecForObject<UIFormFieldConfigSelectOne>() +const codecForUiFormFieldSelectOne = (): Codec<UIFormFieldSelectOne> => + codecForUIFormFieldBaseConfigTemplate<UIFormFieldSelectOne>() .property("type", codecForConstString("selectOne")) - .property("properties", codecForUIFormFieldWithChoiseConfig()) - .build("UIFormFieldConfigSelectOne"); + .property("choices", codecForList(codecForUiFormSelectUiChoice())) + .build("UIFormFieldSelectOne"); -const codecForUiFormFieldText = (): Codec<UIFormFieldConfigText> => - buildCodecForObject<UIFormFieldConfigText>() +const codecForUiFormFieldText = (): Codec<UIFormFieldText> => + codecForUIFormFieldBaseConfigTemplate<UIFormFieldText>() .property("type", codecForConstString("text")) - .property("properties", codecForUIFormFieldBaseConfig()) - .build("UIFormFieldConfigText"); + .build("UIFormFieldText"); -const codecForUiFormFieldTextArea = (): Codec<UIFormFieldConfigTextArea> => - buildCodecForObject<UIFormFieldConfigTextArea>() +const codecForUiFormFieldTextArea = (): Codec<UIFormFieldTextArea> => + codecForUIFormFieldBaseConfigTemplate<UIFormFieldTextArea>() .property("type", codecForConstString("textArea")) - .property("properties", codecForUIFormFieldBaseConfig()) - .build("UIFormFieldConfigTextArea"); + .build("UIFormFieldTextArea"); -const codecForUiFormFieldToggle = (): Codec<UIFormFieldConfigToggle> => - buildCodecForObject<UIFormFieldConfigToggle>() +const codecForUiFormFieldToggle = (): Codec<UIFormFieldToggle> => + codecForUIFormFieldBaseConfigTemplate<UIFormFieldToggle>() .property("type", codecForConstString("toggle")) - .property("properties", codecForUIFormFieldBaseConfig()) - .build("UIFormFieldConfigToggle"); + .build("UIFormFieldToggle"); -const codecForUiFormField = (): Codec<UIFormFieldConfig> => - buildCodecForUnion<UIFormFieldConfig>() +const codecForUiFormField = (): Codec<UIFormElementConfig> => + buildCodecForUnion<UIFormElementConfig>() .discriminateOn("type") .alternative("array", codecForLazy(codecForUiFormFieldArray)) .alternative("group", codecForLazy(codecForUiFormFieldGroup)) - .alternative("absoluteTime", codecForUiFormFieldAbsoluteTime()) + .alternative("absoluteTimeText", codecForUiFormFieldAbsoluteTime()) .alternative("amount", codecForUiFormFieldAmount()) .alternative("caption", codecForUiFormFieldCaption()) .alternative("choiceHorizontal", codecForUiFormFieldChoiceHorizontal()) @@ -420,18 +330,18 @@ const codecForDoubleColumnForm = (): Codec<DoubleColumnForm> => .property("design", codecForList(codecForDoubleColumnFormSection())) .build("DoubleColumnForm"); -const codecForFlexibleForm = (): Codec<FlexibleForm> => - buildCodecForUnion<FlexibleForm>() +const codecForFormConfiguration = (): Codec<FormConfiguration> => + buildCodecForUnion<FormConfiguration>() .discriminateOn("type") .alternative("double-column", codecForDoubleColumnForm()) - .build<FlexibleForm>("FlexibleForm"); + .build<FormConfiguration>("FormConfiguration"); const codecForFormMetadata = (): Codec<FormMetadata> => buildCodecForObject<FormMetadata>() .property("label", codecForString()) .property("id", codecForString()) .property("version", codecForNumber()) - .property("config", codecForFlexibleForm()) + .property("config", codecForFormConfiguration()) .build("FormMetadata"); export const codecForUIForms = (): Codec<UiForms> => @@ -443,7 +353,7 @@ export type FormMetadata = { label: string; id: string; version: number; - config: FlexibleForm; + config: FormConfiguration; }; export interface UiForms {