summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rwxr-xr-xpackages/aml-backoffice-ui/build.mjs2
-rw-r--r--packages/aml-backoffice-ui/package.json2
-rw-r--r--packages/aml-backoffice-ui/src/forms.json886
-rw-r--r--packages/aml-backoffice-ui/src/forms/simplest.ts60
-rw-r--r--packages/aml-backoffice-ui/src/hooks/form.ts28
-rw-r--r--packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx1
-rw-r--r--packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx75
-rw-r--r--packages/anastasis-cli/package.json2
-rw-r--r--packages/anastasis-core/package.json2
-rw-r--r--packages/anastasis-core/src/index.ts33
-rw-r--r--packages/anastasis-webui/package.json2
-rw-r--r--packages/auditor-backoffice-ui/package.json2
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx107
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx2
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx107
-rw-r--r--packages/bank-ui/package.json2
-rw-r--r--packages/bank-ui/src/hooks/preferences.ts11
-rw-r--r--packages/bank-ui/src/i18n/es.po2
-rw-r--r--packages/bank-ui/src/i18n/ru.po (renamed from packages/bank-ui/src/i18n/en.po)766
-rw-r--r--packages/bank-ui/src/pages/OperationState/state.ts24
-rw-r--r--packages/bank-ui/src/pages/PaytoWireTransferForm.tsx57
-rw-r--r--packages/bank-ui/src/pages/QrCodeSection.tsx8
-rw-r--r--packages/bank-ui/src/pages/WalletWithdrawForm.tsx31
-rw-r--r--packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx23
-rw-r--r--packages/bank-ui/src/pages/account/ShowAccountDetails.tsx41
-rw-r--r--packages/bank-ui/src/settings.json2
-rw-r--r--packages/bank-ui/src/settings.ts16
-rw-r--r--packages/challenger-ui/package.json2
-rw-r--r--packages/idb-bridge/package.json4
-rw-r--r--packages/merchant-backend-ui/package.json2
-rw-r--r--packages/merchant-backoffice-ui/package.json2
-rw-r--r--packages/merchant-backoffice-ui/src/Application.tsx5
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx79
-rw-r--r--packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx16
-rw-r--r--packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx4
-rw-r--r--packages/merchant-backoffice-ui/src/components/modal/index.tsx89
-rw-r--r--packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx4
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/templates.ts3
-rw-r--r--packages/merchant-backoffice-ui/src/index.html1
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx30
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx126
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx11
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx36
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx2
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx2
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx3
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx35
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx2
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx15
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx5
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx45
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx25
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx2
-rw-r--r--packages/pogen/package.json2
-rw-r--r--packages/taler-harness/debian/changelog24
-rw-r--r--packages/taler-harness/package.json2
-rw-r--r--packages/taler-harness/src/harness/harness.ts37
-rw-r--r--packages/taler-harness/src/harness/helpers.ts262
-rw-r--r--packages/taler-harness/src/index.ts6
-rw-r--r--packages/taler-harness/src/integrationtests/test-age-restrictions-deposit.ts12
-rw-r--r--packages/taler-harness/src/integrationtests/test-age-restrictions-merchant.ts36
-rw-r--r--packages/taler-harness/src/integrationtests/test-age-restrictions-mixed-merchant.ts28
-rw-r--r--packages/taler-harness/src/integrationtests/test-age-restrictions-peer.ts11
-rw-r--r--packages/taler-harness/src/integrationtests/test-bank-api.ts42
-rw-r--r--packages/taler-harness/src/integrationtests/test-claim-loop.ts14
-rw-r--r--packages/taler-harness/src/integrationtests/test-clause-schnorr.ts19
-rw-r--r--packages/taler-harness/src/integrationtests/test-currency-scope.ts67
-rw-r--r--packages/taler-harness/src/integrationtests/test-denom-lost.ts12
-rw-r--r--packages/taler-harness/src/integrationtests/test-denom-unoffered.ts19
-rw-r--r--packages/taler-harness/src/integrationtests/test-deposit.ts12
-rw-r--r--packages/taler-harness/src/integrationtests/test-exchange-management-fault.ts32
-rw-r--r--packages/taler-harness/src/integrationtests/test-exchange-management.ts4
-rw-r--r--packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts42
-rw-r--r--packages/taler-harness/src/integrationtests/test-fee-regression.ts63
-rw-r--r--packages/taler-harness/src/integrationtests/test-kyc.ts57
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-bank.ts8
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts54
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-longpolling.ts12
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-refund-api.ts17
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-spec-public-orders.ts28
-rw-r--r--packages/taler-harness/src/integrationtests/test-multiexchange.ts69
-rw-r--r--packages/taler-harness/src/integrationtests/test-otp.ts14
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-claim.ts12
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-deleted.ts12
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-expired.ts16
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-fault.ts51
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-forgettable.ts17
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-idempotency.ts12
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-multiple.ts48
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-share.ts19
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-template.ts40
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-zero.ts10
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment.ts24
-rw-r--r--packages/taler-harness/src/integrationtests/test-paywall-flow.ts12
-rw-r--r--packages/taler-harness/src/integrationtests/test-peer-repair.ts14
-rw-r--r--packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts24
-rw-r--r--packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts10
-rw-r--r--packages/taler-harness/src/integrationtests/test-refund-auto.ts178
-rw-r--r--packages/taler-harness/src/integrationtests/test-refund-gone.ts12
-rw-r--r--packages/taler-harness/src/integrationtests/test-refund-incremental.ts12
-rw-r--r--packages/taler-harness/src/integrationtests/test-refund.ts12
-rw-r--r--packages/taler-harness/src/integrationtests/test-revocation.ts67
-rw-r--r--packages/taler-harness/src/integrationtests/test-simple-payment.ts3
-rw-r--r--packages/taler-harness/src/integrationtests/test-stored-backups.ts3
-rw-r--r--packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts91
-rw-r--r--packages/taler-harness/src/integrationtests/test-timetravel-withdraw.ts16
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-backup-basic.ts20
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-backup-doublespend.ts16
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-balance-notifications.ts13
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-balance-zero.ts12
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-balance.ts15
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-blocked-deposit.ts12
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-cli-termination.ts4
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-dd48.ts45
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-denom-expire.ts80
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-exchange-update.ts42
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-gendb.ts26
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-insufficient-balance.ts83
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-notifications.ts49
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-observability.ts46
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-refresh.ts19
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-wirefees.ts50
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallettesting.ts4
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-abort-bank.ts19
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-amount.ts94
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts42
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-conversion.ts53
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts66
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-flex.ts70
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-handover.ts51
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-huge.ts30
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-manual.ts16
-rw-r--r--packages/taler-harness/src/integrationtests/testrunner.ts4
-rw-r--r--packages/taler-util/package.json2
-rw-r--r--packages/taler-util/src/CancellationToken.ts2
-rw-r--r--packages/taler-util/src/MerchantApiClient.ts21
-rw-r--r--packages/taler-util/src/bank-api-client.ts22
-rw-r--r--packages/taler-util/src/codec.ts13
-rw-r--r--packages/taler-util/src/errors.ts5
-rw-r--r--packages/taler-util/src/http-client/bank-integration.ts8
-rw-r--r--packages/taler-util/src/http-client/bank-revenue.ts4
-rw-r--r--packages/taler-util/src/http-client/merchant.ts31
-rw-r--r--packages/taler-util/src/http-client/types.ts351
-rw-r--r--packages/taler-util/src/http-impl.qtart.ts6
-rw-r--r--packages/taler-util/src/index.ts9
-rw-r--r--packages/taler-util/src/invariants.ts2
-rw-r--r--packages/taler-util/src/merchant-api-types.ts352
-rw-r--r--packages/taler-util/src/notifications.ts2
-rw-r--r--packages/taler-util/src/payto.ts20
-rw-r--r--packages/taler-util/src/qtart.ts5
-rw-r--r--packages/taler-util/src/taler-crypto.ts17
-rw-r--r--packages/taler-util/src/taler-error-codes.ts148
-rw-r--r--packages/taler-util/src/taler-types.ts10
-rw-r--r--packages/taler-util/src/taleruri.test.ts63
-rw-r--r--packages/taler-util/src/taleruri.ts23
-rw-r--r--packages/taler-util/src/transactions-types.ts7
-rw-r--r--packages/taler-util/src/wallet-types.ts108
-rw-r--r--packages/taler-wallet-cli/debian/changelog24
-rw-r--r--packages/taler-wallet-cli/package.json2
-rw-r--r--packages/taler-wallet-cli/src/index.ts268
-rw-r--r--packages/taler-wallet-core/package.json2
-rw-r--r--packages/taler-wallet-core/src/backup/index.ts16
-rw-r--r--packages/taler-wallet-core/src/balance.ts85
-rw-r--r--packages/taler-wallet-core/src/coinSelection.ts5
-rw-r--r--packages/taler-wallet-core/src/common.ts92
-rw-r--r--packages/taler-wallet-core/src/crypto/cryptoImplementation.ts35
-rw-r--r--packages/taler-wallet-core/src/db.ts144
-rw-r--r--packages/taler-wallet-core/src/dbless.ts6
-rw-r--r--packages/taler-wallet-core/src/deposits.ts28
-rw-r--r--packages/taler-wallet-core/src/exchanges.ts350
-rw-r--r--packages/taler-wallet-core/src/instructedAmountConversion.ts22
-rw-r--r--packages/taler-wallet-core/src/pay-merchant.ts212
-rw-r--r--packages/taler-wallet-core/src/pay-peer-common.ts6
-rw-r--r--packages/taler-wallet-core/src/pay-peer-pull-credit.ts39
-rw-r--r--packages/taler-wallet-core/src/pay-peer-pull-debit.ts2
-rw-r--r--packages/taler-wallet-core/src/pay-peer-push-credit.ts29
-rw-r--r--packages/taler-wallet-core/src/pay-peer-push-debit.ts35
-rw-r--r--packages/taler-wallet-core/src/recoup.ts4
-rw-r--r--packages/taler-wallet-core/src/refresh.ts143
-rw-r--r--packages/taler-wallet-core/src/shepherd.ts70
-rw-r--r--packages/taler-wallet-core/src/transactions.ts274
-rw-r--r--packages/taler-wallet-core/src/versions.ts9
-rw-r--r--packages/taler-wallet-core/src/wallet-api-types.ts39
-rw-r--r--packages/taler-wallet-core/src/wallet.ts135
-rw-r--r--packages/taler-wallet-core/src/withdraw.ts888
-rw-r--r--packages/taler-wallet-embedded/package.json2
-rw-r--r--packages/taler-wallet-embedded/src/wallet-qjs-tests.ts118
-rw-r--r--packages/taler-wallet-embedded/src/wallet-qjs.ts212
-rw-r--r--packages/taler-wallet-webextension/manifest-common.json4
-rw-r--r--packages/taler-wallet-webextension/package.json2
-rw-r--r--packages/taler-wallet-webextension/src/components/HistoryItem.tsx4
-rw-r--r--packages/taler-wallet-webextension/src/components/Part.tsx10
-rw-r--r--packages/taler-wallet-webextension/src/components/WalletActivity.tsx9
-rw-r--r--packages/taler-wallet-webextension/src/components/styled/index.tsx10
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx21
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/views.tsx48
-rw-r--r--packages/taler-wallet-webextension/src/cta/PaymentTemplate/index.ts2
-rw-r--r--packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts183
-rw-r--r--packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx32
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferPickup/views.tsx22
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/index.ts23
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/state.ts289
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx178
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/test.ts79
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx180
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useIsOnline.ts20
-rw-r--r--packages/taler-wallet-webextension/src/i18n/ru.po1977
-rw-r--r--packages/taler-wallet-webextension/src/platform/chrome.ts30
-rw-r--r--packages/taler-wallet-webextension/src/platform/dev.ts20
-rw-r--r--packages/taler-wallet-webextension/src/popup/BalancePage.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Application.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts2
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts68
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx15
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts57
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx13
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx134
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx1
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Transaction.tsx12
-rw-r--r--packages/taler-wallet-webextension/src/wxApi.ts2
-rw-r--r--packages/taler-wallet-webextension/src/wxBackend.ts150
-rw-r--r--packages/web-util/package.json2
-rw-r--r--packages/web-util/src/components/CopyButton.tsx2
-rw-r--r--packages/web-util/src/forms/InputAbsoluteTime.stories.tsx2
-rw-r--r--packages/web-util/src/forms/InputAmount.tsx29
-rw-r--r--packages/web-util/src/forms/InputArray.tsx28
-rw-r--r--packages/web-util/src/forms/InputChoiceHorizontal.tsx5
-rw-r--r--packages/web-util/src/forms/converter.ts21
-rw-r--r--packages/web-util/src/forms/forms.ts93
-rw-r--r--packages/web-util/src/forms/ui-form.ts382
-rw-r--r--packages/web-util/src/index.build.ts6
231 files changed, 9324 insertions, 4919 deletions
diff --git a/packages/aml-backoffice-ui/build.mjs b/packages/aml-backoffice-ui/build.mjs
index 04a6f646b..b0742c692 100755
--- 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/package.json b/packages/aml-backoffice-ui/package.json
index 749565946..9c33862f7 100644
--- a/packages/aml-backoffice-ui/package.json
+++ b/packages/aml-backoffice-ui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/aml-backoffice-ui",
- "version": "0.10.7",
+ "version": "0.11.4",
"author": "sebasjm",
"license": "AGPL-3.0-OR-LATER",
"description": "Back-office SPA for GNU Taler Exchange.",
diff --git a/packages/aml-backoffice-ui/src/forms.json b/packages/aml-backoffice-ui/src/forms.json
index ed556307b..94dcda317 100644
--- 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
index 37ab0913d..4cd781b74 100644
--- 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
index e9194d86d..70b2db571 100644
--- 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
index 712a1fed9..7801625d0 100644
--- 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
index 3c0301e9f..cdc5d0bc1 100644
--- 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/anastasis-cli/package.json b/packages/anastasis-cli/package.json
index 5a9d6abea..40bdb927e 100644
--- a/packages/anastasis-cli/package.json
+++ b/packages/anastasis-cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/anastasis-cli",
- "version": "0.10.7",
+ "version": "0.11.4",
"description": "",
"engines": {
"node": ">=0.18.0"
diff --git a/packages/anastasis-core/package.json b/packages/anastasis-core/package.json
index 576acc988..c987f0ceb 100644
--- a/packages/anastasis-core/package.json
+++ b/packages/anastasis-core/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/anastasis-core",
- "version": "0.10.7",
+ "version": "0.11.4",
"description": "",
"main": "./lib/index.js",
"module": "./lib/index.js",
diff --git a/packages/anastasis-core/src/index.ts b/packages/anastasis-core/src/index.ts
index 05fa4a49f..a48db5c25 100644
--- a/packages/anastasis-core/src/index.ts
+++ b/packages/anastasis-core/src/index.ts
@@ -43,7 +43,7 @@ import {
URL,
j2s,
} from "@gnu-taler/taler-util";
-import { HttpResponse } from "@gnu-taler/taler-util/http";
+import { HttpResponse, createPlatformHttpLib } from "@gnu-taler/taler-util/http";
import { anastasisData } from "./anastasis-data.js";
import {
codecForChallengeInstructionMessage,
@@ -139,6 +139,11 @@ export * from "./challenge-feedback-types.js";
const logger = new Logger("anastasis-core:index.ts");
+const http = createPlatformHttpLib({
+ enableThrottling: true,
+ requireTls: false,
+});
+
const ANASTASIS_HTTP_HEADER_POLICY_META_DATA = "Anastasis-Policy-Meta-Data";
function getContinents(): ContinentInfo[] {
@@ -279,9 +284,9 @@ async function getProviderInfo(
providerBaseUrl: string,
): Promise<AuthenticationProviderStatus> {
// FIXME: Use a reasonable timeout here.
- let resp: Response;
+ let resp: HttpResponse;
try {
- resp = await fetch(new URL("config", providerBaseUrl).href);
+ resp = await http.fetch(new URL("config", providerBaseUrl).href);
} catch (e) {
console.warn(
"Encountered an HTTP error whilst trying to get the provider's config: ",
@@ -293,7 +298,7 @@ async function getProviderInfo(
hint: "request to anastasis provider failed",
};
}
- if (!resp.ok) {
+ if (resp.status < 200 || resp.status > 299) {
console.warn("Got bad response code whilst getting provider config", resp);
return {
status: "error",
@@ -556,7 +561,7 @@ async function uploadSecret(
// FIXME: Get this from the params
reqUrl.searchParams.set("timeout_ms", "500");
}
- const resp = await fetch(reqUrl.href, {
+ const resp = await http.fetch(reqUrl.href, {
method: "POST",
headers: {
"content-type": "application/json",
@@ -646,7 +651,7 @@ async function uploadSecret(
reqUrl.searchParams.set("timeout_ms", "500");
}
logger.info(`uploading policy to ${prov.provider_url}`);
- const resp = await fetch(reqUrl.href, {
+ const resp = await http.fetch(reqUrl.href, {
method: "POST",
headers: {
"Anastasis-Policy-Signature": encodeCrock(sig),
@@ -757,14 +762,14 @@ async function downloadPolicyFromProvider(
const acctKeypair = accountKeypairDerive(userId);
const reqUrl = new URL(`policy/${acctKeypair.pub}`, providerUrl);
reqUrl.searchParams.set("version", `${version}`);
- const resp = await fetch(reqUrl.href);
+ const resp = await http.fetch(reqUrl.href);
if (resp.status !== 200) {
logger.info(
`Could not download policy from provider ${providerUrl}, status ${resp.status}`,
);
return undefined;
}
- const body = await resp.arrayBuffer();
+ const body = await resp.bytes();
const bodyDecrypted = await decryptRecoveryDocument(
userId,
encodeCrock(body),
@@ -981,10 +986,10 @@ async function requestTruth(
const hresp = await getResponseHash(truth, solveRequest);
- let resp: Response;
+ let resp: HttpResponse;
try {
- resp = await fetch(url.href, {
+ resp = await http.fetch(url.href, {
method: "POST",
headers: {
Accept: "application/json",
@@ -1022,7 +1027,7 @@ async function requestTruth(
truth.provider_salt,
);
- const respBody = new Uint8Array(await resp.arrayBuffer());
+ const respBody = new Uint8Array(await resp.bytes());
const keyShare = await decryptKeyShare(
encodeCrock(respBody),
userId,
@@ -1138,10 +1143,10 @@ async function selectChallenge(
}
}
- let resp: Response;
+ let resp: HttpResponse;
try {
- resp = await fetch(url.href, {
+ resp = await http.fetch(url.href, {
method: "POST",
headers: {
Accept: "application/json",
@@ -1859,7 +1864,7 @@ export async function discoverPolicies(
);
const acctKeypair = accountKeypairDerive(userId);
const reqUrl = new URL(`policy/${acctKeypair.pub}/meta`, providerUrl);
- const resp = await fetch(reqUrl.href);
+ const resp = await http.fetch(reqUrl.href);
if (resp.status !== 200) {
logger.warn(`Could not fetch policy metadate from ${reqUrl.href}`);
continue;
diff --git a/packages/anastasis-webui/package.json b/packages/anastasis-webui/package.json
index 108b1476e..9f56489d1 100644
--- a/packages/anastasis-webui/package.json
+++ b/packages/anastasis-webui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/anastasis-webui",
- "version": "0.10.7",
+ "version": "0.11.4",
"license": "MIT",
"type": "module",
"scripts": {
diff --git a/packages/auditor-backoffice-ui/package.json b/packages/auditor-backoffice-ui/package.json
index 776c179b4..ce420417c 100644
--- a/packages/auditor-backoffice-ui/package.json
+++ b/packages/auditor-backoffice-ui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/auditor-backoffice-ui",
- "version": "0.10.7",
+ "version": "0.11.4",
"license": "AGPL-3.0-or-later",
"type": "module",
"scripts": {
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx
index 947f3572c..502cfea08 100644
--- 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
index 5140aae3a..f2276b0c4 100644
--- 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
index b578d4664..2b73536fb 100644
--- 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/package.json b/packages/bank-ui/package.json
index f06905a93..db89e58be 100644
--- a/packages/bank-ui/package.json
+++ b/packages/bank-ui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/bank-ui",
- "version": "0.10.7",
+ "version": "0.11.4",
"license": "AGPL-3.0-OR-LATER",
"type": "module",
"scripts": {
diff --git a/packages/bank-ui/src/hooks/preferences.ts b/packages/bank-ui/src/hooks/preferences.ts
index bb3dcb153..4cb5e6a95 100644
--- a/packages/bank-ui/src/hooks/preferences.ts
+++ b/packages/bank-ui/src/hooks/preferences.ts
@@ -31,8 +31,6 @@ interface Preferences {
showWithdrawalSuccess: boolean;
showDemoDescription: boolean;
showInstallWallet: boolean;
- maxWithdrawalAmount: number;
- fastWithdrawal: boolean;
showDebugInfo: boolean;
}
@@ -41,17 +39,13 @@ export const codecForPreferences = (): Codec<Preferences> =>
.property("showWithdrawalSuccess", codecForBoolean())
.property("showDemoDescription", codecForBoolean())
.property("showInstallWallet", codecForBoolean())
- .property("fastWithdrawal", codecForBoolean())
.property("showDebugInfo", codecForBoolean())
- .property("maxWithdrawalAmount", codecForNumber())
.build("Settings");
const defaultPreferences: Preferences = {
showWithdrawalSuccess: true,
showDemoDescription: true,
showInstallWallet: true,
- maxWithdrawalAmount: 25,
- fastWithdrawal: false,
showDebugInfo: false,
};
@@ -82,7 +76,6 @@ export function usePreferences(): [
export function getAllBooleanPreferences(): Array<keyof Preferences> {
return [
- "fastWithdrawal",
"showDebugInfo",
"showDemoDescription",
"showInstallWallet",
@@ -95,16 +88,12 @@ export function getLabelForPreferences(
i18n: ReturnType<typeof useTranslationContext>["i18n"],
): TranslatedString {
switch (k) {
- case "maxWithdrawalAmount":
- return i18n.str`Max withdrawal amount`;
case "showWithdrawalSuccess":
return i18n.str`Show withdrawal confirmation`;
case "showDemoDescription":
return i18n.str`Show demo description`;
case "showInstallWallet":
return i18n.str`Show install wallet first`;
- case "fastWithdrawal":
- return i18n.str`Use fast withdrawal form`;
case "showDebugInfo":
return i18n.str`Show debug info`;
}
diff --git a/packages/bank-ui/src/i18n/es.po b/packages/bank-ui/src/i18n/es.po
index fb69822c5..39527f1dd 100644
--- 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/en.po b/packages/bank-ui/src/i18n/ru.po
index a9657bd32..8cd1eec53 100644
--- a/packages/bank-ui/src/i18n/en.po
+++ b/packages/bank-ui/src/i18n/ru.po
@@ -1,395 +1,406 @@
-# 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/>
+# 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 Wallet\n"
+"Project-Id-Version: Taler Bank\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"
+"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=2; plural=(n != 1);\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 ""
+msgstr "Не удалось выполнить операцию, сообщите об этом"
#: src/utils.ts:156
#, c-format
msgid "Request timeout"
-msgstr ""
+msgstr "Тайм-аут запроса"
#: src/utils.ts:165
#, c-format
msgid "Request throttled"
-msgstr ""
+msgstr "Запрос замедлен"
#: src/utils.ts:174
#, c-format
msgid "Malformed response"
-msgstr ""
+msgstr "Неправильный ответ"
#: src/utils.ts:183
#, c-format
msgid "Network error"
-msgstr ""
+msgstr "Ошибка сети"
#: src/utils.ts:192
#, c-format
msgid "Unexpected request error"
-msgstr ""
+msgstr "Неожиданная ошибка запроса"
#: src/utils.ts:201
#, c-format
msgid "Unexpected error"
-msgstr ""
+msgstr "Непредвиденная ошибка"
#: src/utils.ts:377
#, c-format
msgid "IBAN numbers usually have more that 4 digits"
-msgstr ""
+msgstr "Номера IBAN обычно содержат более 4 цифр"
#: src/utils.ts:379
#, c-format
msgid "IBAN numbers usually have less that 34 digits"
-msgstr ""
+msgstr "Номера IBAN обычно содержат менее 34 цифр"
#: src/utils.ts:387
#, c-format
msgid "IBAN country code not found"
-msgstr ""
+msgstr "Код страны IBAN не найден"
#: src/utils.ts:401
#, c-format
msgid "IBAN number is not valid, checksum is wrong"
-msgstr ""
+msgstr "Номер IBAN недействителен, контрольная сумма неверна"
#: src/context/config.ts:136
#, c-format
msgid ""
-"the bank backend is not supported. supported version \"%1$s\", server "
-"version \"%2$s\""
+"the bank backend is not supported. supported version \"%1$s\", server version "
+"\"%2$s\""
msgstr ""
+"Бэкенд банка не поддерживается. Поддерживаемая версия \"%1$s\" а версия "
+"сервера \"%2$s\""
#: src/hooks/preferences.ts:55
-#, fuzzy, c-format
+#, c-format
msgid "Max withdrawal amount"
-msgstr ""
+msgstr "Максимальная сумма вывода"
#: src/hooks/preferences.ts:57
#, c-format
msgid "Show withdrawal confirmation"
-msgstr ""
+msgstr "Показать подтверждение вывода средств"
#: src/hooks/preferences.ts:59
#, c-format
msgid "Show demo description"
-msgstr ""
+msgstr "Показать описание демо"
#: src/hooks/preferences.ts:61
#, c-format
msgid "Show install wallet first"
-msgstr ""
+msgstr "Сначала показать как установить кошелёк"
#: src/hooks/preferences.ts:63
-#, fuzzy, c-format
+#, c-format
msgid "Use fast withdrawal form"
-msgstr ""
+msgstr "Используйте форму быстрого вывода средств"
#: src/hooks/preferences.ts:65
#, c-format
msgid "Show debug info"
-msgstr ""
+msgstr "Показать информацию для отладки"
#: src/pages/PaytoWireTransferForm.tsx:90
#, c-format
msgid "required"
-msgstr ""
+msgstr "обязательно"
#: src/pages/PaytoWireTransferForm.tsx:92
#, c-format
msgid "IBAN should have just uppercased letters and numbers"
-msgstr ""
+msgstr "IBAN должен состоять только из прописных букв и цифр"
#: src/pages/PaytoWireTransferForm.tsx:98
#, c-format
msgid "not valid"
-msgstr ""
+msgstr "недопустимый"
#: src/pages/PaytoWireTransferForm.tsx:100
#, c-format
msgid "should be greater than 0"
-msgstr ""
+msgstr "должно быть больше 0"
#: src/pages/PaytoWireTransferForm.tsx:102
#, c-format
msgid "balance is not enough"
-msgstr ""
+msgstr "Недостаточно средств на балансе"
#: src/pages/PaytoWireTransferForm.tsx:112
#, c-format
msgid "does not follow the pattern"
-msgstr ""
+msgstr "не следует шаблону"
#: src/pages/PaytoWireTransferForm.tsx:114
#, c-format
msgid "only \"IBAN\" target are supported"
-msgstr ""
+msgstr "поддерживаются только \"IBAN\""
#: src/pages/PaytoWireTransferForm.tsx:116
#, c-format
msgid "use the \"amount\" parameter to specify the amount to be transferred"
-msgstr ""
+msgstr "Используйте параметр \"Сумма\" для указания суммы перевода"
#: src/pages/PaytoWireTransferForm.tsx:118
#, c-format
msgid "the amount is not valid"
-msgstr ""
+msgstr "сумма не является действительной"
#: src/pages/PaytoWireTransferForm.tsx:120
#, c-format
-msgid ""
-"use the \"message\" parameter to specify a reference text for the transfer"
-msgstr ""
+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 ""
+msgstr "Не хватает разрешения для завершения операции."
#: src/pages/PaytoWireTransferForm.tsx:174
#, c-format
msgid "The destination account \"%1$s\" was not found."
-msgstr ""
+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 ""
+msgstr "Пункт отправления и пункт назначения перевода не могут совпадать."
#: src/pages/PaytoWireTransferForm.tsx:188
#, c-format
msgid "Your balance is not enough."
-msgstr ""
+msgstr "Вашего баланса недостаточно."
#: src/pages/PaytoWireTransferForm.tsx:195
#, c-format
msgid "The origin account \"%1$s\" was not found."
-msgstr ""
+msgstr "Исходный аккаунт \"%1$s\" не найден."
#: src/pages/PaytoWireTransferForm.tsx:212
#, c-format
msgid "Wire transfer created!"
-msgstr ""
+msgstr "Банковский перевод создан!"
#: src/pages/PaytoWireTransferForm.tsx:270
#, c-format
msgid "Using a form"
-msgstr ""
+msgstr "Используя форму"
#: src/pages/PaytoWireTransferForm.tsx:310
#, c-format
msgid "Import payto:// URI"
-msgstr ""
+msgstr "Импорт payto:// URI"
#: src/pages/PaytoWireTransferForm.tsx:335
#, c-format
msgid "Recipient"
-msgstr ""
+msgstr "Получатель"
#: src/pages/PaytoWireTransferForm.tsx:359
#, c-format
msgid "IBAN of the recipient's account"
-msgstr ""
+msgstr "IBAN счета получателя"
#: src/pages/PaytoWireTransferForm.tsx:369
#, c-format
msgid "Transfer subject"
-msgstr ""
+msgstr "Причина перевода"
#: src/pages/PaytoWireTransferForm.tsx:377
#, c-format
msgid "subject"
-msgstr ""
+msgstr "причина"
#: src/pages/PaytoWireTransferForm.tsx:390
#, c-format
msgid "some text to identify the transfer"
-msgstr ""
+msgstr "какой-то текст для идентификации перевода"
#: src/pages/PaytoWireTransferForm.tsx:400
#, c-format
msgid "Amount"
-msgstr ""
+msgstr "Сумма"
#: src/pages/PaytoWireTransferForm.tsx:415
-#, fuzzy, c-format
+#, c-format
msgid "amount to transfer"
-msgstr ""
+msgstr "сумма для перевода"
#: src/pages/PaytoWireTransferForm.tsx:425
#, c-format
msgid "payto URI:"
-msgstr ""
+msgstr "payto URI:"
#: src/pages/PaytoWireTransferForm.tsx:436
#, c-format
msgid "uniform resource identifier of the target account"
-msgstr ""
+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 ""
+msgstr "Отмена"
#: src/pages/PaytoWireTransferForm.tsx:471
#, c-format
+msgctxt "wire_transfer"
msgid "Send"
-msgstr ""
+msgstr "Отправить"
#: src/pages/LoginForm.tsx:71
#, c-format
msgid "Missing username"
-msgstr ""
+msgstr "Отсутствует имя пользователя"
#: src/pages/LoginForm.tsx:75
#, c-format
msgid "Missing password"
-msgstr ""
+msgstr "Отсутствует пароль"
#: src/pages/LoginForm.tsx:104
#, c-format
msgid "Wrong credentials for \"%1$s\""
-msgstr ""
+msgstr "Неверные учетные данные для «%1$s» ‎"
#: src/pages/LoginForm.tsx:111
#, c-format
msgid "Account not found"
-msgstr ""
+msgstr "Учётная запись не найдена"
#: src/pages/LoginForm.tsx:142
#, c-format
msgid "Username"
-msgstr ""
+msgstr "Имя пользователя"
#: src/pages/LoginForm.tsx:156
#, c-format
msgid "username of the account"
-msgstr ""
+msgstr "имя пользователя счёта"
#: src/pages/LoginForm.tsx:175
#, c-format
msgid "Password"
-msgstr ""
+msgstr "Пароль"
#: src/pages/LoginForm.tsx:188
#, c-format
msgid "password of the account"
-msgstr ""
+msgstr "пароль от счёта"
#: src/pages/LoginForm.tsx:223
#, c-format
msgid "Check"
-msgstr ""
+msgstr "Проверить"
#: src/pages/LoginForm.tsx:237
#, c-format
msgid "Log in"
-msgstr ""
+msgstr "Войти"
#: src/pages/LoginForm.tsx:249
#, c-format
msgid "Register"
-msgstr ""
+msgstr "Регистрация"
#: src/components/Transactions/views.tsx:52
#, c-format
msgid "Latest transactions"
-msgstr ""
+msgstr "Последние транзакции"
#: src/components/Transactions/views.tsx:63
#, c-format
msgid "Date"
-msgstr ""
+msgstr "Дата"
#: src/components/Transactions/views.tsx:71
#, c-format
msgid "Counterpart"
-msgstr ""
+msgstr "Контрагент"
#: src/components/Transactions/views.tsx:75
#, c-format
msgid "Subject"
-msgstr ""
+msgstr "Причина"
#: src/components/Transactions/views.tsx:111
#, c-format
msgid "sent"
-msgstr ""
+msgstr "отправлено"
#: src/components/Transactions/views.tsx:112
#, c-format
msgid "received"
-msgstr ""
+msgstr "получено"
#: src/components/Transactions/views.tsx:127
#, c-format
msgid "invalid value"
-msgstr ""
+msgstr "Недопустимое значение"
#: src/components/Transactions/views.tsx:136
#, c-format
msgid "to"
-msgstr ""
+msgstr "к"
#: src/components/Transactions/views.tsx:136
#, c-format
msgid "from"
-msgstr ""
+msgstr "от"
#: src/components/Transactions/views.tsx:202
#, c-format
msgid "First page"
-msgstr ""
+msgstr "Первая страница"
#: src/components/Transactions/views.tsx:209
#, c-format
msgid "Next"
-msgstr ""
+msgstr "Далее"
#: src/pages/WithdrawalConfirmationQuestion.tsx:86
#, c-format
msgid "Wire transfer completed!"
-msgstr ""
+msgstr "Отправка перевода завершена!"
#: src/pages/WithdrawalConfirmationQuestion.tsx:93
#, c-format
msgid "The withdrawal has been aborted previously and can't be confirmed"
-msgstr ""
+msgstr "Вывод средств был прерван ранее и не может быть подтвержден"
#: src/pages/WithdrawalConfirmationQuestion.tsx:100
#, c-format
@@ -397,62 +408,63 @@ 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 ""
+msgstr "Идентификатор операции недействителен."
#: src/pages/WithdrawalConfirmationQuestion.tsx:114
#, c-format
msgid "The operation was not found."
-msgstr ""
+msgstr "Операция не найдена."
#: src/pages/WithdrawalConfirmationQuestion.tsx:121
#, c-format
msgid "Your balance is not enough for the operation."
-msgstr ""
+msgstr "Вашего баланса недостаточно для проведения операции."
#: src/pages/WithdrawalConfirmationQuestion.tsx:155
#, c-format
-msgid ""
-"The reserve operation has been confirmed previously and can't be aborted"
-msgstr ""
+msgid "The reserve operation has been confirmed previously and can't be aborted"
+msgstr "Резервная операция была подтверждена ранее и не может быть прервана"
#: src/pages/WithdrawalConfirmationQuestion.tsx:186
-#, fuzzy, c-format
+#, c-format
msgid "Confirm the withdrawal operation"
-msgstr ""
+msgstr "Подтвердите операцию вывода"
#: src/pages/WithdrawalConfirmationQuestion.tsx:203
#, c-format
msgid "Wire transfer details"
-msgstr ""
+msgstr "Детали банковского перевода"
#: src/pages/WithdrawalConfirmationQuestion.tsx:217
#, c-format
msgid "Taler Exchange operator's account"
-msgstr ""
+msgstr "Счет оператора Обменника Taler"
#: src/pages/WithdrawalConfirmationQuestion.tsx:228
#, c-format
msgid "Taler Exchange operator's name"
-msgstr ""
+msgstr "Название оператора Обменника Taler"
#: src/pages/WithdrawalConfirmationQuestion.tsx:317
#, c-format
msgid "Transfer"
-msgstr ""
+msgstr "Перевести"
#: src/pages/WithdrawalConfirmationQuestion.tsx:342
#, c-format
msgid "Authentication required"
-msgstr ""
+msgstr "Требуется аутентификация"
#: src/pages/WithdrawalConfirmationQuestion.tsx:352
#, c-format
msgid "This operation was created with other username"
-msgstr ""
+msgstr "Эта операция была создана с другим именем пользователя"
#: src/pages/OperationState/views.tsx:209
#, c-format
@@ -460,16 +472,18 @@ 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 ""
+msgstr "Операция отклонена из-за нехватки средств."
#: src/pages/OperationState/views.tsx:268
#, c-format
msgid "Withdrawal confirmed"
-msgstr ""
+msgstr "Вывод подтверждён"
#: src/pages/OperationState/views.tsx:272
#, c-format
@@ -477,524 +491,544 @@ 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 ""
+msgstr "Не показывать снова"
#: src/pages/OperationState/views.tsx:319
#, c-format
msgid "Close"
-msgstr ""
+msgstr "Закрыть"
#: src/pages/OperationState/views.tsx:399
#, c-format
msgid "On this device"
-msgstr ""
+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."
+"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 ""
+msgstr "Старт"
#: src/pages/OperationState/views.tsx:426
#, c-format
msgid "On a mobile phone"
-msgstr ""
+msgstr "На мобильном телефоне"
#: src/pages/OperationState/views.tsx:431
#, c-format
msgid "Scan the QR code with your mobile device."
-msgstr ""
+msgstr "Отсканируйте QR-код с помощью мобильного устройства."
#: src/pages/WalletWithdrawForm.tsx:73
#, c-format
msgid "There is an operation already"
-msgstr ""
+msgstr "Операция уже идет"
#: src/pages/WalletWithdrawForm.tsx:75
-#, fuzzy, c-format
+#, c-format
msgid "Complete or cancel the operation in"
-msgstr ""
+msgstr "Завершите или отмените операцию в"
#: src/pages/WalletWithdrawForm.tsx:84
#, c-format
msgid "this page"
-msgstr ""
+msgstr "этой странице"
#: src/pages/WalletWithdrawForm.tsx:101
#, c-format
msgid "invalid"
-msgstr ""
+msgstr "недействительно"
#: src/pages/WalletWithdrawForm.tsx:116
#, c-format
msgid "Server responded with an invalid withdraw URI"
-msgstr ""
+msgstr "Сервер ответил с недопустимым URI вывода"
#: src/pages/WalletWithdrawForm.tsx:117
-#, fuzzy, c-format
+#, c-format
msgid "Withdraw URI: %1$s"
-msgstr ""
+msgstr "URI вывода: %1$s"
#: src/pages/WalletWithdrawForm.tsx:132
#, c-format
msgid "The operation was rejected due to insufficient funds"
-msgstr ""
+msgstr "Операция отклонена из-за нехватки средств."
#: src/pages/WalletWithdrawForm.tsx:253
#, c-format
msgid "Continue"
-msgstr ""
+msgstr "Продолжить"
#: src/pages/WalletWithdrawForm.tsx:282
#, c-format
msgid "Prepare your wallet"
-msgstr ""
+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."
+"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
+#, c-format
msgid "You need a GNU Taler Wallet"
-msgstr ""
+msgstr "Вам нужен кошелёк Taler"
#: src/pages/WalletWithdrawForm.tsx:300
#, c-format
msgid "If you don't have one yet you can follow the instruction in"
-msgstr ""
+msgstr "Если у вас его еще нет, вы можете следовать инструкциям на"
#: src/pages/PaymentOptions.tsx:55
#, c-format
msgid "Send money"
-msgstr ""
+msgstr "Отправить деньги"
#: src/pages/PaymentOptions.tsx:73
#, c-format
msgid "to a %1$s wallet"
-msgstr ""
+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 ""
+msgstr "операция готова"
#: src/pages/PaymentOptions.tsx:129
#, c-format
msgid "to another bank account"
-msgstr ""
+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
+#, c-format
msgid "Transfer details"
-msgstr ""
+msgstr "Подробности перевода"
#: src/pages/AccountPage/views.tsx:41
#, c-format
msgid "This is a demo bank"
-msgstr ""
+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."
+"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."
+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 ""
+msgstr "Ожидание операции удаления счёта"
#: src/pages/AccountPage/views.tsx:72
#, c-format
msgid "Pending account update operation"
-msgstr ""
+msgstr "Ожидание операции обновления счёта"
#: src/pages/AccountPage/views.tsx:74
#, c-format
msgid "Pending password update operation"
-msgstr ""
+msgstr "Ожидание операции обновления пароля"
#: src/pages/AccountPage/views.tsx:76
#, c-format
msgid "Pending transaction operation"
-msgstr ""
+msgstr "Ожидание операции транзакции"
#: src/pages/AccountPage/views.tsx:78
#, c-format
msgid "Pending withdrawal operation"
-msgstr ""
+msgstr "Ожидание операции вывода средств"
#: src/pages/AccountPage/views.tsx:80
#, c-format
msgid "Pending cashout operation"
-msgstr ""
+msgstr "Ожидание операции обналички"
#: src/pages/AccountPage/views.tsx:91
#, c-format
msgid "You can complete or cancel the operation in"
-msgstr ""
+msgstr "Завершить или отменить операцию можно в"
#: src/pages/BankFrame.tsx:64
#, c-format
msgid "Internal error, please report."
-msgstr ""
+msgstr "Внутренняя ошибка, пожалуйста, сообщите."
#: src/pages/BankFrame.tsx:100
#, c-format
msgid "Preferences"
-msgstr ""
+msgstr "Настройки"
#: src/pages/BankFrame.tsx:184
#, c-format
msgid "Welcome, %1$s"
-msgstr ""
+msgstr "Добро пожаловать, %1$s"
#: src/pages/WireTransfer.tsx:79
#, c-format
msgid "Make a wire transfer"
-msgstr ""
+msgstr "Сделать банковский перевод"
#: src/pages/admin/AccountList.tsx:72
#, c-format
msgid "Accounts"
-msgstr ""
+msgstr "Счета"
#: src/pages/admin/AccountList.tsx:75
#, c-format
msgid "A list of all business account in the bank."
-msgstr ""
+msgstr "Список всех бизнес-счетов в банке."
#: src/pages/admin/AccountList.tsx:86
#, c-format
msgid "Create account"
-msgstr ""
+msgstr "Создать учётную запись"
#: src/pages/admin/AccountList.tsx:106
#, c-format
msgid "Name"
-msgstr ""
+msgstr "Название"
#: src/pages/admin/AccountList.tsx:110
#, c-format
msgid "Balance"
-msgstr ""
+msgstr "Баланс"
#: src/pages/admin/AccountList.tsx:112
#, c-format
msgid "Actions"
-msgstr ""
+msgstr "Действия"
#: src/pages/admin/AccountList.tsx:151
#, c-format
msgid "unknown"
-msgstr ""
+msgstr "неизвестно"
#: src/pages/admin/AccountList.tsx:170
#, c-format
msgid "change password"
-msgstr ""
+msgstr "изменить пароль"
#: src/pages/admin/AccountList.tsx:179
#, c-format
msgid "cashouts"
-msgstr ""
+msgstr "выплаты"
#: src/pages/admin/AccountList.tsx:189
#, c-format
msgid "remove"
-msgstr ""
+msgstr "удалить"
#: src/pages/admin/AdminHome.tsx:168
#, c-format
msgid "Cashout not implemented"
-msgstr ""
+msgstr "Обналичка не реализована"
#: src/pages/admin/AdminHome.tsx:184
#, c-format
msgid "Select a section"
-msgstr ""
+msgstr "Выберите раздел"
#: src/pages/admin/AdminHome.tsx:202
#, c-format
msgid "Last hour"
-msgstr ""
+msgstr "Последний час"
#: src/pages/admin/AdminHome.tsx:208
#, c-format
msgid "Last day"
-msgstr ""
+msgstr "Последний день"
#: src/pages/admin/AdminHome.tsx:216
#, c-format
msgid "Last month"
-msgstr ""
+msgstr "Последний месяц"
#: src/pages/admin/AdminHome.tsx:222
#, c-format
msgid "Last year"
-msgstr ""
+msgstr "Последний год"
#: src/pages/admin/AdminHome.tsx:310
#, c-format
msgid "Last Year"
-msgstr ""
+msgstr "Прошлый год"
#: src/pages/admin/AdminHome.tsx:325
#, c-format
msgid "Trading volume on %1$s compared to %2$s"
-msgstr ""
+msgstr "Объем торгов на %1$s по сравнению с %2$s"
#: src/pages/admin/AdminHome.tsx:342
#, c-format
msgid "Cashin"
-msgstr ""
+msgstr "Внесения"
#: src/pages/admin/AdminHome.tsx:352
#, c-format
msgid "Cashout"
-msgstr ""
+msgstr "Выплата"
#: src/pages/admin/AdminHome.tsx:364
#, c-format
msgid "Payin"
-msgstr ""
+msgstr "Отплата"
#: src/pages/admin/AdminHome.tsx:374
#, c-format
msgid "Payout"
-msgstr ""
+msgstr "Выплата"
#: src/pages/admin/AdminHome.tsx:388
#, c-format
msgid "download stats as CSV"
-msgstr ""
+msgstr "скачать статистику в формате CSV"
#: src/pages/admin/AdminHome.tsx:494
#, c-format
msgid "Decreased by"
-msgstr ""
+msgstr "Уменьшилось на"
#: src/pages/admin/AdminHome.tsx:498
#, c-format
msgid "Increased by"
-msgstr ""
+msgstr "Увеличение на"
#: src/pages/DownloadStats.tsx:89
#, c-format
msgid "Download bank stats"
-msgstr ""
+msgstr "Скачивать статистику банка"
#: src/pages/DownloadStats.tsx:110
#, c-format
msgid "Include hour metric"
-msgstr ""
+msgstr "Включить часовую метрику"
#: src/pages/DownloadStats.tsx:143
#, c-format
msgid "Include day metric"
-msgstr ""
+msgstr "Включить дневную метрику"
#: src/pages/DownloadStats.tsx:173
#, c-format
msgid "Include month metric"
-msgstr ""
+msgstr "Включить месячную метрику"
#: src/pages/DownloadStats.tsx:206
#, c-format
msgid "Include year metric"
-msgstr ""
+msgstr "Включить годовую метрику"
#: src/pages/DownloadStats.tsx:239
#, c-format
msgid "Include table header"
-msgstr ""
+msgstr "Включить заголовок таблицы"
#: src/pages/DownloadStats.tsx:272
#, c-format
msgid "Add previous metric for compare"
-msgstr ""
+msgstr "Добавить предыдущую метрику для сравнения"
#: src/pages/DownloadStats.tsx:307
#, c-format
msgid "Fail on first error"
-msgstr ""
+msgstr "Сбой при первой ошибке"
#: src/pages/DownloadStats.tsx:364
#, c-format
msgid "Download"
-msgstr ""
+msgstr "Скачивать"
#: src/pages/DownloadStats.tsx:381
#, c-format
msgid "downloading... %1$s"
-msgstr ""
+msgstr "скачивание... %1$s"
#: src/pages/DownloadStats.tsx:399
#, c-format
msgid "Download completed"
-msgstr ""
+msgstr "Скачивание завершено"
#: src/pages/DownloadStats.tsx:400
#, c-format
msgid "click here to save the file in your computer"
-msgstr ""
+msgstr "Нажмите здесь, чтобы сохранить файл на своем компьютере"
#: src/pages/PublicHistoriesPage.tsx:78
#, c-format
msgid "History of public accounts"
-msgstr ""
+msgstr "История публичных счетов"
#: src/pages/RegistrationPage.tsx:48
#, c-format
msgid "Currently, the bank is not accepting new registrations!"
-msgstr ""
+msgstr "В настоящее время банк не принимает новые регистрации!"
#: src/pages/RegistrationPage.tsx:87
#, c-format
msgid "Missing name"
-msgstr ""
+msgstr "Отсутствует имя"
#: src/pages/RegistrationPage.tsx:91
#, c-format
msgid "Use letters and numbers only, and start with a lowercase letter"
-msgstr ""
+msgstr "Используйте только буквы и цифры и начинайте со строчной буквы"
#: src/pages/RegistrationPage.tsx:107
#, c-format
msgid "Passwords don't match"
-msgstr ""
+msgstr "Пароли не совпадают"
#: src/pages/RegistrationPage.tsx:130
#, c-format
msgid "Server replied with invalid phone or email."
-msgstr ""
+msgstr "Сервер ответил что телефон или электронной почта недействительны."
#: src/pages/RegistrationPage.tsx:137
#, c-format
msgid "Registration is disabled because the bank ran out of bonus credit."
-msgstr ""
+msgstr "Регистрация отключена, так как в банке закончился бонусный кредит."
#: src/pages/RegistrationPage.tsx:144
#, c-format
msgid "No enough permission to create that account."
-msgstr ""
+msgstr "Недостаточно разрешений для создания этого счёта."
#: src/pages/RegistrationPage.tsx:151
#, c-format
msgid "That account id is already taken."
-msgstr ""
+msgstr "Этот идентификатор счёта уже занят."
#: src/pages/RegistrationPage.tsx:158
#, c-format
msgid "That username is already taken."
-msgstr ""
+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 ""
+msgstr "Только администратор может установить лимит задолженности."
#: src/pages/RegistrationPage.tsx:179
#, c-format
msgid "No information for the selected authentication channel."
-msgstr ""
+msgstr "Нет информации о выбранном канале аутентификации."
#: src/pages/RegistrationPage.tsx:186
#, c-format
msgid "Authentication channel is not supported."
-msgstr ""
+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 ""
+msgstr "Регистрация счёта"
#: src/pages/RegistrationPage.tsx:315
#, c-format
msgid "Repeat password"
-msgstr ""
+msgstr "Повторите Пароль"
#: src/pages/RegistrationPage.tsx:457
#, c-format
msgid "Create a random temporary user"
-msgstr ""
+msgstr "Создать случайного временного пользователя"
#: src/pages/QrCodeSection.tsx:110
#, c-format
msgid "If you have a Taler wallet installed in this device"
-msgstr ""
+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"
+"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
+#, c-format
msgid "Withdraw"
-msgstr ""
+msgstr "Снять средства"
#: src/pages/QrCodeSection.tsx:152
#, c-format
msgid "Or if you have the wallet in another device"
-msgstr ""
+msgstr "Или если у вас есть кошелёк в другом устройстве"
#: src/pages/QrCodeSection.tsx:157
-#, fuzzy, c-format
+#, c-format
msgid "Scan the QR below to start the withdrawal."
-msgstr ""
+msgstr "Отсканируйте QR-код ниже чтобы начать вывод средств."
#: src/pages/WithdrawalQRCode.tsx:79
#, c-format
msgid "Operation aborted"
-msgstr ""
+msgstr "Операция прервана"
#: src/pages/WithdrawalQRCode.tsx:82
#, c-format
@@ -1002,32 +1036,34 @@ 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 ""
+msgstr "Теперь вы можете закрыть эту страницу или перейти на страницу счёта."
#: src/pages/WithdrawalQRCode.tsx:147
#, c-format
msgid "Done"
-msgstr ""
+msgstr "Готово"
#: src/pages/WithdrawalQRCode.tsx:158
#, c-format
msgid "Operation canceled"
-msgstr ""
+msgstr "Операция отменена"
#: src/pages/WithdrawalQRCode.tsx:173
#, c-format
-msgid ""
-"The operation is marked as 'selected' but some step in the withdrawal failed"
+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 ""
+msgstr "Счёт выбран, но идентификатор вывода средств не найден."
#: src/pages/WithdrawalQRCode.tsx:188
#, c-format
@@ -1035,12 +1071,14 @@ 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."
+"No withdrawal ID found and no account has been selected or the selected account "
+"is invalid."
msgstr ""
#: src/pages/WithdrawalQRCode.tsx:259
@@ -1103,12 +1141,12 @@ msgstr ""
#: src/pages/SolveChallengePage.tsx:224
#, c-format
msgid "Account delete"
-msgstr ""
+msgstr "Удаление счёта"
#: src/pages/SolveChallengePage.tsx:226
#, c-format
msgid "Account update"
-msgstr ""
+msgstr "Обновление счёта"
#: src/pages/SolveChallengePage.tsx:228
#, c-format
@@ -1118,158 +1156,159 @@ msgstr ""
#: src/pages/SolveChallengePage.tsx:230
#, c-format
msgid "Wire transfer"
-msgstr ""
+msgstr "Перевод"
#: src/pages/SolveChallengePage.tsx:232
-#, fuzzy, c-format
+#, c-format
msgid "Withdrawal"
-msgstr ""
+msgstr "Вывод"
#: src/pages/SolveChallengePage.tsx:248
-#, fuzzy, c-format
+#, c-format
msgid "Confirm the operation"
-msgstr ""
+msgstr "Подтвердить операцию"
#: src/pages/SolveChallengePage.tsx:271
#, c-format
msgid "Enter the confirmation code"
-msgstr ""
+msgstr "Введите код подтверждения"
#: src/pages/SolveChallengePage.tsx:313
#, c-format
msgid "Confirm"
-msgstr ""
+msgstr "Подтвердить"
#: src/pages/SolveChallengePage.tsx:348
#, c-format
msgid "Send again"
-msgstr ""
+msgstr "Отправить ещё раз"
#: src/pages/SolveChallengePage.tsx:359
#, c-format
msgid "Send code"
-msgstr ""
+msgstr "Отправить код"
#: src/pages/SolveChallengePage.tsx:369
#, c-format
msgid "Operation details"
-msgstr ""
+msgstr "Сведения об операции"
#: src/pages/SolveChallengePage.tsx:529
#, c-format
msgid "Challenge details"
-msgstr ""
+msgstr "Детали подтверждения"
#: src/pages/SolveChallengePage.tsx:536
#, c-format
msgid "Sent at"
-msgstr ""
+msgstr "Время отправления"
#: src/pages/SolveChallengePage.tsx:551
#, c-format
msgid "To phone"
-msgstr ""
+msgstr "На телефон"
#: src/pages/SolveChallengePage.tsx:553
#, c-format
msgid "To email"
-msgstr ""
+msgstr "На email"
#: src/pages/WithdrawalOperationPage.tsx:49
#, c-format
msgid "The Withdrawal URI is not valid"
-msgstr ""
+msgstr "URI вывода недействителен"
#: src/components/Cashouts/views.tsx:100
#, c-format
msgid "Latest cashouts"
-msgstr ""
+msgstr "Последние обналички"
#: src/components/Cashouts/views.tsx:111
#, c-format
msgid "Created"
-msgstr ""
+msgstr "Создано"
#: src/components/Cashouts/views.tsx:115
#, c-format
msgid "Total debit"
-msgstr ""
+msgstr "Всего дебет"
#: src/components/Cashouts/views.tsx:119
#, c-format
msgid "Total credit"
-msgstr ""
+msgstr "Итого кредит"
#: src/pages/ProfileNavigation.tsx:70
#, c-format
msgid "Details"
-msgstr ""
+msgstr "Подробности"
#: src/pages/ProfileNavigation.tsx:74
#, c-format
msgid "Delete"
-msgstr ""
+msgstr "Удалить"
#: src/pages/ProfileNavigation.tsx:78
#, c-format
msgid "Credentials"
-msgstr ""
+msgstr "Учетные данные"
#: src/pages/ProfileNavigation.tsx:82
#, c-format
msgid "Cashouts"
-msgstr ""
+msgstr "Выплаты"
#: src/pages/business/CreateCashout.tsx:95
#, c-format
msgid "Unable to create a cashout"
-msgstr ""
+msgstr "Не удается создать выплату"
#: src/pages/business/CreateCashout.tsx:96
#, c-format
msgid "The bank configuration does not support cashout operations."
-msgstr ""
+msgstr "Конфигурация банка не поддерживает операции выплаты."
#: src/pages/business/CreateCashout.tsx:223
#, c-format
msgid "need to be higher due to fees"
-msgstr ""
+msgstr "должна быть выше из-за комиссий"
#: src/pages/business/CreateCashout.tsx:225
#, c-format
msgid "the total transfer at destination will be zero"
-msgstr ""
+msgstr "общая сумма перевода в назначенее будет равна нулю"
#: src/pages/business/CreateCashout.tsx:250
#, c-format
msgid "Cashout created"
-msgstr ""
+msgstr "Выплата создана"
#: src/pages/business/CreateCashout.tsx:272
#, c-format
-msgid ""
-"Duplicated request detected, check if the operation succeeded or try again."
+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 ""
+msgstr "Неправильно применен курс конвертации"
#: src/pages/business/CreateCashout.tsx:286
#, c-format
msgid "The account does not have sufficient funds"
-msgstr ""
+msgstr "На счете недостаточно средств"
#: src/pages/business/CreateCashout.tsx:293
#, c-format
msgid "Cashouts are not supported"
-msgstr ""
+msgstr "Выплаты не поддерживаются"
#: src/pages/business/CreateCashout.tsx:300
#, c-format
msgid "Missing cashout URI in the profile"
-msgstr ""
+msgstr "Отсутствующий URI вылат в профиле"
#: src/pages/business/CreateCashout.tsx:307
#, c-format
@@ -1277,237 +1316,242 @@ 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 ""
+msgstr "Обменный курс"
#: src/pages/business/CreateCashout.tsx:360
#, c-format
msgid "Fee"
-msgstr ""
+msgstr "Комиссия"
#: src/pages/business/CreateCashout.tsx:374
#, c-format
msgid "To account"
-msgstr ""
+msgstr "На счёт"
#: src/pages/business/CreateCashout.tsx:381
#, c-format
msgid "No cashout account"
-msgstr ""
+msgstr "Нет счёта для выплат"
#: src/pages/business/CreateCashout.tsx:382
#, c-format
msgid "Before doing a cashout you need to complete your profile"
-msgstr ""
+msgstr "Перед тем, как сделать выплату, вам необходимо заполнить свой профиль"
#: src/pages/business/CreateCashout.tsx:440
-#, fuzzy, c-format
+#, c-format
msgid "Amount to send"
-msgstr ""
+msgstr "Сумма к отправке"
#: src/pages/business/CreateCashout.tsx:441
-#, fuzzy, c-format
+#, c-format
msgid "Amount to receive"
-msgstr ""
+msgstr "Сумма к получению"
#: src/pages/business/CreateCashout.tsx:490
#, c-format
msgid "Total cost"
-msgstr ""
+msgstr "Общая стоимость"
#: src/pages/business/CreateCashout.tsx:505
#, c-format
msgid "Balance left"
-msgstr ""
+msgstr "Остаток баланса"
#: src/pages/business/CreateCashout.tsx:520
#, c-format
msgid "Before fee"
-msgstr ""
+msgstr "Комиссия до"
#: src/pages/business/CreateCashout.tsx:533
#, c-format
msgid "Total cashout transfer"
-msgstr ""
+msgstr "Общий сумма перевода выплаты"
#: src/pages/business/CreateCashout.tsx:553
#, c-format
msgid "No cashout channel available"
-msgstr ""
+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"
+"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 ""
+msgstr "Двухфакторная аутентификация"
#: src/pages/business/CreateCashout.tsx:598
#, c-format
msgid "Email"
-msgstr ""
+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 ""
+msgstr "SMS"
#: src/pages/business/CreateCashout.tsx:648
#, c-format
msgid "add a phone number in your profile to enable this option"
-msgstr ""
+msgstr "Добавьте номер телефона в свой профиль, чтобы включить эту опцию"
#: src/pages/account/CashoutListForAccount.tsx:52
#, c-format
msgid "Cashout for account %1$s"
-msgstr ""
+msgstr "Выплата для аккаунта %1$s"
#: src/pages/admin/AccountForm.tsx:165
#, c-format
msgid "it doesn't have the pattern of an IBAN number"
-msgstr ""
+msgstr "у него нет шаблона номера IBAN"
#: src/pages/admin/AccountForm.tsx:185
#, c-format
msgid "it doesn't have the pattern of an email"
-msgstr ""
+msgstr "У него нет шаблона электронного письма"
#: src/pages/admin/AccountForm.tsx:190
#, c-format
msgid "should start with +"
-msgstr ""
+msgstr "должен начинаться с +"
#: src/pages/admin/AccountForm.tsx:192
#, c-format
msgid "phone number can't have other than numbers"
-msgstr ""
+msgstr "Номер телефона не может иметь ничего, кроме цифр"
#: src/pages/admin/AccountForm.tsx:329
#, c-format
msgid "account identification in the bank"
-msgstr ""
+msgstr "Идентификация счета в банке"
#: src/pages/admin/AccountForm.tsx:365
#, c-format
msgid "name of the person owner the account"
-msgstr ""
+msgstr "имя владельца счёта"
#: src/pages/admin/AccountForm.tsx:374
#, c-format
msgid "Internal IBAN"
-msgstr ""
+msgstr "Внутренний IBAN"
#: src/pages/admin/AccountForm.tsx:377
#, c-format
msgid "if empty a random account number will be assigned"
-msgstr ""
+msgstr "Если пусто, будет присвоен случайный номер счета"
#: src/pages/admin/AccountForm.tsx:378
#, c-format
msgid "account identification for bank transfer"
-msgstr ""
+msgstr "Идентификация счета для банковского перевода"
#: src/pages/admin/AccountForm.tsx:423
#, c-format
msgid "Phone"
-msgstr ""
+msgstr "Телефон"
#: src/pages/admin/AccountForm.tsx:451
#, c-format
msgid "Cashout IBAN"
-msgstr ""
+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 ""
+msgstr "номер счета, на который будут отправлены деньги при выводе средств"
#: src/pages/admin/AccountForm.tsx:470
#, c-format
msgid "Max debt"
-msgstr ""
+msgstr "Максимальная задолженность"
#: src/pages/admin/AccountForm.tsx:494
#, c-format
msgid "how much is user able to transfer after zero balance"
-msgstr ""
+msgstr "Какую сумму пользователь может перевести после нулевого баланса"
#: src/pages/admin/AccountForm.tsx:508
#, c-format
msgid "Is this a Taler Exchange?"
-msgstr ""
+msgstr "Это Обменник Taler?"
#: src/pages/admin/AccountForm.tsx:549
#, c-format
msgid "This server doesn't support second factor authentication."
-msgstr ""
+msgstr "Этот сервер не поддерживает двухфакторную аутентификацию."
#: src/pages/admin/AccountForm.tsx:560
#, c-format
msgid "Enable second factor authentication"
-msgstr ""
+msgstr "Включите двухфакторную аутентификацию"
#: src/pages/admin/AccountForm.tsx:596
#, c-format
msgid "Using email"
-msgstr ""
+msgstr "Используя email"
#: src/pages/admin/AccountForm.tsx:654
#, c-format
msgid "Using SMS"
-msgstr ""
+msgstr "Используя SMS"
#: src/pages/admin/AccountForm.tsx:691
#, c-format
msgid "Is this account public?"
-msgstr ""
+msgstr "Является ли этот счёт общедоступным?"
#: src/pages/admin/AccountForm.tsx:719
#, c-format
msgid "public accounts have their balance publicly accessible"
-msgstr ""
+msgstr "Баланс публичных счётов находится в открытом доступе"
#: src/pages/account/ShowAccountDetails.tsx:100
#, c-format
msgid "Account updated"
-msgstr ""
+msgstr "Счёт обновлён"
#: src/pages/account/ShowAccountDetails.tsx:107
#, c-format
msgid "The rights to change the account are not sufficient"
-msgstr ""
+msgstr "Недостаточно прав на изменение счёта"
#: src/pages/account/ShowAccountDetails.tsx:114
#, c-format
msgid "The username was not found"
-msgstr ""
+msgstr "Имя пользователя не найдено"
#: src/pages/account/ShowAccountDetails.tsx:121
#, c-format
-msgid ""
-"You can't change the legal name, please contact the your account "
-"administrator."
+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."
+msgid "You can't change the debt limit, please contact the your account administrator."
msgstr ""
+"Вы не можете изменить лимит задолженности, обратитесь к администратору "
+"аккаунта."
#: src/pages/account/ShowAccountDetails.tsx:135
#, c-format
@@ -1515,31 +1559,33 @@ 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 ""
+msgstr "Счет \"%1$s\""
#: src/pages/account/ShowAccountDetails.tsx:190
#, c-format
msgid "Change details"
-msgstr ""
+msgstr "Изменение реквизитов"
#: src/pages/account/ShowAccountDetails.tsx:235
#, c-format
msgid "Update"
-msgstr ""
+msgstr "Обновить"
#: src/pages/account/UpdateAccountPassword.tsx:78
#, c-format
msgid "password doesn't match"
-msgstr ""
+msgstr "пароль не совпадает"
#: src/pages/account/UpdateAccountPassword.tsx:95
#, c-format
msgid "Password changed"
-msgstr ""
+msgstr "Пароль изменен"
#: src/pages/account/UpdateAccountPassword.tsx:102
#, c-format
@@ -1549,8 +1595,8 @@ 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."
+"You need to provide the old password. If you don't have it contact your account "
+"administrator."
msgstr ""
#: src/pages/account/UpdateAccountPassword.tsx:117
@@ -1561,27 +1607,27 @@ msgstr ""
#: src/pages/account/UpdateAccountPassword.tsx:149
#, c-format
msgid "Update password"
-msgstr ""
+msgstr "Обновить пароль"
#: src/pages/account/UpdateAccountPassword.tsx:167
#, c-format
msgid "New password"
-msgstr ""
+msgstr "Новый пароль"
#: src/pages/account/UpdateAccountPassword.tsx:195
#, c-format
msgid "Type it again"
-msgstr ""
+msgstr "Введите его ещё раз"
#: src/pages/account/UpdateAccountPassword.tsx:217
#, c-format
msgid "repeat the same password"
-msgstr ""
+msgstr "повторите этот же пароль"
#: src/pages/account/UpdateAccountPassword.tsx:227
#, c-format
msgid "Current password"
-msgstr ""
+msgstr "Текущий пароль"
#: src/pages/account/UpdateAccountPassword.tsx:248
#, c-format
@@ -1591,13 +1637,13 @@ msgstr ""
#: src/pages/account/UpdateAccountPassword.tsx:272
#, c-format
msgid "Change"
-msgstr ""
+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."
+"Account created with password \"%1$s\". The user must change the password on the "
+"next login."
msgstr ""
#: src/pages/admin/CreateNewAccount.tsx:83
@@ -1643,12 +1689,12 @@ msgstr ""
#: src/pages/admin/CreateNewAccount.tsx:183
#, c-format
msgid "New business account"
-msgstr ""
+msgstr "Новый бизнес счёт"
#: src/pages/admin/CreateNewAccount.tsx:209
#, c-format
msgid "Create"
-msgstr ""
+msgstr "Создать"
#: src/pages/admin/RemoveAccount.tsx:94
#, c-format
@@ -1658,8 +1704,8 @@ 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."
+"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
@@ -1705,12 +1751,12 @@ msgstr ""
#: src/pages/admin/RemoveAccount.tsx:188
#, c-format
msgid "Deleting account \"%1$s\""
-msgstr ""
+msgstr "Удаление счёта \"%1$s\""
#: src/pages/admin/RemoveAccount.tsx:206
#, c-format
msgid "Verification"
-msgstr ""
+msgstr "Проверка"
#: src/pages/admin/RemoveAccount.tsx:231
#, c-format
@@ -1730,55 +1776,19 @@ msgstr ""
#: src/pages/business/ShowCashoutDetails.tsx:106
#, c-format
msgid "Cashout detail"
-msgstr ""
+msgstr "Подробности обналичивания"
#: src/pages/business/ShowCashoutDetails.tsx:139
#, c-format
msgid "Debited"
-msgstr ""
+msgstr "Дебетировано"
#: src/pages/business/ShowCashoutDetails.tsx:154
#, c-format
msgid "Credited"
-msgstr ""
+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 ""
+msgstr "Добро пожаловать в %1$s!"
diff --git a/packages/bank-ui/src/pages/OperationState/state.ts b/packages/bank-ui/src/pages/OperationState/state.ts
index 19c097d18..5544c4e23 100644
--- a/packages/bank-ui/src/pages/OperationState/state.ts
+++ b/packages/bank-ui/src/pages/OperationState/state.ts
@@ -18,6 +18,7 @@ import {
Amounts,
HttpStatusCode,
TalerCoreBankErrorsByMethod,
+ TalerCorebankApi,
TalerError,
assertUnreachable,
parsePaytoUri,
@@ -33,6 +34,7 @@ import { useSessionState } from "../../hooks/session.js";
import { useBankState } from "../../hooks/bank-state.js";
import { usePreferences } from "../../hooks/preferences.js";
import { Props, State } from "./index.js";
+import { useSettingsContext } from "../../context/settings.js";
export function useComponentState({
currency,
@@ -41,7 +43,8 @@ export function useComponentState({
routeHere,
onAuthorizationRequired,
}: Props): utils.RecursiveState<State> {
- const [settings] = usePreferences();
+ const [preference] = usePreferences();
+ const settings = useSettingsContext();
const [bankState, updateBankState] = useBankState();
const { state: credentials } = useSessionState();
const creds = credentials.status !== "loggedIn" ? undefined : credentials;
@@ -52,15 +55,22 @@ export function useComponentState({
const [failure, setFailure] = useState<
TalerCoreBankErrorsByMethod<"createWithdrawal"> | undefined
>();
- const amount = settings.maxWithdrawalAmount;
+ const amount = settings.defaultSuggestedAmount;
async function doSilentStart() {
// FIXME: if amount is not enough use balance
const parsedAmount = Amounts.parseOrThrow(`${currency}:${amount}`);
if (!creds) return;
- const resp = await bank.createWithdrawal(creds, {
- amount: Amounts.stringify(parsedAmount),
- });
+ const params: TalerCorebankApi.BankAccountCreateWithdrawalRequest =
+ settings.fastWithdrawalForm
+ ? {
+ suggested_amount: Amounts.stringify(parsedAmount),
+ }
+ : {
+ amount: Amounts.stringify(parsedAmount),
+ };
+
+ const resp = await bank.createWithdrawal(creds, params);
if (resp.type === "fail") {
setFailure(resp);
return;
@@ -73,7 +83,7 @@ export function useComponentState({
if (withdrawalOperationId === undefined) {
doSilentStart();
}
- }, [settings.fastWithdrawal, amount]);
+ }, [settings.fastWithdrawalForm, amount]);
if (failure) {
return {
@@ -174,7 +184,7 @@ export function useComponentState({
}
if (data.status === "confirmed") {
- if (!settings.showWithdrawalSuccess) {
+ if (!preference.showWithdrawalSuccess) {
updateBankState("currentWithdrawalOperationId", undefined);
// onClose()
}
diff --git a/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx
index 3bf891504..0fb8c0ac1 100644
--- a/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx
+++ b/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -79,6 +79,7 @@ export function PaytoWireTransferForm({
routeHere,
onAuthorizationRequired,
limit,
+ balance,
}: Props): VNode {
const [inputType, setInputType] = useState<"form" | "payto" | "qr">("form");
const isRawPayto = inputType !== "form";
@@ -111,6 +112,16 @@ export function PaytoWireTransferForm({
? ("x-taler-bank" as const)
: ("iban" as const);
+ const wireFee =
+ config.wire_transfer_fees === undefined
+ ? Amounts.zeroOfCurrency(config.currency)
+ : Amounts.parseOrThrow(config.wire_transfer_fees);
+
+ const limitWithFee =
+ Amounts.cmp(limit, wireFee) === 1
+ ? Amounts.sub(limit, wireFee).amount
+ : Amounts.zeroOfAmount(limit);
+
const errorsWire = undefinedIfEmpty({
account: !account
? i18n.str`Required`
@@ -124,7 +135,7 @@ export function PaytoWireTransferForm({
? i18n.str`Required`
: !parsedAmount
? i18n.str`Not valid`
- : validateAmount(parsedAmount, limit, i18n),
+ : validateAmount(parsedAmount, limitWithFee, i18n),
});
const parsed = !rawPaytoInput ? undefined : parsePaytoUri(rawPaytoInput);
@@ -134,7 +145,7 @@ export function PaytoWireTransferForm({
? i18n.str`Required`
: !parsed
? i18n.str`Does not follow the pattern`
- : validateRawPayto(parsed, limit, url.host, i18n, paytoType),
+ : validateRawPayto(parsed, limitWithFee, url.host, i18n, paytoType),
});
async function doSend() {
@@ -479,9 +490,9 @@ export function PaytoWireTransferForm({
e.preventDefault();
}}
>
- <div class="p-4 sm:p-8">
+ <div class="m-4">
{!isRawPayto ? (
- <div class="grid max-w-xs grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <div class="grid max-w-xs grid-cols-1 gap-x-6 gap-y-8 ">
{(() => {
switch (paytoType) {
case "x-taler-bank": {
@@ -622,7 +633,45 @@ export function PaytoWireTransferForm({
</div>
</div>
)}
+ {Amounts.cmp(limitWithFee, balance) > 0 ? (
+ <p class="mt-2 text-sm text-gray-900">
+ <i18n.Translate>
+ You can transfer{" "}
+ <RenderAmount
+ value={limitWithFee}
+ spec={config.currency_specification}
+ />
+ </i18n.Translate>
+ </p>
+ ) : undefined}
</div>
+ {Amounts.isZero(wireFee) ? undefined : (
+ <div class="px-4 my-4">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <div class="sm:col-span-6">
+ <dl class="mt-4 space-y-4">
+ <Fragment>
+ <div class="flex items-center justify-between ">
+ <dt class="flex items-center text-sm text-gray-600">
+ <span>
+ <i18n.Translate>Cost</i18n.Translate>
+ </span>
+ </dt>
+ <dd class="text-sm text-gray-900">
+ <RenderAmount
+ value={wireFee}
+ negative
+ withColor
+ spec={config.currency_specification}
+ />
+ </dd>
+ </div>
+ </Fragment>
+ </dl>
+ </div>
+ </div>
+ </div>
+ )}
<div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
{routeCancel ? (
<a
diff --git a/packages/bank-ui/src/pages/QrCodeSection.tsx b/packages/bank-ui/src/pages/QrCodeSection.tsx
index 359d4c18f..2a21295c7 100644
--- a/packages/bank-ui/src/pages/QrCodeSection.tsx
+++ b/packages/bank-ui/src/pages/QrCodeSection.tsx
@@ -86,10 +86,10 @@ export function QrCodeSection({
<div class="mt-4 mb-4 text-sm text-gray-500">
<p>
<i18n.Translate>
- You will see the details of the operation in your wallet
- including the fees (if applies). If you still don't have one you
- can install it following instructions in
- </i18n.Translate>{" "}
+ Your wallet will display the details of the transaction
+ including the fees (if applicable). If you do not yet have a
+ wallet, please follow the instructions on
+ </i18n.Translate>
<a
class="font-semibold text-gray-500 hover:text-gray-400"
name="wallet page"
diff --git a/packages/bank-ui/src/pages/WalletWithdrawForm.tsx b/packages/bank-ui/src/pages/WalletWithdrawForm.tsx
index a9c652643..7cf2c7881 100644
--- a/packages/bank-ui/src/pages/WalletWithdrawForm.tsx
+++ b/packages/bank-ui/src/pages/WalletWithdrawForm.tsx
@@ -19,6 +19,7 @@ import {
AmountJson,
Amounts,
HttpStatusCode,
+ TalerCorebankApi,
TranslatedString,
assertUnreachable,
parseWithdrawUri,
@@ -45,6 +46,7 @@ import {
RenderAmount,
doAutoFocus,
} from "./PaytoWireTransferForm.js";
+import { useSettingsContext } from "../context/settings.js";
const RefAmount = forwardRef(InputAmount);
@@ -64,7 +66,7 @@ function OldWithdrawalForm({
routeCancel: RouteDefinition;
}): VNode {
const { i18n } = useTranslationContext();
- const [settings] = usePreferences();
+ const settings = useSettingsContext();
// const walletInegrationApi = useTalerWalletIntegrationAPI()
// const { navigateTo } = useNavigationContext();
@@ -79,7 +81,7 @@ function OldWithdrawalForm({
const creds = credentials.status !== "loggedIn" ? undefined : credentials;
const [amountStr, setAmountStr] = useState<string | undefined>(
- `${settings.maxWithdrawalAmount}`,
+ `${settings.defaultSuggestedAmount ?? 1}`,
);
const [notification, notify, handleError] = useLocalNotification();
@@ -141,9 +143,15 @@ function OldWithdrawalForm({
async function doStart() {
if (!parsedAmount || !creds) return;
await handleError(async () => {
- const resp = await api.createWithdrawal(creds, {
- amount: Amounts.stringify(parsedAmount),
- });
+ const params: TalerCorebankApi.BankAccountCreateWithdrawalRequest =
+ settings.fastWithdrawalForm
+ ? {
+ suggested_amount: Amounts.stringify(parsedAmount),
+ }
+ : {
+ amount: Amounts.stringify(parsedAmount),
+ };
+ const resp = await api.createWithdrawal(creds, params);
if (resp.type === "ok") {
const uri = parseWithdrawUri(resp.body.taler_withdraw_uri);
if (!uri) {
@@ -234,9 +242,9 @@ function OldWithdrawalForm({
</i18n.Translate>
</p>
{Amounts.cmp(limit, balance) > 0 ? (
- <p class="mt-2 text-sm text-gray-500">
+ <p class="mt-2 text-sm text-gray-900">
<i18n.Translate>
- Your account allows you to withdraw{" "}
+ You can withdraw{" "}
<RenderAmount
value={limit}
spec={config.currency_specification}
@@ -340,7 +348,8 @@ export function WalletWithdrawForm({
routeCancel: RouteDefinition;
}): VNode {
const { i18n } = useTranslationContext();
- const [settings, updateSettings] = usePreferences();
+ const [pref, updatePref] = usePreferences();
+ const settings = useSettingsContext();
return (
<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-6 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
@@ -357,11 +366,11 @@ export function WalletWithdrawForm({
</div>
<div class="col-span-2">
- {settings.showInstallWallet && (
+ {pref.showInstallWallet && (
<Attention
title={i18n.str`You need a Taler wallet`}
onClose={() => {
- updateSettings("showInstallWallet", false);
+ updatePref("showInstallWallet", false);
}}
>
<i18n.Translate>
@@ -379,7 +388,7 @@ export function WalletWithdrawForm({
</Attention>
)}
- {!settings.fastWithdrawal ? (
+ {!settings.fastWithdrawalForm ? (
<OldWithdrawalForm
focus={focus}
routeOperationDetails={routeOperationDetails}
diff --git a/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
index 853dd7bae..b270c447a 100644
--- a/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
+++ b/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
@@ -17,6 +17,7 @@
import {
AbsoluteTime,
AmountJson,
+ Amounts,
HttpStatusCode,
PaytoUri,
PaytoUriIBAN,
@@ -79,6 +80,11 @@ export function WithdrawalConfirmationQuestion({
lib: { bank: api },
} = useBankCoreApiContext();
+ const wireFee =
+ config.wire_transfer_fees === undefined
+ ? Amounts.zeroOfCurrency(config.currency)
+ : Amounts.parseOrThrow(config.wire_transfer_fees);
+
async function doTransfer() {
await handleError(async () => {
if (!creds) return;
@@ -357,6 +363,23 @@ export function WithdrawalConfirmationQuestion({
/>
</dd>
</div>
+ {Amounts.isZero(wireFee) ? undefined : (
+ <Fragment>
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>Cost</i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ <RenderAmount
+ value={wireFee}
+ negative
+ withColor
+ spec={config.currency_specification}
+ />
+ </dd>
+ </div>
+ </Fragment>
+ )}
</dl>
</div>
</div>
diff --git a/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx b/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx
index 6db0e5512..0e2144d77 100644
--- a/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx
+++ b/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx
@@ -15,6 +15,7 @@
*/
import {
AbsoluteTime,
+ AccountLetter,
HttpStatusCode,
TalerCorebankApi,
TalerError,
@@ -200,28 +201,17 @@ export function ShowAccountDetails({
}
const url = bank.getRevenueAPI(account);
- url.username = account;
const baseURL = url.href;
-
+ const revenueURL = new URL(baseURL)
+ revenueURL.username = account;
+ revenueURL.password = creds?.token ?? ""
const ac = parsePaytoUri(result.body.payto_uri);
const payto = !ac?.isKnown ? undefined : ac;
- let accountLetter: string | undefined = undefined;
- if (payto) {
- switch (payto.targetType) {
- case "iban": {
- accountLetter = `account-info-url=${url.href}\naccount-type=${payto.targetType}\niban=${payto.iban}\nreceiver-name=${result.body.name}\n`;
- break;
- }
- case "x-taler-bank": {
- accountLetter = `account-info-url=${url.href}\naccount-type=${payto.targetType}\naccount=${payto.account}\nhost=${payto.host}\nreceiver-name=${result.body.name}\n`;
- break;
- }
- case "bitcoin": {
- accountLetter = `account-info-url=${url.href}\naccount-type=${payto.targetType}\naddress=${payto.address}\nreceiver-name=${result.body.name}\n`;
- break;
- }
+ const accountLetter : AccountLetter | undefined = !payto
+ ? undefined
+ : {
+ accountURI: result.body.payto_uri, infoURL: revenueURL.href
}
- }
return (
<Fragment>
@@ -327,7 +317,7 @@ export function ShowAccountDetails({
name="account-type"
id="account-type"
disabled={true}
- value={account}
+ value={payto.targetType}
autocomplete="off"
/>
</div>
@@ -372,16 +362,16 @@ export function ShowAccountDetails({
<div class="sm:col-span-5">
<label
class="block text-sm font-medium leading-6 text-gray-900"
- for="iban"
+ for="account-name"
>
- {i18n.str`IBAN`}
+ {i18n.str`Account name`}
</label>
<div class="mt-2">
<input
type="text"
class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- name="iban"
- id="iban"
+ name="account-name"
+ id="account-name"
disabled={true}
value={payto.account}
autocomplete="off"
@@ -389,7 +379,7 @@ export function ShowAccountDetails({
</div>
<p class="mt-2 text-sm text-gray-500">
<i18n.Translate>
- International Bank Account Number.
+ Bank account identifier for wire transfers.
</i18n.Translate>
</p>
</div>
@@ -486,7 +476,7 @@ export function ShowAccountDetails({
<i18n.Translate>Cancel</i18n.Translate>
</a>
<CopyButton
- getContent={() => accountLetter ?? ""}
+ getContent={() => !accountLetter ? "" : JSON.stringify(accountLetter)}
class="flex text-center disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
>
<i18n.Translate>Copy</i18n.Translate>
@@ -498,3 +488,4 @@ export function ShowAccountDetails({
</Fragment>
);
}
+
diff --git a/packages/bank-ui/src/settings.json b/packages/bank-ui/src/settings.json
index df5fe75ce..f14168e77 100644
--- a/packages/bank-ui/src/settings.json
+++ b/packages/bank-ui/src/settings.json
@@ -2,6 +2,8 @@
"backendBaseURL": "http://bank.taler.test:1180/",
"simplePasswordForRandomAccounts": true,
"allowRandomAccountCreation": true,
+ "fastWithdrawalForm": true,
+ "defaultSuggestedAmount": 11,
"bankName": "Taler DEVELOPMENT Bank",
"topNavSites": {
"Exchange": "http://Exchnage.taler.test:1180/",
diff --git a/packages/bank-ui/src/settings.ts b/packages/bank-ui/src/settings.ts
index c085c7cd8..6d8f7b850 100644
--- a/packages/bank-ui/src/settings.ts
+++ b/packages/bank-ui/src/settings.ts
@@ -20,6 +20,7 @@ import {
canonicalizeBaseUrl,
codecForBoolean,
codecForMap,
+ codecForNumber,
codecForString,
codecOptional,
} from "@gnu-taler/taler-util";
@@ -45,6 +46,17 @@ export interface UiSettings {
// - value: link target, where the user is going to be redirected
// default: empty list
topNavSites?: Record<string, string>;
+ // Use the withdrawal form which redirect the user to the wallet
+ // without asking the amount to the user.
+ // - true: on withdrawal creation the spa will use suggested_amount instead
+ // of fixed amount
+ // - false: on withdrawal creation the spa will use fixed amount
+ // default: false
+ fastWithdrawalForm?: boolean;
+ // When the withdrawal form use the suggested amount the bank
+ // will send a default value that the user can change.
+ // default: 10
+ defaultSuggestedAmount?: number;
}
/**
@@ -56,12 +68,16 @@ const defaultSettings: UiSettings = {
simplePasswordForRandomAccounts: false,
allowRandomAccountCreation: false,
topNavSites: {},
+ fastWithdrawalForm: false,
+ defaultSuggestedAmount: 10,
};
const codecForUISettings = (): Codec<UiSettings> =>
buildCodecForObject<UiSettings>()
.property("backendBaseURL", codecOptional(codecForString()))
.property("allowRandomAccountCreation", codecOptional(codecForBoolean()))
+ .property("fastWithdrawalForm", codecOptional(codecForBoolean()))
+ .property("defaultSuggestedAmount", codecOptional(codecForNumber()))
.property(
"simplePasswordForRandomAccounts",
codecOptional(codecForBoolean()),
diff --git a/packages/challenger-ui/package.json b/packages/challenger-ui/package.json
index 8234e2385..7cc73771b 100644
--- a/packages/challenger-ui/package.json
+++ b/packages/challenger-ui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/challenger-ui",
- "version": "0.10.7",
+ "version": "0.11.4",
"author": "sebasjm",
"license": "AGPL-3.0-OR-LATER",
"description": "UI for GNU Challenger.",
diff --git a/packages/idb-bridge/package.json b/packages/idb-bridge/package.json
index 376265c0f..ce3123619 100644
--- a/packages/idb-bridge/package.json
+++ b/packages/idb-bridge/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/idb-bridge",
- "version": "0.10.7",
+ "version": "0.11.4",
"description": "IndexedDB implementation that uses SQLite3 as storage",
"main": "./dist/idb-bridge.js",
"module": "./lib/index.js",
@@ -38,6 +38,6 @@
"failFast": true
},
"optionalDependencies": {
- "better-sqlite3": "9.4.0"
+ "better-sqlite3": "10.0.0"
}
}
diff --git a/packages/merchant-backend-ui/package.json b/packages/merchant-backend-ui/package.json
index bd16317f5..bc8627312 100644
--- a/packages/merchant-backend-ui/package.json
+++ b/packages/merchant-backend-ui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/merchant-backend-ui",
- "version": "0.10.7",
+ "version": "0.11.4",
"license": "AGPL-3.0-or-later",
"scripts": {
"compile": "tsc && ./build.mjs",
diff --git a/packages/merchant-backoffice-ui/package.json b/packages/merchant-backoffice-ui/package.json
index e80604777..8aabdce87 100644
--- a/packages/merchant-backoffice-ui/package.json
+++ b/packages/merchant-backoffice-ui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/merchant-backoffice-ui",
- "version": "0.10.7",
+ "version": "0.11.4",
"license": "AGPL-3.0-or-later",
"type": "module",
"scripts": {
diff --git a/packages/merchant-backoffice-ui/src/Application.tsx b/packages/merchant-backoffice-ui/src/Application.tsx
index 097e98567..5be21ff8f 100644
--- 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/form/InputPaytoForm.tsx b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
index a0c15c77c..4ac798afe 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
@@ -18,13 +18,10 @@
*
* @author Sebastian Javier Marchano (sebasjm)
*/
-import {
- parsePaytoUri,
- PaytoUriGeneric,
- stringifyPaytoUri,
-} from "@gnu-taler/taler-util";
+import { parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
+import { useEffect, useState } from "preact/hooks";
import { COUNTRY_TABLE } from "../../utils/constants.js";
import { undefinedIfEmpty } from "../../utils/table.js";
import { FormErrors, FormProvider } from "./FormProvider.js";
@@ -32,7 +29,6 @@ import { Input } from "./Input.js";
import { InputGroup } from "./InputGroup.js";
import { InputSelector } from "./InputSelector.js";
import { InputProps, useField } from "./useField.js";
-import { useEffect, useState } from "preact/hooks";
export interface Props<T> extends InputProps<T> {
isValid?: (e: any) => boolean;
@@ -108,13 +104,13 @@ function validateEthereum_path1(
* bank.com/path
* bank.com/path/subpath/
*/
-const DOMAIN_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9](?:\.[a-zA-Z]{2,})+(\/[a-zA-Z0-9-.]+)*\/?$/
+const DOMAIN_REGEX =
+ /^[a-zA-Z0-9][a-zA-Z0-9-_]{1,61}[a-zA-Z0-9-_](?:\.[a-zA-Z0-9-_]{2,})+(:[0-9]+)?(\/[a-zA-Z0-9-.]+)*\/?$/;
function validateTalerBank_path1(
addr: string,
i18n: ReturnType<typeof useTranslationContext>["i18n"],
): string | undefined {
- console.log(addr, DOMAIN_REGEX.test(addr))
try {
const valid = DOMAIN_REGEX.test(addr);
if (valid) return undefined;
@@ -206,6 +202,7 @@ export function InputPaytoForm<T>({
const { value: initialValueStr, onChange } = useField<T>(name);
const initialPayto = parsePaytoUri(initialValueStr ?? "");
+
const paths = !initialPayto ? [] : initialPayto.targetPath.split("/");
const initialPath1 = paths.length >= 1 ? paths[0] : undefined;
const initialPath2 = paths.length >= 2 ? paths[1] : undefined;
@@ -219,6 +216,22 @@ export function InputPaytoForm<T>({
path2: initialPath2,
};
const [value, setValue] = useState<Partial<Entity>>(initial);
+ useEffect(() => {
+ const nv = parsePaytoUri(initialValueStr ?? "");
+ const paths = !initialPayto ? [] : initialPayto.targetPath.split("/");
+ if (nv !== undefined && nv.isKnown) {
+ if (nv.targetType === "iban" && paths.length >= 2) {
+ //FIXME: workaround EBIC not supported
+ paths[0] = paths[1]
+ }
+ setValue({
+ target: nv.targetType,
+ params: nv.params,
+ path1: paths.length >= 1 ? paths[0] : undefined,
+ path2: paths.length >= 2 ? paths[1] : undefined,
+ });
+ }
+ }, [initialValueStr]);
const { i18n } = useTranslationContext();
@@ -252,7 +265,8 @@ export function InputPaytoForm<T>({
(k) => (errors as any)[k] !== undefined,
);
- const path1WithSlash = value.path1 && !value.path1.endsWith("/") ? value.path1 + "/" : value.path1
+ const path1WithSlash =
+ value.path1 && !value.path1.endsWith("/") ? value.path1 + "/" : value.path1;
const str =
hasErrors || !value.target
? undefined
@@ -268,37 +282,6 @@ export function InputPaytoForm<T>({
onChange(str as any);
}, [str]);
- // const submit = useCallback((): void => {
- // // const accounts: TalerMerchantApi.AccountAddDetails[] = paytos;
- // // const alreadyExists =
- // // accounts.findIndex((x) => x.payto_uri === paytoURL) !== -1;
- // // if (!alreadyExists) {
- // const newValue: TalerMerchantApi.AccountAddDetails = {
- // payto_uri: paytoURL,
- // };
- // if (value.auth) {
- // if (value.auth.url) {
- // newValue.credit_facade_url = value.auth.url;
- // }
- // if (value.auth.type === "none") {
- // newValue.credit_facade_credentials = {
- // type: "none",
- // };
- // }
- // if (value.auth.type === "basic") {
- // newValue.credit_facade_credentials = {
- // type: "basic",
- // username: value.auth.username ?? "",
- // password: value.auth.password ?? "",
- // };
- // }
- // }
- // onChange(newValue as any);
- // // }
- // // valueHandler(defaultTarget);
- // }, [value]);
-
- //FIXME: translating plural singular
return (
<InputGroup name="payto" label={label} fixed tooltip={tooltip}>
<FormProvider<Entity>
@@ -413,11 +396,17 @@ export function InputPaytoForm<T>({
return v;
}}
tooltip={i18n.str`Bank host.`}
- help={<Fragment>
- <div><i18n.Translate>Without scheme and may include subpath:</i18n.Translate></div>
- <div>bank.com/</div>
- <div>bank.com/path/subpath/</div>
- </Fragment>}
+ help={
+ <Fragment>
+ <div>
+ <i18n.Translate>
+ Without scheme and may include subpath:
+ </i18n.Translate>
+ </div>
+ <div>bank.com/</div>
+ <div>bank.com/path/subpath/</div>
+ </Fragment>
+ }
/>
<Input<Entity>
name="path2"
diff --git a/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx b/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
index 864d09f48..efcca302f 100644
--- a/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
+++ b/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
@@ -60,22 +60,6 @@ export function DefaultInstanceFormFields({
tooltip={i18n.str`Legal name of the business represented by this instance.`}
/>
- <TextField name="asdasd" label="">
- <i18n.Translate>
- Choose individual if you don't have or are not required to have legal business permission.
- </i18n.Translate>
- </TextField>
-
- <InputSelector<Entity>
- name="user_type"
- label={i18n.str`Selling as`}
- tooltip={i18n.str`Different type of account can have different rules and requirements.`}
- values={["business", "individual"]}
- toStr={(d: string) => {
- return d.toUpperCase();
- }}
- />
-
<Input<Entity>
name="email"
label={i18n.str`Email`}
diff --git a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
index 2090704d9..4a1f6a9df 100644
--- a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
+++ b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
@@ -111,7 +111,7 @@ export function Sidebar({ mobile }: Props): VNode {
<li>
<a href={"/templates"} class="has-icon">
<span class="icon">
- <i class="mdi mdi-newspaper" />
+ <i class="mdi mdi-qrcode" />
</span>
<span class="menu-item-label">
<i18n.Translate>Templates</i18n.Translate>
@@ -156,7 +156,7 @@ export function Sidebar({ mobile }: Props): VNode {
<li>
<a href={"/webhooks"} class="has-icon">
<span class="icon">
- <i class="mdi mdi-newspaper" />
+ <i class="mdi mdi-webhook" />
</span>
<span class="menu-item-label">
<i18n.Translate>Webhooks</i18n.Translate>
diff --git a/packages/merchant-backoffice-ui/src/components/modal/index.tsx b/packages/merchant-backoffice-ui/src/components/modal/index.tsx
index 1335d0f77..43062d13e 100644
--- a/packages/merchant-backoffice-ui/src/components/modal/index.tsx
+++ b/packages/merchant-backoffice-ui/src/components/modal/index.tsx
@@ -24,9 +24,14 @@ import { ComponentChildren, Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { DEFAULT_REQUEST_TIMEOUT } from "../../utils/constants.js";
import { Spinner } from "../exception/loading.js";
-import { FormProvider } from "../form/FormProvider.js";
+import { FormErrors, FormProvider } from "../form/FormProvider.js";
import { Input } from "../form/Input.js";
import { useSessionContext } from "../../context/session.js";
+import {
+ AccountLetter,
+ codecForAccountLetter,
+ PaytoString,
+} from "@gnu-taler/taler-util";
interface Props {
active?: boolean;
@@ -201,6 +206,88 @@ export function ClearConfirmModal({
);
}
+interface ImportingAccountModalProps {
+ onCancel: () => void;
+ onConfirm: (account: AccountLetter) => void;
+}
+
+export function ImportingAccountModal({
+ onCancel,
+ onConfirm,
+}: ImportingAccountModalProps): VNode {
+ const { i18n } = useTranslationContext();
+ const [letter, setLetter] = useState<string>();
+ let parsed = undefined;
+ try {
+ parsed = JSON.parse(letter ?? "");
+ } catch (e) {
+ parsed = undefined;
+ }
+ let account: AccountLetter | undefined = undefined;
+ let parsingError: string | undefined = undefined;
+ try {
+ account =
+ parsed !== undefined ? codecForAccountLetter().decode(parsed) : undefined;
+ } catch (e) {
+ account = undefined;
+ if (e instanceof Error) {
+ parsingError = e.message;
+ }
+ }
+ const errors: FormErrors<{ letter: string }> = {
+ letter: !letter
+ ? i18n.str`required`
+ : parsed === undefined
+ ? i18n.str`letter should be a JSON string`
+ : account === undefined
+ ? i18n.str`JSON string is invalid`
+ : undefined,
+ };
+ return (
+ <ConfirmModal
+ label={i18n.str`Import`}
+ description={i18n.str`Importing an account from the bank`}
+ active
+ onCancel={onCancel}
+ disabled={account === undefined}
+ onConfirm={() => onConfirm(account!)}
+ >
+ <p>
+ <i18n.Translate>
+ You can export your account settings from the Libeufin Bank's account
+ profile. Paste the content in the next field.
+ </i18n.Translate>
+ </p>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <i18n.Translate>Account information</i18n.Translate>
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input
+ class="input"
+ value={letter ?? ""}
+ onChange={(e) => {
+ setLetter(e.currentTarget.value);
+ }}
+ />
+ </p>
+ {letter !== undefined && errors.letter && (
+ <p class="help is-danger">{errors.letter}</p>
+ )}
+ {parsingError !== undefined && (
+ <p class="help is-danger">{parsingError}</p>
+ )}
+ </div>
+ </div>
+ </div>
+ </ConfirmModal>
+ );
+}
+
interface DeleteModalProps {
element: { id: string; name: string };
onCancel: () => void;
diff --git a/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx
index c6d687b85..dede0008f 100644
--- 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
index 12d99f3fc..500a94a48 100644
--- 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/index.html b/packages/merchant-backoffice-ui/src/index.html
index b005f967d..6b738d2b7 100644
--- 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/accounts/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx
index d05375b6c..d0e7a83cd 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx
@@ -31,6 +31,7 @@ import {
import { Input } from "../../../../components/form/Input.js";
import { InputPaytoForm } from "../../../../components/form/InputPaytoForm.js";
import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { ImportingAccountModal } from "../../../../components/modal/index.js";
import { undefinedIfEmpty } from "../../../../utils/table.js";
import { safeConvertURL } from "../update/UpdatePage.js";
@@ -46,6 +47,7 @@ const accountAuthType = ["none", "basic"];
export function CreatePage({ onCreate, onBack }: Props): VNode {
const { i18n } = useTranslationContext();
+ const [importing, setImporting] = useState(false);
const [state, setState] = useState<Partial<Entity>>({});
const facadeURL = safeConvertURL(state.credit_facade_url);
const errors: FormErrors<Entity> = {
@@ -115,9 +117,25 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
credit_facade_url,
});
};
-
return (
<div>
+ {importing && <ImportingAccountModal onCancel={()=> {setImporting(false)}} onConfirm={(ac) => {
+ state.payto_uri = ac.accountURI
+ const u = new URL(ac.infoURL)
+ u.password = ""
+ if (u.username || u.password) {
+ state.credit_facade_credentials = {
+ type: "basic",
+ password: u.password,
+ username: u.username,
+ }
+ state.repeatPassword = u.password
+ }
+ u.password = ""
+ u.username = ""
+ state.credit_facade_url = u.href;
+ setImporting(false)
+ }} />}
<section class="section is-main-section">
<div class="columns">
<div class="column" />
@@ -171,6 +189,16 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
</FormProvider>
<div class="buttons is-right mt-5">
+ <button
+ class="button is-info"
+ data-tooltip={i18n.str`Need to complete marked fields`}
+ onClick={() => {
+ setImporting(true)
+ }}
+ >
+ <i18n.Translate>Import from bank</i18n.Translate>
+ </button>
+
{onBack && (
<button class="button" onClick={onBack}>
<i18n.Translate>Cancel</i18n.Translate>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx
index 9bab33f6f..aa1481a2e 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx
@@ -24,6 +24,7 @@ import {
HttpStatusCode,
OperationFail,
OperationOk,
+ PaytoString,
TalerError,
TalerMerchantApi,
TalerRevenueHttpClient,
@@ -67,51 +68,55 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode {
const resp = await testRevenueAPI(
revenueAPI,
request.credit_facade_credentials,
+ request.payto_uri,
);
+ if (resp instanceof TalerError) {
+ setNotif({
+ message: i18n.str`Could not add bank account`,
+ type: "ERROR",
+ description: i18n.str`The request to check the revenue API failed.`,
+ details: JSON.stringify(resp.errorDetail, undefined, 2),
+ });
+ return;
+ }
if (resp.type === "fail") {
switch (resp.case) {
- case TestRevenueErrorType.NO_CONFIG: {
- setNotif({
- message: i18n.str`Could not create account`,
- type: "ERROR",
- description: i18n.str`The endpoint doesn't seems to be a Taler Revenue API`,
- });
- return;
- }
- case TestRevenueErrorType.CLIENT_BAD_REQUEST: {
+ case HttpStatusCode.BadRequest: {
setNotif({
- message: i18n.str`Could not create account`,
+ message: i18n.str`Could not add bank account`,
type: "ERROR",
description: i18n.str`Server replied with "bad request".`,
});
return;
+
}
- case TestRevenueErrorType.UNAUTHORIZED: {
+ case HttpStatusCode.Unauthorized: {
setNotif({
- message: i18n.str`Could not create account`,
+ message: i18n.str`Could not add bank account`,
type: "ERROR",
description: i18n.str`Unauthorized, try with another credentials.`,
});
return;
+
}
- case TestRevenueErrorType.NOT_FOUND: {
+ case HttpStatusCode.NotFound: {
setNotif({
- message: i18n.str`Could not create account`,
+ message: i18n.str`Could not add bank account`,
type: "ERROR",
- description: i18n.str`Check facade URL, server replied with "not found".`,
+ description: i18n.str`The endpoint doesn't seems to be a Taler Revenue API`,
});
return;
}
- case TestRevenueErrorType.GENERIC_ERROR: {
+ case TestRevenueErrorType.ANOTHER_ACCOUNT: {
setNotif({
- message: i18n.str`Could not create account`,
+ message: i18n.str`Could not add bank account`,
type: "ERROR",
- description: resp.detail.hint,
+ description: i18n.str`The account info URL returned information from an account which is not the same in the account form: ${resp.detail.hint}`,
});
return;
}
default: {
- assertUnreachable(resp.case);
+ assertUnreachable(resp);
}
}
}
@@ -136,17 +141,18 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode {
}
export enum TestRevenueErrorType {
- NO_CONFIG,
- CLIENT_BAD_REQUEST,
- UNAUTHORIZED,
- NOT_FOUND,
- GENERIC_ERROR,
+ ANOTHER_ACCOUNT,
}
export async function testRevenueAPI(
revenueAPI: URL,
creds: FacadeCredentials | undefined,
-): Promise<OperationOk<void> | OperationFail<TestRevenueErrorType>> {
+ account: PaytoString,
+): Promise<OperationOk<void> | OperationFail<HttpStatusCode.NotFound>
+| OperationFail<HttpStatusCode.Unauthorized>
+| OperationFail<HttpStatusCode.BadRequest>
+| OperationFail<TestRevenueErrorType.ANOTHER_ACCOUNT>
+| TalerError> {
const api = new TalerRevenueHttpClient(
revenueAPI.href,
new BrowserFetchHttpLib(),
@@ -167,69 +173,33 @@ export async function testRevenueAPI(
const config = await api.getConfig(auth);
if (config.type === "fail") {
- switch (config.case) {
- case HttpStatusCode.Unauthorized: {
- return {
- type: "fail",
- case: TestRevenueErrorType.UNAUTHORIZED,
- detail: {
- code: 1,
- },
- };
- }
- case HttpStatusCode.NotFound: {
- return {
- type: "fail",
- case: TestRevenueErrorType.NO_CONFIG,
- detail: {
- code: 1,
- },
- };
- }
- }
+ return config;
}
const history = await api.getHistory(auth);
if (history.type === "fail") {
- switch (history.case) {
- case HttpStatusCode.BadRequest: {
- return {
- type: "fail",
- case: TestRevenueErrorType.CLIENT_BAD_REQUEST,
- detail: {
- code: 1,
- },
- };
- }
- case HttpStatusCode.Unauthorized: {
- return {
- type: "fail",
- case: TestRevenueErrorType.UNAUTHORIZED,
- detail: {
- code: 1,
- },
- };
- }
- case HttpStatusCode.NotFound: {
- return {
- type: "fail",
- case: TestRevenueErrorType.NOT_FOUND,
- detail: {
- code: 1,
- },
- };
- }
- }
+ return history;
}
- } catch (err) {
- if (err instanceof TalerError) {
+ if (history.body.credit_account !== account) {
return {
type: "fail",
- case: TestRevenueErrorType.GENERIC_ERROR,
- detail: err.errorDetail,
+ case: TestRevenueErrorType.ANOTHER_ACCOUNT,
+ detail: {
+ code: 1,
+ hint: history.body.credit_account
+ },
};
}
+ } catch (err) {
+ if (err instanceof TalerError) {
+ return err;
+ // return {
+ // type: "fail",
+ // case: TestRevenueErrorType.GENERIC_ERROR,
+ // detail: err.errorDetail,
+ // };
+ }
}
return opFixedSuccess(undefined);
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx
index efe484402..a9cb2805b 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx
@@ -48,7 +48,7 @@ export function CardTable({
<header class="card-header">
<p class="card-header-title">
<span class="icon">
- <i class="mdi mdi-newspaper" />
+ <i class="mdi mdi-bank" />
</span>
<i18n.Translate>Bank accounts</i18n.Translate>
</p>
@@ -240,9 +240,6 @@ function Table({
<th>
<i18n.Translate>IBAN</i18n.Translate>
</th>
- <th>
- <i18n.Translate>BIC</i18n.Translate>
- </th>
<th />
</tr>
</thead>
@@ -263,12 +260,6 @@ function Table({
>
{ac.iban}
</td>
- <td
- onClick={(): void => onSelect(acc)}
- style={{ cursor: "pointer" }}
- >
- {ac.bic ?? ""}
- </td>
<td class="is-actions-cell right-sticky">
<div class="buttons is-right">
<button
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx
index 70942fd55..9116aaa62 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx
@@ -88,51 +88,55 @@ export default function UpdateValidator({
const resp = await testRevenueAPI(
revenueAPI,
request.credit_facade_credentials,
+ result.body.payto_uri,
);
+ if (resp instanceof TalerError) {
+ setNotif({
+ message: i18n.str`Could not create account`,
+ type: "ERROR",
+ description: i18n.str`The request to check the revenue API failed.`,
+ details: JSON.stringify(resp.errorDetail, undefined, 2),
+ });
+ return;
+ }
if (resp.type === "fail") {
switch (resp.case) {
- case TestRevenueErrorType.NO_CONFIG: {
- setNotif({
- message: i18n.str`Could not create account`,
- type: "ERROR",
- description: i18n.str`The endpoint doesn't seems to be a Taler Revenue API`,
- });
- return;
- }
- case TestRevenueErrorType.CLIENT_BAD_REQUEST: {
+ case HttpStatusCode.BadRequest: {
setNotif({
message: i18n.str`Could not create account`,
type: "ERROR",
description: i18n.str`Server replied with "bad request".`,
});
return;
+
}
- case TestRevenueErrorType.UNAUTHORIZED: {
+ case HttpStatusCode.Unauthorized: {
setNotif({
message: i18n.str`Could not create account`,
type: "ERROR",
description: i18n.str`Unauthorized, try with another credentials.`,
});
return;
+
}
- case TestRevenueErrorType.NOT_FOUND: {
+ case HttpStatusCode.NotFound: {
setNotif({
message: i18n.str`Could not create account`,
type: "ERROR",
- description: i18n.str`Check facade URL, server replied with "not found".`,
+ description: i18n.str`The endpoint doesn't seems to be a Taler Revenue API`,
});
return;
}
- case TestRevenueErrorType.GENERIC_ERROR: {
+ case TestRevenueErrorType.ANOTHER_ACCOUNT: {
setNotif({
- message: i18n.str`Could not create account`,
+ message: i18n.str`Could not add bank account`,
type: "ERROR",
- description: resp.detail.hint,
+ description: i18n.str`The account info URL returned information from an account which is not the same in the account form: ${resp.detail.hint}`,
});
return;
}
default: {
- assertUnreachable(resp.case)
+ assertUnreachable(resp);
}
}
}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx
index d5522c2d4..a16817bab 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx
@@ -101,7 +101,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
/>
<Input<Entity>
name="otp_device_description"
- label={i18n.str`Descripiton`}
+ label={i18n.str`Description`}
tooltip={i18n.str`Useful to identify the device physically`}
/>
<InputSelector<Entity>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx
index afe3c98e2..e4206ff7d 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx
@@ -52,7 +52,7 @@ export function CardTable({
<header class="card-header">
<p class="card-header-title">
<span class="icon">
- <i class="mdi mdi-newspaper" />
+ <i class="mdi mdi-lock" />
</span>
<i18n.Translate>OTP Devices</i18n.Translate>
</p>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx
index 9d5701fa7..39e2fd0c7 100644
--- 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
index 78d7c83ac..336a336ed 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx
@@ -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,29 @@ 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,
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/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx
index 082e622e3..4c55bae2a 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx
@@ -56,7 +56,7 @@ export function CardTable({
<header class="card-header">
<p class="card-header-title">
<span class="icon">
- <i class="mdi mdi-newspaper" />
+ <i class="mdi mdi-qrcode" />
</span>
<i18n.Translate>Templates</i18n.Translate>
</p>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx
index 9e59609c7..fce14dcc3 100644
--- 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
index 7322ca169..547996ea1 100644
--- 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
index eedb77f28..d284fda67 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx
@@ -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,28 @@ 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,
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
index 360c9d373..5b1404b55 100644
--- 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/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx
index 919285e78..877bd30e5 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx
@@ -52,7 +52,7 @@ export function CardTable({
<header class="card-header">
<p class="card-header-title">
<span class="icon">
- <i class="mdi mdi-newspaper" />
+ <i class="mdi mdi-webhook" />
</span>
<i18n.Translate>Webhooks</i18n.Translate>
</p>
diff --git a/packages/pogen/package.json b/packages/pogen/package.json
index 24edc348b..81d66125f 100644
--- a/packages/pogen/package.json
+++ b/packages/pogen/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/pogen",
- "version": "0.10.7",
+ "version": "0.11.4",
"bin": {
"pogen": "bin/pogen"
},
diff --git a/packages/taler-harness/debian/changelog b/packages/taler-harness/debian/changelog
index 269c6b99d..a891cc7ba 100644
--- a/packages/taler-harness/debian/changelog
+++ b/packages/taler-harness/debian/changelog
@@ -1,3 +1,27 @@
+taler-harness (0.11.4) unstable; urgency=low
+
+ * Release 0.11.4
+
+ -- Florian Dold <dold@taler.net> Mon, 10 Jun 2024 19:57:55 +0200
+
+taler-harness (0.11.3) unstable; urgency=low
+
+ * Release 0.11.3
+
+ -- Florian Dold <dold@taler.net> Fri, 07 Jun 2024 19:12:44 +0200
+
+taler-harness (0.11.2) unstable; urgency=low
+
+ * Release 0.11.2
+
+ -- Florian Dold <dold@taler.net> Wed, 05 Jun 2024 20:17:56 +0200
+
+taler-harness (0.11.1) unstable; urgency=low
+
+ * Release 0.11.1
+
+ -- Florian Dold <dold@taler.net> Mon, 27 May 2024 14:46:35 -0600
+
taler-harness (0.10.7) unstable; urgency=low
* Release 0.10.7
diff --git a/packages/taler-harness/package.json b/packages/taler-harness/package.json
index 38d640f51..bca870c8b 100644
--- a/packages/taler-harness/package.json
+++ b/packages/taler-harness/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-harness",
- "version": "0.10.7",
+ "version": "0.11.4",
"description": "",
"engines": {
"node": ">=0.12.0"
diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts
index b27eaa371..4fc462ddf 100644
--- 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,
@@ -273,6 +274,7 @@ export class GlobalTestState {
procs: ProcessWrapper[];
servers: http.Server[];
inShutdown: boolean = false;
+ stepSet: Set<string> = new Set();
constructor(params: GlobalTestParams) {
this.testDir = params.testDir;
this.procs = [];
@@ -422,6 +424,9 @@ export class GlobalTestState {
// Now we just log, later we may report the steps that were done
// to easily see where the test hangs.
console.info(`STEP: ${stepName}`);
+ if (this.stepSet.has(stepName)) {
+ throw Error(`duplicate step (${stepName})`);
+ }
}
}
@@ -779,6 +784,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 +903,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 +1792,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 +2256,6 @@ export function generateRandomTestIban(salt: string | null = null): string {
}
export function getWireMethodForTest(): string {
- if (useLibeufinBank) return "iban";
return "x-taler-bank";
}
@@ -2243,10 +2264,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
index ea9047d0b..d194b0d36 100644
--- 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;
@@ -100,6 +116,8 @@ export interface EnvOptions {
mixedAgeRestriction?: boolean;
+ skipWireFeeCreation?: boolean;
+
additionalExchangeConfig?(e: ExchangeService): void;
additionalMerchantConfig?(m: MerchantService): void;
additionalBankConfig?(b: BankService): void;
@@ -130,12 +148,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 +312,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 +420,160 @@ 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: HarnessExchangeBankAccount = {
+ wireGatewayApiBaseUrl,
+ accountName: exchangeBankUsername,
+ accountPassword: exchangeBankPassword,
+ accountPaytoUri: exchangePaytoUri,
+ skipWireFeeCreation: opts.skipWireFeeCreation === true,
+ };
+
+ 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 +629,7 @@ export async function createWalletDaemonWithClient(
export interface FaultyMerchantTestEnvironment {
commonDb: DbInfo;
- bank: BankService;
+ bank: FakebankService;
exchange: ExchangeService;
faultyExchange: FaultInjectedExchangeService;
exchangeBankAccount: HarnessExchangeBankAccount;
@@ -466,6 +638,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 +657,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 +803,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 +904,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
index 315173b7f..99b5502d8 100644
--- 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
index d36ba0e61..a0e97c218 100644
--- 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
index bba571328..85bd96034 100644
--- 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
index 244de1972..e822b15d8 100644
--- 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
index aea59b706..c9faa586a 100644
--- 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
index 9c5b06397..58f8bb106 100644
--- 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
index a424e0101..01be6ea80 100644
--- 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
index a5ad382a7..c104edc85 100644
--- 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
index 058941e16..34d18d87d 100644
--- a/packages/taler-harness/src/integrationtests/test-currency-scope.ts
+++ b/packages/taler-harness/src/integrationtests/test-currency-scope.ts
@@ -17,13 +17,14 @@
/**
* Imports.
*/
-import { Duration, j2s } from "@gnu-taler/taler-util";
+import { Duration, TalerCorebankApiClient, j2s } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { defaultCoinConfig } from "../harness/denomStructures.js";
import {
BankService,
ExchangeService,
GlobalTestState,
+ HarnessExchangeBankAccount,
MerchantService,
generateRandomPayto,
setupDb,
@@ -31,6 +32,7 @@ import {
import {
createWalletDaemonWithClient,
withdrawViaBankV2,
+ withdrawViaBankV3,
} from "../harness/helpers.js";
/**
@@ -72,17 +74,25 @@ export async function runCurrencyScopeTest(t: GlobalTestState) {
database: dbDefault.connStr,
});
- const exchangeOneBankAccount = await bank.createExchangeAccount(
- "myexchange",
- "x",
- );
- await exchangeOne.addBankAccount("1", exchangeOneBankAccount);
-
- const exchangeTwoBankAccount = await bank.createExchangeAccount(
- "myexchange2",
- "x",
- );
- await exchangeTwo.addBankAccount("1", exchangeTwoBankAccount);
+ let exchangeOneBankAccount: HarnessExchangeBankAccount = {
+ wireGatewayApiBaseUrl: new URL(
+ "accounts/myexchange/taler-wire-gateway/",
+ bank.corebankApiBaseUrl,
+ ).href,
+ accountName: "myexchange",
+ accountPassword: "x",
+ accountPaytoUri: generateRandomPayto("myexchange"),
+ };
+
+ let exchangeTwoBankAccount: HarnessExchangeBankAccount = {
+ wireGatewayApiBaseUrl: new URL(
+ "accounts/myexchange2/taler-wire-gateway/",
+ bank.corebankApiBaseUrl,
+ ).href,
+ accountName: "myexchange2",
+ accountPassword: "x",
+ accountPaytoUri: generateRandomPayto("myexchange2"),
+ };
bank.setSuggestedExchange(
exchangeOne,
@@ -93,6 +103,31 @@ export async function runCurrencyScopeTest(t: GlobalTestState) {
await bank.pingUntilAvailable();
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, {
+ auth: {
+ username: "admin",
+ password: "adminpw",
+ },
+ });
+
+ await bankClient.registerAccountExtended({
+ name: exchangeOneBankAccount.accountName,
+ username: exchangeOneBankAccount.accountName,
+ password: exchangeOneBankAccount.accountPassword,
+ is_taler_exchange: true,
+ payto_uri: exchangeOneBankAccount.accountPaytoUri,
+ });
+ await exchangeOne.addBankAccount("1", exchangeOneBankAccount);
+
+ await bankClient.registerAccountExtended({
+ name: exchangeTwoBankAccount.accountName,
+ username: exchangeTwoBankAccount.accountName,
+ password: exchangeTwoBankAccount.accountPassword,
+ is_taler_exchange: true,
+ payto_uri: exchangeTwoBankAccount.accountPaytoUri,
+ });
+ await exchangeTwo.addBankAccount("1", exchangeTwoBankAccount);
+
// Set up the first exchange
exchangeOne.addOfferedCoins(defaultCoinConfig);
@@ -139,16 +174,16 @@ export async function runCurrencyScopeTest(t: GlobalTestState) {
// Withdraw digital cash into the wallet.
- const w1 = await withdrawViaBankV2(t, {
+ const w1 = await withdrawViaBankV3(t, {
walletClient,
- bank,
+ bankClient,
exchange: exchangeOne,
amount: "TESTKUDOS:6",
});
- const w2 = await withdrawViaBankV2(t, {
+ const w2 = await withdrawViaBankV3(t, {
walletClient,
- bank,
+ bankClient,
exchange: exchangeTwo,
amount: "TESTKUDOS:6",
});
diff --git a/packages/taler-harness/src/integrationtests/test-denom-lost.ts b/packages/taler-harness/src/integrationtests/test-denom-lost.ts
index 307ae352a..b57518437 100644
--- 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
index 6a82d586c..8042c0817 100644
--- 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
index 74b318226..0879c9e9f 100644
--- 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
index d3bd022ae..801162ac8 100644
--- 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
index 9d3c1d867..072e9736d 100644
--- 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
index 714a7f879..4f2fb1ee4 100644
--- 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
index f164606c4..6ae7b5de8 100644
--- 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
index a9ef654fd..213dd9df4 100644
--- 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
index 3900779e2..01b20ddbf 100644
--- 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
index 35e3267b1..19f89ae2c 100644
--- 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
index bd63a8445..656fc4ded 100644
--- 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
index 7ee4c977b..1d712f745 100644
--- 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
index 8e664dfa9..8a22eae57 100644
--- 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
index e27bccc46..26e843073 100644
--- a/packages/taler-harness/src/integrationtests/test-multiexchange.ts
+++ b/packages/taler-harness/src/integrationtests/test-multiexchange.ts
@@ -17,13 +17,14 @@
/**
* Imports.
*/
-import { Duration } from "@gnu-taler/taler-util";
+import { Duration, TalerCorebankApiClient, TalerMerchantApi } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { defaultCoinConfig } from "../harness/denomStructures.js";
import {
- BankService,
+ BankService,
ExchangeService,
GlobalTestState,
+ HarnessExchangeBankAccount,
MerchantService,
generateRandomPayto,
setupDb,
@@ -32,6 +33,7 @@ import {
createWalletDaemonWithClient,
makeTestPaymentV2,
withdrawViaBankV2,
+ withdrawViaBankV3,
} from "../harness/helpers.js";
/**
@@ -73,17 +75,25 @@ export async function runMultiExchangeTest(t: GlobalTestState) {
database: dbDefault.connStr,
});
- const exchangeOneBankAccount = await bank.createExchangeAccount(
- "myexchange",
- "x",
- );
- await exchangeOne.addBankAccount("1", exchangeOneBankAccount);
+ let exchangeOneBankAccount: HarnessExchangeBankAccount = {
+ wireGatewayApiBaseUrl: new URL(
+ "accounts/myexchange/taler-wire-gateway/",
+ bank.corebankApiBaseUrl,
+ ).href,
+ accountName: "myexchange",
+ accountPassword: "x",
+ accountPaytoUri: generateRandomPayto("myexchange"),
+ };
- const exchangeTwoBankAccount = await bank.createExchangeAccount(
- "myexchange2",
- "x",
- );
- await exchangeTwo.addBankAccount("1", exchangeTwoBankAccount);
+ let exchangeTwoBankAccount: HarnessExchangeBankAccount = {
+ wireGatewayApiBaseUrl: new URL(
+ "accounts/myexchange2/taler-wire-gateway/",
+ bank.corebankApiBaseUrl,
+ ).href,
+ accountName: "myexchange2",
+ accountPassword: "x",
+ accountPaytoUri: generateRandomPayto("myexchange2"),
+ };
bank.setSuggestedExchange(
exchangeOne,
@@ -94,6 +104,31 @@ export async function runMultiExchangeTest(t: GlobalTestState) {
await bank.pingUntilAvailable();
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, {
+ auth: {
+ username: "admin",
+ password: "adminpw",
+ },
+ });
+
+ await bankClient.registerAccountExtended({
+ name: exchangeOneBankAccount.accountName,
+ username: exchangeOneBankAccount.accountName,
+ password: exchangeOneBankAccount.accountPassword,
+ is_taler_exchange: true,
+ payto_uri: exchangeOneBankAccount.accountPaytoUri,
+ });
+ await exchangeOne.addBankAccount("1", exchangeOneBankAccount);
+
+ await bankClient.registerAccountExtended({
+ name: exchangeTwoBankAccount.accountName,
+ username: exchangeTwoBankAccount.accountName,
+ password: exchangeTwoBankAccount.accountPassword,
+ is_taler_exchange: true,
+ payto_uri: exchangeTwoBankAccount.accountPaytoUri,
+ });
+ await exchangeTwo.addBankAccount("1", exchangeTwoBankAccount);
+
// Set up the first exchange
exchangeOne.addOfferedCoins(defaultCoinConfig);
@@ -141,23 +176,23 @@ export async function runMultiExchangeTest(t: GlobalTestState) {
// Withdraw digital cash into the wallet.
- await withdrawViaBankV2(t, {
+ await withdrawViaBankV3(t, {
walletClient,
- bank,
+ bankClient,
exchange: exchangeOne,
amount: "TESTKUDOS:6",
});
- await withdrawViaBankV2(t, {
+ await withdrawViaBankV3(t, {
walletClient,
- bank,
+ bankClient,
exchange: exchangeTwo,
amount: "TESTKUDOS:6",
});
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
index d0aeba095..4fcc8c6e9 100644
--- 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
index 3595a1750..dfadd9539 100644
--- 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
index 1d25848fd..bab8a4df1 100644
--- 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
index a837b18fa..3f1f7f2dd 100644
--- 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
index cadcc9056..dabe42a6b 100644
--- 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
index 21d76397d..827c299a4 100644
--- 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
index 65fd3a562..4a8e95af3 100644
--- 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
index 0caa3c3e7..3c902ee17 100644
--- 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
index 141faa81e..25cfb50c6 100644
--- 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
index c3ab5ffc8..af92d43c5 100644
--- 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,28 +55,47 @@ 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`,
- templateParams: {},
+ talerPayTemplateUri,
+ templateParams: {
+ amount: "TESTKUDOS:1",
+ },
},
);
diff --git a/packages/taler-harness/src/integrationtests/test-payment-zero.ts b/packages/taler-harness/src/integrationtests/test-payment-zero.ts
index 7423751a5..3a74a9cf2 100644
--- 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
index 9d1ce0e22..5da6d608d 100644
--- 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
index 247ec9cad..de3961dec 100644
--- 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
index 8bc7ed0a1..22d3fe7ad 100644
--- 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
index e1565f295..d94c5985f 100644
--- 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
index 21e0d384a..e38b690ab 100644
--- 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
index 2a2e26ea4..6e02071af 100644
--- a/packages/taler-harness/src/integrationtests/test-refund-auto.ts
+++ b/packages/taler-harness/src/integrationtests/test-refund-auto.ts
@@ -17,12 +17,17 @@
/**
* Imports.
*/
-import { Duration, MerchantApiClient } from "@gnu-taler/taler-util";
+import {
+ Duration,
+ MerchantApiClient,
+ TransactionMajorState,
+ TransactionMinorState,
+} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { GlobalTestState } from "../harness/harness.js";
import {
- createSimpleTestkudosEnvironmentV2,
- withdrawViaBankV2,
+ createSimpleTestkudosEnvironmentV3,
+ withdrawViaBankV3,
} from "../harness/helpers.js";
/**
@@ -31,83 +36,150 @@ 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",
});
await wres.withdrawalFinishedCond;
- // Set up order.
- const orderResp = await merchantClient.createOrder({
- order: {
- summary: "Buy me!",
- amount: "TESTKUDOS:5",
- fulfillment_url: "taler://fulfillment-success/thx",
- auto_refund: {
- d_us: 3000 * 1000,
+ // Test case where the auto-refund happens
+ {
+ // Set up order.
+ const orderResp = await merchantClient.createOrder({
+ order: {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ auto_refund: {
+ d_us: 3000 * 1000,
+ },
},
- },
- refund_delay: Duration.toTalerProtocolDuration(
- Duration.fromSpec({ minutes: 5 }),
- ),
- });
+ refund_delay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 5 }),
+ ),
+ });
- let orderStatus = await merchantClient.queryPrivateOrderStatus({
- orderId: orderResp.order_id,
- });
+ let orderStatus = await merchantClient.queryPrivateOrderStatus({
+ orderId: orderResp.order_id,
+ });
- t.assertTrue(orderStatus.order_status === "unpaid");
+ t.assertTrue(orderStatus.order_status === "unpaid");
- // Make wallet pay for the order
+ // Make wallet pay for the order
- const r1 = await walletClient.call(WalletApiOperation.PreparePayForUri, {
- talerPayUri: orderStatus.taler_pay_uri,
- });
+ const r1 = await walletClient.call(WalletApiOperation.PreparePayForUri, {
+ talerPayUri: orderStatus.taler_pay_uri,
+ });
- await walletClient.call(WalletApiOperation.ConfirmPay, {
- transactionId: r1.transactionId,
- });
+ await walletClient.call(WalletApiOperation.ConfirmPay, {
+ transactionId: r1.transactionId,
+ });
- // Check if payment was successful.
+ // Check if payment was successful.
- orderStatus = await merchantClient.queryPrivateOrderStatus({
- orderId: orderResp.order_id,
- });
+ orderStatus = await merchantClient.queryPrivateOrderStatus({
+ orderId: orderResp.order_id,
+ });
- t.assertTrue(orderStatus.order_status === "paid");
+ t.assertTrue(orderStatus.order_status === "paid");
- const ref = await merchantClient.giveRefund({
- amount: "TESTKUDOS:5",
- instance: "default",
- justification: "foo",
- orderId: orderResp.order_id,
- });
+ const ref = await merchantClient.giveRefund({
+ amount: "TESTKUDOS:5",
+ instance: "default",
+ justification: "foo",
+ orderId: orderResp.order_id,
+ });
+
+ console.log(ref);
+
+ // The wallet should now automatically pick up the refund.
+ await walletClient.call(
+ WalletApiOperation.TestingWaitTransactionsFinal,
+ {},
+ );
+
+ const transactions = await walletClient.call(
+ WalletApiOperation.GetTransactions,
+ {
+ sort: "stable-ascending",
+ },
+ );
+ console.log(JSON.stringify(transactions, undefined, 2));
+
+ const transactionTypes = transactions.transactions.map((x) => x.type);
+ t.assertDeepEqual(transactionTypes, ["withdrawal", "payment", "refund"]);
+ }
+
+ // Now test the case where the auto-refund just expires
+
+ {
+ // Set up order.
+ const orderResp = await merchantClient.createOrder({
+ order: {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ auto_refund: {
+ d_us: 3000 * 1000,
+ },
+ },
+ refund_delay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 5 }),
+ ),
+ });
+
+ let orderStatus = await merchantClient.queryPrivateOrderStatus({
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
- console.log(ref);
+ // Make wallet pay for the order
- // The wallet should now automatically pick up the refund.
- await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+ const r1 = await walletClient.call(WalletApiOperation.PreparePayForUri, {
+ talerPayUri: orderStatus.taler_pay_uri,
+ });
- const transactions = await walletClient.call(
- WalletApiOperation.GetTransactions,
- {},
- );
- console.log(JSON.stringify(transactions, undefined, 2));
+ await walletClient.call(WalletApiOperation.ConfirmPay, {
+ transactionId: r1.transactionId,
+ });
- const transactionTypes = transactions.transactions.map((x) => x.type);
- t.assertDeepEqual(transactionTypes, ["withdrawal", "payment", "refund"]);
+ // Check if payment was successful.
- await t.shutdown();
+ orderStatus = await merchantClient.queryPrivateOrderStatus({
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "paid");
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: r1.transactionId,
+ txState: {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.AutoRefund,
+ },
+ });
+ // Only time-travel the wallet
+ await walletClient.call(WalletApiOperation.TestingSetTimetravel, {
+ offsetMs: 5000,
+ });
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: r1.transactionId,
+ txState: {
+ major: TransactionMajorState.Done,
+ },
+ });
+ }
}
runRefundAutoTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-refund-gone.ts b/packages/taler-harness/src/integrationtests/test-refund-gone.ts
index 8a661868f..ac3a5aebe 100644
--- 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
index 8a5d23315..2f78d7e91 100644
--- 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
index 999a9b621..4b197a01f 100644
--- 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
index ac118e4eb..65aa86f98 100644
--- 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
index 58ab61435..846b8c8e1 100644
--- 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
index a3a5e6ca3..732ac0aed 100644
--- 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
index e144683cb..046bd5aed 100644
--- a/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts
+++ b/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts
@@ -23,6 +23,8 @@ import {
MerchantApiClient,
NotificationType,
PreparePayResultType,
+ TalerCorebankApiClient,
+ j2s,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { makeNoFeeCoinConfig } from "../harness/denomStructures.js";
@@ -37,7 +39,7 @@ import {
import {
applyTimeTravelV2,
createWalletDaemonWithClient,
- withdrawViaBankV2,
+ withdrawViaBankV3,
} from "../harness/helpers.js";
/**
@@ -69,18 +71,42 @@ 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();
@@ -107,29 +133,42 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) {
const { walletClient } = await createWalletDaemonWithClient(t, {
name: "w1",
+ persistent: true,
});
const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
- // Withdraw digital cash into the wallet.
+ t.logStep("exchangeUpdated1Cond");
- const wres = await withdrawViaBankV2(t, {
+ // Withdraw digital cash into the wallet.
+ t.logStep("Withdraw digital cash into the wallet.");
+ const wres = await withdrawViaBankV3(t, {
walletClient,
- bank,
+ bankClient,
exchange,
amount: "TESTKUDOS:15",
});
+ t.logStep("wait");
await wres.withdrawalFinishedCond;
-
const exchangeUpdated1Cond = walletClient.waitForNotificationCond(
(x) =>
- x.type === NotificationType.ExchangeStateTransition &&
- x.exchangeBaseUrl === exchange.baseUrl,
+ {
+ t.logStep(`EXCHANGE UPDATE, ${j2s(x)}`)
+ return x.type === NotificationType.ExchangeStateTransition &&
+ x.exchangeBaseUrl === exchange.baseUrl
+ }
);
+ t.logStep("waiting tx");
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+ {
+ const balance = await walletClient.call(WalletApiOperation.GetBalances, {});
+ t.assertAmountEquals(balance.balances[0].available, "TESTKUDOS:15");
+ }
+
// Travel into the future, the deposit expiration is two years
// into the future.
- console.log("applying first time travel");
+ t.logStep("applying first time travel");
await applyTimeTravelV2(
Duration.toMilliseconds(Duration.fromSpec({ days: 400 })),
{
@@ -140,19 +179,32 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) {
);
// The time travel should cause exchanges to update.
+ t.logStep("The time travel should cause exchanges to update");
await exchangeUpdated1Cond;
+ t.logStep("exchange updated, waiting for tx");
await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+ {
+ const balance = await walletClient.call(WalletApiOperation.GetBalances, {});
+ t.assertAmountEquals(balance.balances[0].available, "TESTKUDOS:15");
+ }
- const wres2 = await withdrawViaBankV2(t, {
+ t.logStep("withdrawing second time");
+ const wres2 = await withdrawViaBankV3(t, {
walletClient,
- bank,
+ bankClient,
exchange,
amount: "TESTKUDOS:20",
});
await wres2.withdrawalFinishedCond;
+ t.logStep("witdrawn, waiting tx");
await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+ {
+ const balance = await walletClient.call(WalletApiOperation.GetBalances, {});
+ t.assertAmountEquals(balance.balances[0].available, "TESTKUDOS:35");
+ }
+
const exchangeUpdated2Cond = walletClient.waitForNotificationCond(
(x) =>
x.type === NotificationType.ExchangeStateTransition &&
@@ -161,7 +213,7 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) {
// Travel into the future, the deposit expiration is two years
// into the future.
- console.log("applying second time travel");
+ t.logStep("applying second time travel");
await applyTimeTravelV2(
Duration.toMilliseconds(Duration.fromSpec({ years: 2, months: 6 })),
{
@@ -172,8 +224,13 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) {
);
// The time travel should cause exchanges to update.
+ t.logStep("The time travel should cause exchanges to update.");
await exchangeUpdated2Cond;
await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+ {
+ const balance = await walletClient.call(WalletApiOperation.GetBalances, {});
+ t.assertAmountEquals(balance.balances[0].available, "TESTKUDOS:35");
+ }
// At this point, the original coins should've been refreshed.
// It would be too late to refresh them now, as we're past
diff --git a/packages/taler-harness/src/integrationtests/test-timetravel-withdraw.ts b/packages/taler-harness/src/integrationtests/test-timetravel-withdraw.ts
index 9cd0beb42..4ee3a86e9 100644
--- 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
index cb4a50a2b..94d43e195 100644
--- 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
index c761c4fb0..abcd71a3b 100644
--- 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
index 290ef7e2d..1586e2a72 100644
--- 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
index 7d65b60cf..01cf7c159 100644
--- 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
index eb7359781..c37a6e482 100644
--- 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
index 69b721789..66f985114 100644
--- 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
index 4f015799f..bcd7de74b 100644
--- 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
index 3341b6a53..ba2b2670c 100644
--- 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
index 4ce8cde4c..a2573eda1 100644
--- a/packages/taler-harness/src/integrationtests/test-wallet-denom-expire.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-denom-expire.ts
@@ -21,17 +21,13 @@ import { Duration, Logger, NotificationType, j2s } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { makeNoFeeCoinConfig } from "../harness/denomStructures.js";
import {
- BankService,
- ExchangeService,
GlobalTestState,
- MerchantService,
- generateRandomPayto,
setupDb,
} from "../harness/harness.js";
import {
applyTimeTravelV2,
- createWalletDaemonWithClient,
- withdrawViaBankV2,
+ createSimpleTestkudosEnvironmentV3,
+ withdrawViaBankV3,
} from "../harness/helpers.js";
const logger = new Logger("test-exchange-timetravel.ts");
@@ -44,74 +40,20 @@ export async function runWalletDenomExpireTest(t: GlobalTestState) {
const db = await setupDb(t);
- const bank = await BankService.create(t, {
- allowRegistrations: true,
- currency: "TESTKUDOS",
- database: db.connStr,
- httpPort: 8082,
- });
-
- const exchange = ExchangeService.create(t, {
- name: "testexchange-1",
- currency: "TESTKUDOS",
- httpPort: 8081,
- database: db.connStr,
- });
-
- const merchant = await MerchantService.create(t, {
- name: "testmerchant-1",
- currency: "TESTKUDOS",
- httpPort: 8083,
- database: db.connStr,
- });
-
- const exchangeBankAccount = await bank.createExchangeAccount(
- "myexchange",
- "x",
- );
- exchange.addBankAccount("1", exchangeBankAccount);
-
- bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri);
-
- await bank.start();
-
- await bank.pingUntilAvailable();
-
- exchange.addCoinConfigList(makeNoFeeCoinConfig("TESTKUDOS"));
-
- await exchange.start();
- await exchange.pingUntilAvailable();
+ const coinConfig = makeNoFeeCoinConfig("TESTKUDOS");
- merchant.addExchange(exchange);
-
- await merchant.start();
- await merchant.pingUntilAvailable();
-
- console.log("merchant started, configuring instances");
-
- await merchant.addInstanceWithWireAccount({
- id: "default",
- name: "Default Instance",
- paytoUris: [generateRandomPayto("merchant-default")],
- });
-
- await merchant.addInstanceWithWireAccount({
- id: "minst1",
- name: "minst1",
- paytoUris: [generateRandomPayto("minst1")],
- });
-
- console.log("setup done!");
-
- const { walletClient } = await createWalletDaemonWithClient(t, {
- name: "default",
- });
+ const {
+ walletClient,
+ bankClient,
+ exchange,
+ merchant,
+ } = await createSimpleTestkudosEnvironmentV3(t, coinConfig, {});
// Withdraw digital cash into the wallet.
- 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
index 3251750da..3a1b467c3 100644
--- a/packages/taler-harness/src/integrationtests/test-wallet-exchange-update.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-exchange-update.ts
@@ -21,6 +21,7 @@ import {
AmountString,
ExchangeUpdateStatus,
NotificationType,
+ TalerCorebankApiClient,
j2s,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
@@ -28,12 +29,16 @@ import { defaultCoinConfig } from "../harness/denomStructures.js";
import {
BankService,
ExchangeService,
+ FakebankService,
GlobalTestState,
+ HarnessExchangeBankAccount,
+ generateRandomPayto,
setupDb,
} from "../harness/harness.js";
import {
createWalletDaemonWithClient,
withdrawViaBankV2,
+ withdrawViaBankV3,
} from "../harness/helpers.js";
/**
@@ -74,10 +79,27 @@ export async function runWalletExchangeUpdateTest(
database: db2.connStr,
});
- const exchangeBankAccount = await bank.createExchangeAccount(
- "myexchange",
- "x",
- );
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, {
+ auth: {
+ username: "admin",
+ password: "adminpw",
+ },
+ });
+
+ // const exchangeBankAccount = await bank.createExchangeAccount(
+ // "myexchange",
+ // "x",
+ // );
+
+ let exchangeBankAccount: HarnessExchangeBankAccount = {
+ wireGatewayApiBaseUrl: new URL(
+ "accounts/myexchange/taler-wire-gateway/",
+ bank.corebankApiBaseUrl,
+ ).href,
+ accountName: "myexchange",
+ accountPassword: "x",
+ accountPaytoUri: generateRandomPayto("myexchange"),
+ };
await exchangeOne.addBankAccount("1", exchangeBankAccount);
await exchangeTwo.addBankAccount("1", exchangeBankAccount);
@@ -87,6 +109,14 @@ export async function runWalletExchangeUpdateTest(
await bank.start();
+ bankClient.registerAccountExtended({
+ name: exchangeBankAccount.accountName,
+ username: exchangeBankAccount.accountName,
+ password: exchangeBankAccount.accountPassword,
+ is_taler_exchange: true,
+ payto_uri: exchangeBankAccount.accountPaytoUri,
+ });
+
exchangeOne.addCoinConfigList(defaultCoinConfig.map((x) => x("TESTKUDOS")));
exchangeTwo.addCoinConfigList(defaultCoinConfig.map((x) => x("TESTKUDOS")));
@@ -107,9 +137,9 @@ export async function runWalletExchangeUpdateTest(
t.assertDeepEqual(exchangesListResult.exchanges.length, 0);
- const wres = await withdrawViaBankV2(t, {
+ const wres = await withdrawViaBankV3(t, {
walletClient,
- bank,
+ bankClient,
exchange: exchangeOne,
amount: "TESTKUDOS:10",
});
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-gendb.ts b/packages/taler-harness/src/integrationtests/test-wallet-gendb.ts
index 9e3b60899..778f36432 100644
--- 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-insufficient-balance.ts b/packages/taler-harness/src/integrationtests/test-wallet-insufficient-balance.ts
index ac1244446..4062e186d 100644
--- a/packages/taler-harness/src/integrationtests/test-wallet-insufficient-balance.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-insufficient-balance.ts
@@ -22,71 +22,32 @@ import {
Duration,
PaymentInsufficientBalanceDetails,
TalerErrorCode,
- WalletNotification,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
import {
- ExchangeService,
- FakebankService,
GlobalTestState,
- MerchantService,
- WalletClient,
- WalletService,
generateRandomPayto,
setupDb,
} from "../harness/harness.js";
-import { withdrawViaBankV2 } from "../harness/helpers.js";
+import { createSimpleTestkudosEnvironmentV3, withdrawViaBankV3 } from "../harness/helpers.js";
export async function runWalletInsufficientBalanceTest(t: GlobalTestState) {
// Set up test environment
const db = await setupDb(t);
- const bank = await FakebankService.create(t, {
- allowRegistrations: true,
- currency: "TESTKUDOS",
- database: db.connStr,
- httpPort: 8082,
- });
-
- const exchange = ExchangeService.create(t, {
- name: "testexchange-1",
- currency: "TESTKUDOS",
- httpPort: 8081,
- database: db.connStr,
- });
-
- const merchant = await MerchantService.create(t, {
- name: "testmerchant-1",
- currency: "TESTKUDOS",
- httpPort: 8083,
- database: db.connStr,
- });
-
- const exchangeBankAccount = await bank.createExchangeAccount(
- "myexchange",
- "x",
- );
- exchangeBankAccount.skipWireFeeCreation = true;
- exchange.addBankAccount("1", exchangeBankAccount);
-
- bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri);
-
- await bank.start();
-
- await bank.pingUntilAvailable();
-
const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS"));
- exchange.addCoinConfigList(coinConfig);
-
- await exchange.start();
- await exchange.pingUntilAvailable();
- merchant.addExchange(exchange);
-
- await merchant.start();
- await merchant.pingUntilAvailable();
+ let {
+ bankClient,
+ exchange,
+ merchant,
+ walletService,
+ walletClient,
+ } = await createSimpleTestkudosEnvironmentV3(t, coinConfig, {
+ skipWireFeeCreation: true,
+ });
await merchant.addInstanceWithWireAccount({
id: "default",
@@ -106,24 +67,6 @@ export async function runWalletInsufficientBalanceTest(t: GlobalTestState) {
),
});
- const walletService = new WalletService(t, {
- name: "wallet",
- useInMemoryDb: true,
- });
- await walletService.start();
- await walletService.pingUntilAvailable();
-
- const allNotifications: WalletNotification[] = [];
-
- const walletClient = new WalletClient({
- name: "wallet",
- unixPath: walletService.socketPath,
- onNotification(n) {
- console.log("got notification", n);
- allNotifications.push(n);
- },
- });
- await walletClient.connect();
await walletClient.client.call(WalletApiOperation.InitWallet, {
config: {
testing: {
@@ -132,9 +75,9 @@ export async function runWalletInsufficientBalanceTest(t: GlobalTestState) {
},
});
- const wres = await withdrawViaBankV2(t, {
+ const wres = await withdrawViaBankV3(t, {
amount: "TESTKUDOS:10",
- bank,
+ bankClient,
exchange,
walletClient,
});
@@ -146,10 +89,12 @@ export async function runWalletInsufficientBalanceTest(t: GlobalTestState) {
depositPaytoUri: "payto://x-taler-bank/localhost/foobar",
});
});
+
t.assertDeepEqual(
exc.errorDetail.code,
TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE,
);
+
const insufficientBalanceDetails: PaymentInsufficientBalanceDetails =
exc.errorDetail.insufficientBalanceDetails;
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-notifications.ts b/packages/taler-harness/src/integrationtests/test-wallet-notifications.ts
index 28b73a9f9..5088c8228 100644
--- 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
index 5dff8670e..55a60cb76 100644
--- 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
index f1c544a4e..93fe94270 100644
--- 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
index 1bf9bd659..c5a0fd363 100644
--- 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
index 932284d62..001081532 100644
--- 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
index 39389e3c6..b87e67a68 100644
--- 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
new file mode 100644
index 000000000..cd6a1e325
--- /dev/null
+++ 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
index 76dec50d3..3ec2a3bcd 100644
--- 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,22 +35,18 @@ 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",
);
- // Hand it to the wallet
+ t.logStep("Hand it to the wallet")
const r1 = await walletClient.client.call(
WalletApiOperation.GetWithdrawalDetailsForUri,
@@ -60,7 +55,7 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
},
);
- // Withdraw
+ t.logStep("Withdraw")
const r2 = await walletClient.client.call(
WalletApiOperation.AcceptBankIntegratedWithdrawal,
@@ -70,6 +65,7 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
},
);
+ t.logStep("wait confirmed")
const withdrawalBankConfirmedCond = walletClient.waitForNotificationCond(
(x) => {
return (
@@ -81,6 +77,7 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
},
);
+ t.logStep("wait finished")
const withdrawalFinishedCond = walletClient.waitForNotificationCond((x) => {
return (
x.type === NotificationType.TransactionStateTransition &&
@@ -89,6 +86,7 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
);
});
+ t.logStep("wait withdraw coins")
const withdrawalReserveReadyCond = walletClient.waitForNotificationCond(
(x) => {
return (
@@ -100,7 +98,7 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
},
);
- // Do it twice to check idempotency
+ t.logStep("Do it twice to check idempotency")
const r3 = await walletClient.client.call(
WalletApiOperation.AcceptBankIntegratedWithdrawal,
{
@@ -109,9 +107,10 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
},
);
+ t.logStep("stop wirewatch")
await exchange.stopWirewatch();
- // Check status before withdrawal is confirmed by bank.
+ t.logStep("Check status before withdrawal is confirmed by bank.")
{
const txn = await walletClient.client.call(
WalletApiOperation.GetTransactions,
@@ -127,9 +126,9 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
t.assertTrue(tx0.withdrawalDetails.reserveIsReady === false);
}
- // Confirm it
+ t.logStep("Confirm it")
- await corebankApiClient.confirmWithdrawalOperation(user.username, {
+ await bankClient.confirmWithdrawalOperation(user.username, {
withdrawalOperationId: wop.withdrawal_id,
});
@@ -137,6 +136,7 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
// Check status after withdrawal is confirmed by bank,
// but before funds are wired to the exchange.
+ t.logStep("Check status after withdrawal")
{
const txn = await walletClient.client.call(
WalletApiOperation.GetTransactions,
@@ -152,11 +152,13 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
t.assertTrue(tx0.withdrawalDetails.reserveIsReady === false);
}
+ t.logStep("start wirewatch")
await exchange.startWirewatch();
+ t.logStep("wait reserve")
await withdrawalReserveReadyCond;
- // Check status after funds were wired.
+ t.logStep("Check status after funds were wired.")
{
const txn = await walletClient.client.call(
WalletApiOperation.GetTransactions,
@@ -174,7 +176,7 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
await withdrawalFinishedCond;
- // Check balance
+ t.logStep("Check balance")
const balResp = await walletClient.client.call(
WalletApiOperation.GetBalances,
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-conversion.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-conversion.ts
index 8351e5251..c55e1faf0 100644
--- 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,9 +33,11 @@ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import * as http from "node:http";
import { defaultCoinConfig } from "../harness/denomStructures.js";
import {
- BankService,
+ BankService,
ExchangeService,
+ FakebankService,
GlobalTestState,
+ HarnessExchangeBankAccount,
MerchantService,
generateRandomPayto,
setupDb,
@@ -102,7 +103,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") {
@@ -157,17 +158,40 @@ export async function runWithdrawalConversionTest(t: GlobalTestState) {
database: db.connStr,
});
- const exchangeBankAccount = await bank.createExchangeAccount(
- "myexchange",
- "x",
- );
- exchangeBankAccount.conversionUrl = "http://localhost:8071/";
+ let exchangeBankAccount: HarnessExchangeBankAccount = {
+ wireGatewayApiBaseUrl: new URL(
+ "accounts/myexchange/taler-wire-gateway/",
+ bank.corebankApiBaseUrl,
+ ).href,
+ accountName: "myexchange",
+ accountPassword: "x",
+ accountPaytoUri: generateRandomPayto("myexchange"),
+ conversionUrl: "http://localhost:8071/",
+ };
+
await exchange.addBankAccount("1", exchangeBankAccount);
await bank.start();
await bank.pingUntilAvailable();
+ const bankClientAuth = {
+ username: "admin",
+ password: "adminpw",
+ };
+
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, {
+ auth: bankClientAuth,
+ });
+
+ await bankClient.registerAccountExtended({
+ name: exchangeBankAccount.accountName,
+ username: exchangeBankAccount.accountName,
+ password: exchangeBankAccount.accountPassword,
+ is_taler_exchange: true,
+ payto_uri: exchangeBankAccount.accountPaytoUri,
+ });
+
exchange.addOfferedCoins(defaultCoinConfig);
await exchange.start();
@@ -195,7 +219,7 @@ export async function runWithdrawalConversionTest(t: GlobalTestState) {
),
});
- const { walletClient, walletService } = await createWalletDaemonWithClient(
+ const { walletClient } = await createWalletDaemonWithClient(
t,
{ name: "wallet" },
);
@@ -204,11 +228,7 @@ export async function runWithdrawalConversionTest(t: GlobalTestState) {
// Create a withdrawal operation
- const bankAccessApiClient = new TalerCorebankApiClient(
- bank.corebankApiBaseUrl,
- );
-
- const user = await bankAccessApiClient.createRandomBankUser();
+ const user = await bankClient.createRandomBankUser();
await walletClient.call(WalletApiOperation.AddExchange, {
exchangeBaseUrl: exchange.baseUrl,
@@ -278,10 +298,7 @@ export async function runWithdrawalConversionTest(t: GlobalTestState) {
const wireGatewayApiClient = new WireGatewayApiClient(
exchangeBankAccount.wireGatewayApiBaseUrl,
{
- auth: {
- username: exchangeBankAccount.accountName,
- password: exchangeBankAccount.accountPassword,
- },
+ auth: bankClientAuth,
},
);
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts
index f702376e1..0657d2da7 100644
--- 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,42 @@ 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,17 +134,11 @@ 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(
- user.username,
- amount,
- );
+ const user = await bankClient.createRandomBankUser();
+ bankClient.setAuth(user);
+ const wop = await bankClient.createWithdrawalOperation(user.username, amount);
- // Hand it to the wallet
+ t.logStep("Hand it to the wallet")
const details = await wallet.client.call(
WalletApiOperation.GetWithdrawalDetailsForUri,
@@ -128,10 +149,13 @@ export async function runWithdrawalFeesTest(t: GlobalTestState) {
console.log(j2s(details));
+ const myAmount = details.amount;
+ t.assertTrue(!!myAmount);
+
const amountDetails = await wallet.client.call(
WalletApiOperation.GetWithdrawalDetailsForAmount,
{
- amount: details.amount,
+ amount: myAmount,
exchangeBaseUrl: details.possibleExchanges[0].exchangeBaseUrl,
},
);
@@ -141,23 +165,25 @@ export async function runWithdrawalFeesTest(t: GlobalTestState) {
t.assertAmountEquals(amountDetails.amountEffective, "TESTKUDOS:5");
t.assertAmountEquals(amountDetails.amountRaw, "TESTKUDOS:7.5");
+ t.logStep("Complete all pending operations")
+
await wallet.runPending();
- // Withdraw (AKA select)
+ t.logStep("Withdraw (AKA select)")
await wallet.client.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, {
exchangeBaseUrl: exchange.baseUrl,
talerWithdrawUri: wop.taler_withdraw_uri,
});
- // Confirm it
+ t.logStep("Confirm it")
- await bankAccessApiClient.confirmWithdrawalOperation(user.username, {
+ await bankClient.confirmWithdrawalOperation(user.username, {
withdrawalOperationId: wop.withdrawal_id,
});
await wallet.runUntilDone();
- // Check balance
+ t.logStep("Check balance")
const balResp = await wallet.client.call(WalletApiOperation.GetBalances, {});
console.log(j2s(balResp));
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-flex.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-flex.ts
new file mode 100644
index 000000000..ffc7249b8
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-flex.ts
@@ -0,0 +1,70 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import { j2s } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import { createSimpleTestkudosEnvironmentV3 } from "../harness/helpers.js";
+
+/**
+ * Run test for bank-integrated withdrawal with flexible amount,
+ * i.e. the amount is chosen by the wallet.
+ */
+export async function runWithdrawalFlexTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bankClient, exchange } =
+ await createSimpleTestkudosEnvironmentV3(t);
+
+ // Create a withdrawal operation
+ const user = await bankClient.createRandomBankUser();
+ bankClient.setAuth(user);
+ const wop = await bankClient.createWithdrawalOperation(
+ user.username,
+ undefined,
+ );
+
+ const r1 = await walletClient.call(
+ WalletApiOperation.GetWithdrawalDetailsForUri,
+ {
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ },
+ );
+
+ console.log(j2s(r1));
+
+ // Withdraw
+
+ const r2 = await walletClient.call(
+ WalletApiOperation.AcceptBankIntegratedWithdrawal,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ amount: "TESTKUDOS:10",
+ },
+ );
+
+ await bankClient.confirmWithdrawalOperation(user.username, {
+ withdrawalOperationId: wop.withdrawal_id,
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+}
+
+runWithdrawalFlexTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-handover.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-handover.ts
index 23c0f938c..82d551948 100644
--- 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
index b483b8706..aaa6701f8 100644
--- 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
index 8ab029acc..cd7d137cc 100644
--- 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
index 4b23d7762..4588310b1 100644
--- a/packages/taler-harness/src/integrationtests/testrunner.ts
+++ b/packages/taler-harness/src/integrationtests/testrunner.ts
@@ -113,10 +113,12 @@ import { runWalletRefreshTest } from "./test-wallet-refresh.js";
import { runWalletWirefeesTest } from "./test-wallet-wirefees.js";
import { runWallettestingTest } from "./test-wallettesting.js";
import { runWithdrawalAbortBankTest } from "./test-withdrawal-abort-bank.js";
+import { runWithdrawalAmountTest } from "./test-withdrawal-amount.js";
import { runWithdrawalBankIntegratedTest } from "./test-withdrawal-bank-integrated.js";
import { runWithdrawalConversionTest } from "./test-withdrawal-conversion.js";
import { runWithdrawalFakebankTest } from "./test-withdrawal-fakebank.js";
import { runWithdrawalFeesTest } from "./test-withdrawal-fees.js";
+import { runWithdrawalFlexTest } from "./test-withdrawal-flex.js";
import { runWithdrawalHandoverTest } from "./test-withdrawal-handover.js";
import { runWithdrawalHugeTest } from "./test-withdrawal-huge.js";
import { runWithdrawalManualTest } from "./test-withdrawal-manual.js";
@@ -230,6 +232,8 @@ const allTests: TestMainFunction[] = [
runPeerPullLargeTest,
runPeerPushLargeTest,
runWithdrawalHandoverTest,
+ runWithdrawalAmountTest,
+ runWithdrawalFlexTest,
];
export interface TestRunSpec {
diff --git a/packages/taler-util/package.json b/packages/taler-util/package.json
index 74b2d6155..87e6a7cfa 100644
--- a/packages/taler-util/package.json
+++ b/packages/taler-util/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-util",
- "version": "0.10.7",
+ "version": "0.11.4",
"description": "Generic helper functionality for GNU Taler",
"type": "module",
"types": "./lib/index.node.d.ts",
diff --git a/packages/taler-util/src/CancellationToken.ts b/packages/taler-util/src/CancellationToken.ts
index 3aa576d77..5f38f0c7b 100644
--- a/packages/taler-util/src/CancellationToken.ts
+++ b/packages/taler-util/src/CancellationToken.ts
@@ -172,7 +172,7 @@ class CancellationToken {
} = CancellationToken.create();
let timer: NodeJS.Timeout | null;
- timer = setTimeout(() => originalCancel(CancellationToken.timeout), ms);
+ timer = setTimeout(() => originalCancel(`CancellationToken.timeout ${ms}`), ms);
const disposeTimer = () => {
if (timer == null) return;
clearTimeout(timer);
diff --git a/packages/taler-util/src/MerchantApiClient.ts b/packages/taler-util/src/MerchantApiClient.ts
index c27f1d582..f58757fb5 100644
--- 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
index 51359129d..e1409087f 100644
--- 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();
@@ -383,7 +385,7 @@ export class TalerCorebankApiClient {
async createWithdrawalOperation(
user: string,
- amount: string,
+ amount: string | undefined,
): Promise<WithdrawalOperationInfo> {
const url = new URL(`accounts/${user}/withdrawals`, this.baseUrl);
const resp = await this.httpLib.fetch(url.href, {
@@ -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
index 54d450d82..b04ce0612 100644
--- 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/errors.ts b/packages/taler-util/src/errors.ts
index 9378d25e8..d68177e4e 100644
--- a/packages/taler-util/src/errors.ts
+++ b/packages/taler-util/src/errors.ts
@@ -166,6 +166,11 @@ export interface DetailsMap {
[TalerErrorCode.WALLET_DB_UNAVAILABLE]: {
innerError: TalerErrorDetail | undefined;
};
+ [TalerErrorCode.WALLET_EXCHANGE_TOS_NOT_ACCEPTED]: {
+ exchangeBaseUrl: string;
+ tosStatus: string;
+ currentEtag: string | undefined;
+ };
}
type ErrBody<Y> = Y extends keyof DetailsMap ? DetailsMap[Y] : empty;
diff --git a/packages/taler-util/src/http-client/bank-integration.ts b/packages/taler-util/src/http-client/bank-integration.ts
index 75e6a627a..23740328b 100644
--- a/packages/taler-util/src/http-client/bank-integration.ts
+++ b/packages/taler-util/src/http-client/bank-integration.ts
@@ -50,7 +50,9 @@ export type TalerBankIntegrationErrorsByMethod<
* The API is used by the wallets.
*/
export class TalerBankIntegrationHttpClient {
- public readonly PROTOCOL_VERSION = "2:0:2";
+ public static readonly PROTOCOL_VERSION = "2:0:1";
+ public readonly PROTOCOL_VERSION =
+ TalerBankIntegrationHttpClient.PROTOCOL_VERSION;
httpLib: HttpRequestLibrary;
@@ -147,6 +149,10 @@ export class TalerBankIntegrationHttpClient {
return opKnownTalerFailure(details.code, details);
case TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE:
return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_AMOUNT_DIFFERS:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_AMOUNT_REQUIRED:
+ return opKnownTalerFailure(details.code, details);
default:
return opUnknownFailure(resp, details);
}
diff --git a/packages/taler-util/src/http-client/bank-revenue.ts b/packages/taler-util/src/http-client/bank-revenue.ts
index 34afe7d86..8331856a9 100644
--- a/packages/taler-util/src/http-client/bank-revenue.ts
+++ b/packages/taler-util/src/http-client/bank-revenue.ts
@@ -25,6 +25,7 @@ import { LibtoolVersion } from "../libtool-version.js";
import {
FailCasesByMethod,
ResultByMethod,
+ opFixedSuccess,
opKnownHttpFailure,
opSuccessFromHttp,
opUnknownFailure,
@@ -117,6 +118,9 @@ export class TalerRevenueHttpClient {
switch (resp.status) {
case HttpStatusCode.Ok:
return opSuccessFromHttp(resp, codecForRevenueIncomingHistory());
+ // FIXME: missing in docs
+ case HttpStatusCode.NoContent:
+ return opFixedSuccess({incoming_transactions: [], credit_account: "" });
case HttpStatusCode.BadRequest:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Unauthorized:
diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts
index d682dcfa0..892971fee 100644
--- 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
index c0004a218..3e6d857cb 100644
--- 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,8 @@ export const codecForCoreBankConfig = (): Codec<TalerCorebankApi.Config> =>
),
),
)
- .property("wire_type", codecForString())
+ .property("wire_type", codecOptionalDefault(codecForString(), "iban"))
+ .property("wire_transfer_fees", codecOptional(codecForAmountString()))
.build("TalerCorebankApi.Config");
//FIXME: implement this codec
@@ -603,6 +605,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 +644,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())
@@ -870,7 +903,6 @@ export const codecForTemplateDetails =
.property("template_description", codecForString())
.property("otp_id", codecOptional(codecForString()))
.property("template_contract", codecForTemplateContractDetails())
- .property("required_currency", codecOptional(codecForString()))
.property(
"editable_defaults",
codecOptional(codecForTemplateContractDetailsDefaults()),
@@ -893,15 +925,12 @@ 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 =
(): Codec<TalerMerchantApi.WalletTemplateDetails> =>
buildCodecForObject<TalerMerchantApi.WalletTemplateDetails>()
.property("template_contract", codecForTemplateContractDetails())
- .property("required_currency", codecOptional(codecForString()))
.property(
"editable_defaults",
codecOptional(codecForTemplateContractDetailsDefaults()),
@@ -1281,9 +1310,12 @@ export const codecForBankWithdrawalOperationStatus =
codecForConstString("confirmed"),
),
)
- .property("amount", codecForAmountString())
+ .property("amount", codecOptional(codecForAmountString()))
+ .property("currency", codecOptional(codecForCurrencyName()))
+ .property("suggested_amount", codecOptional(codecForAmountString()))
+ .property("card_fees", codecOptional(codecForAmountString()))
.property("sender_wire", codecOptional(codecForPaytoString()))
- .property("suggested_exchange", codecOptional(codecForString()))
+ .property("suggested_exchange", codecOptional(codecForURL()))
.property("confirm_transfer_url", codecOptional(codecForURL()))
.property("wire_types", codecForList(codecForString()))
.property("selected_reserve_pub", codecOptional(codecForString()))
@@ -1587,6 +1619,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;
@@ -1983,20 +2030,53 @@ export namespace TalerBankIntegrationApi {
// confirmed: the transfer has been confirmed and registered by the bank
status: WithdrawalOperationStatus;
- // Amount that will be withdrawn with this operation
- // (raw amount without fee considerations).
- amount: AmountString;
+ // Currency used for the withdrawal.
+ // MUST be present when amount is absent.
+ // @since v2, may become mandatory in the future.
+ currency?: string;
- // Bank account of the customer that is withdrawing, as a
- // payto URI.
+ // Amount that will be withdrawn with this operation
+ // (raw amount without fee considerations). Only
+ // given once the amount is fixed and cannot be changed.
+ // Optional since **vC2EC**.
+ amount?: AmountString | undefined;
+
+ // Suggestion for the amount to be withdrawn with this
+ // operation. Given if a suggestion was made but the
+ // user may still change the amount.
+ // Optional since **vC2EC**.
+ suggested_amount?: AmountString | undefined;
+
+ // Maximum amount that the wallet can choose to withdraw.
+ // Only applicable when the amount is not fixed.
+ // @since **vC2EC**.
+ max_amount?: AmountString | undefined;
+
+ // The non-Taler card fees the customer will have
+ // to pay to the bank / payment service provider
+ // they are using to make the withdrawal.
+ // @since **vC2EC**
+ card_fees?: AmountString | undefined;
+
+ // Bank account of the customer that is debiting, as an
+ // RFC 8905 payto URI.
sender_wire?: PaytoString;
- // Suggestion for an exchange given by the bank.
+ // Base URL of the suggested exchange. The bank may have
+ // neither a suggestion nor a requirement for the exchange.
+ // This value is typically set in the bank's configuration.
suggested_exchange?: string;
+ // Base URL of an exchange that must be used. Optional,
+ // not given *unless* a particular exchange is mandatory.
+ // This value is typically set in the bank's configuration.
+ // @since **vC2EC**
+ required_exchange?: string;
+
// URL that the user needs to navigate to in order to
// complete some final confirmation (e.g. 2FA).
- // It may contain withdrawal operation id
+ // Only applicable when status is selected or pending.
+ // It may contain the withdrawal operation id.
confirm_transfer_url?: string;
// Wire transfer types supported by the bank.
@@ -2006,17 +2086,24 @@ export namespace TalerBankIntegrationApi {
// only non-null if status is selected or confirmed.
selected_reserve_pub?: string;
- // Exchange account selected by the wallet
+ // Exchange account selected by the wallet;
// only non-null if status is selected or confirmed.
+ // @since **v1**
selected_exchange_account?: string;
}
export interface BankWithdrawalOperationPostRequest {
- // Reserve public key.
+ // Reserve public key that should become the wire transfer
+ // subject to fund the withdrawal.
reserve_pub: string;
// Payto address of the exchange selected for the withdrawal.
selected_exchange: PaytoString;
+
+ // Selected amount to be transferred. Optional if the
+ // backend already knows the amount.
+ // @since **vC2EC**
+ amount?: AmountString | undefined;
}
export interface BankWithdrawalOperationPostResponse {
@@ -2030,7 +2117,7 @@ export namespace TalerBankIntegrationApi {
// URL that the user needs to navigate to in order to
// complete some final confirmation (e.g. 2FA).
//
- // Only applicable when status is selected.
+ // Only applicable when status is selected or pending.
// It may contain withdrawal operation id
confirm_transfer_url?: string;
}
@@ -2105,12 +2192,31 @@ export namespace TalerCorebankApi {
// Default to 'iban' is missing
// @since v4, may become mandatory in the future.
wire_type: string;
+
+ // Wire transfer execution fees.
+ // @since v4, will become mandatory in the next version.
+ wire_transfer_fees?: AmountString;
}
export interface BankAccountCreateWithdrawalRequest {
- // Amount to withdraw.
- amount: AmountString;
+ // Amount to withdraw. If given, the wallet
+ // cannot change the amount.
+ // Optional since **vC2EC**.
+ amount?: AmountString;
+
+ // Suggested amount to withdraw. The wallet can
+ // still change the suggestion.
+ // @since **vC2EC**
+ suggested_amount?: AmountString;
+
+ // The non-Taler card fees the customer will have
+ // to pay to the account owner, bank and/or
+ // payment service provider
+ // they are using to make this withdrawal.
+ // @since **vC2EC**
+ card_fees?: AmountString;
}
+
export interface BankAccountCreateWithdrawalResponse {
// ID of the withdrawal, can be used to view/modify the withdrawal operation.
withdrawal_id: string;
@@ -2394,7 +2500,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
@@ -2453,10 +2559,6 @@ export namespace TalerCorebankApi {
export interface CashoutInfo {
cashout_id: number;
- /**
- * @deprecated since 4, use new 2fa
- */
- status?: "pending" | "aborted" | "confirmed";
}
export interface GlobalCashouts {
// Every string represents a cash-out operation ID.
@@ -4068,6 +4170,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 +4253,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 +4268,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 +4329,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 +4350,7 @@ export namespace TalerMerchantApi {
fulfillment_message?: string;
}
- interface MinimalInventoryProduct {
+ export interface MinimalInventoryProduct {
// Which product is requested (here mandatory!).
product_id: string;
@@ -4464,7 +4628,6 @@ export namespace TalerMerchantApi {
confirmed?: boolean;
}
-
export interface OtpDeviceAddDetails {
// Device ID to use.
otp_device_id: string;
@@ -4587,17 +4750,6 @@ export namespace TalerMerchantApi {
// user-editable defaults for this template.
// Since protocol **v13**.
editable_defaults?: TemplateContractDetailsDefaults;
-
- // Required currency for payments. Useful if no
- // amount is specified in the template_contract
- // but the user should be required to pay in a
- // particular currency anyway. Merchant backends
- // may reject requests if the template_contract
- // or editable_defaults do
- // specify an amount in a different currency.
- // This parameter is optional.
- // Since protocol **v13**.
- required_currency?: string;
}
export interface TemplateContractDetails {
// Human-readable summary for the template.
@@ -4627,12 +4779,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;
@@ -4649,17 +4801,6 @@ export namespace TalerMerchantApi {
// user-editable defaults for this template.
// Since protocol **v13**.
editable_defaults?: TemplateContractDetailsDefaults;
-
- // Required currency for payments. Useful if no
- // amount is specified in the template_contract
- // but the user should be required to pay in a
- // particular currency anyway. Merchant backends
- // may reject requests if the template_contract
- // or editable_defaults do
- // specify an amount in a different currency.
- // This parameter is optional.
- // Since protocol **v13**.
- required_currency?: string;
}
export interface TemplateSummaryResponse {
@@ -4685,17 +4826,6 @@ export namespace TalerMerchantApi {
// user-editable defaults for this template.
// Since protocol **v13**.
editable_defaults?: TemplateContractDetailsDefaults;
-
- // Required currency for payments. Useful if no
- // amount is specified in the template_contract
- // but the user should be required to pay in a
- // particular currency anyway. Merchant backends
- // may reject requests if the template_contract
- // or editable_defaults do
- // specify an amount in a different currency.
- // This parameter is optional.
- // Since protocol **v13**.
- required_currency?: string;
}
export interface TemplateDetails {
@@ -4714,17 +4844,6 @@ export namespace TalerMerchantApi {
// user-editable defaults for this template.
// Since protocol **v13**.
editable_defaults?: TemplateContractDetailsDefaults;
-
- // Required currency for payments. Useful if no
- // amount is specified in the template_contract
- // but the user should be required to pay in a
- // particular currency anyway. Merchant backends
- // may reject requests if the template_contract
- // or editable_defaults do
- // specify an amount in a different currency.
- // This parameter is optional.
- // Since protocol **v13**.
- required_currency?: string;
}
export interface UsingTemplateDetails {
// Summary of the template
@@ -5158,6 +5277,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/http-impl.qtart.ts b/packages/taler-util/src/http-impl.qtart.ts
index b4e4ebbe7..f60c82fc3 100644
--- a/packages/taler-util/src/http-impl.qtart.ts
+++ b/packages/taler-util/src/http-impl.qtart.ts
@@ -118,7 +118,10 @@ export class HttpLibImpl implements HttpRequestLibrary {
// Just like WHATWG fetch(), the qjs http client doesn't
// really support cancellation, so cancellation here just
// means that the result is ignored!
- const fetchProm = qjsOs.fetchHttp(url, {
+ const {
+ promise: fetchProm,
+ cancelFn
+ } = qjsOs.fetchHttp(url, {
method,
data,
headers: headersList,
@@ -135,6 +138,7 @@ export class HttpLibImpl implements HttpRequestLibrary {
if (opt?.cancellationToken) {
cancelCancelledHandler = opt.cancellationToken.onCancelled(() => {
+ cancelFn();
cancelPromCap.reject(new RequestCancelledError());
});
}
diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts
index 24d6e9950..9f99f2f5a 100644
--- 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/invariants.ts b/packages/taler-util/src/invariants.ts
index c6e9b8113..113d697c3 100644
--- a/packages/taler-util/src/invariants.ts
+++ b/packages/taler-util/src/invariants.ts
@@ -33,7 +33,7 @@ export class InvariantViolatedError extends Error {
*
* A violation of this invariant means that the database is inconsistent.
*/
-export function checkDbInvariant(b: boolean, m?: string): asserts b {
+export function checkDbInvariant(b: boolean, m: string): asserts b {
if (!b) {
if (m) {
throw Error(`BUG: database invariant failed (${m})`);
diff --git a/packages/taler-util/src/merchant-api-types.ts b/packages/taler-util/src/merchant-api-types.ts
deleted file mode 100644
index 639ae8d13..000000000
--- a/packages/taler-util/src/merchant-api-types.ts
+++ /dev/null
@@ -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/notifications.ts b/packages/taler-util/src/notifications.ts
index d4dfe7589..a8a8c3299 100644
--- a/packages/taler-util/src/notifications.ts
+++ b/packages/taler-util/src/notifications.ts
@@ -128,7 +128,7 @@ export enum ObservabilityEventType {
TaskStart = "task-start",
TaskStop = "task-stop",
TaskReset = "task-reset",
- ShepherdTaskResult = "sheperd-task-result",
+ ShepherdTaskResult = "shepherd-task-result",
DeclareTaskDependency = "declare-task-dependency",
CryptoStart = "crypto-start",
CryptoFinishSuccess = "crypto-finish-success",
diff --git a/packages/taler-util/src/payto.ts b/packages/taler-util/src/payto.ts
index a471d0b87..39c25cffd 100644
--- a/packages/taler-util/src/payto.ts
+++ b/packages/taler-util/src/payto.ts
@@ -15,7 +15,7 @@
*/
import { generateFakeSegwitAddress } from "./bitcoin.js";
-import { Codec, Context, DecodingError, renderContext } from "./codec.js";
+import { Codec, Context, DecodingError, buildCodecForObject, codecForStringURL, renderContext } from "./codec.js";
import { URLSearchParams } from "./url.js";
export type PaytoUri =
@@ -291,3 +291,21 @@ export function talerPaytoFromExchangeReserve(
return `payto://${proto}/${url.host}${url.pathname}${reservePub}`;
}
+
+/**
+ * The account letter is all the information
+ * the merchant backend requires from the
+ * bank account to check transfer.
+ *
+ */
+export type AccountLetter = {
+ accountURI: PaytoString;
+ infoURL: string;
+};
+
+export const codecForAccountLetter =
+ (): Codec<AccountLetter> =>
+ buildCodecForObject<AccountLetter>()
+ .property("infoURL", codecForStringURL(true))
+ .property("accountURI", codecForPaytoString())
+ .build("AccountLetter");
diff --git a/packages/taler-util/src/qtart.ts b/packages/taler-util/src/qtart.ts
index e298a157c..6a5984973 100644
--- a/packages/taler-util/src/qtart.ts
+++ b/packages/taler-util/src/qtart.ts
@@ -17,7 +17,10 @@ export interface QjsHttpOptions {
}
export interface QjsOsLib {
- fetchHttp(url: string, options?: QjsHttpOptions): Promise<QjsHttpResp>;
+ fetchHttp(url: string, options?: QjsHttpOptions): {
+ promise: Promise<QjsHttpResp>,
+ cancelFn: () => number,
+ };
postMessageToHost(s: string): void;
setMessageFromHostHandler(h: (s: string) => void): void;
rename(oldPath: string, newPath: string): number;
diff --git a/packages/taler-util/src/taler-crypto.ts b/packages/taler-util/src/taler-crypto.ts
index e587773e2..950161b10 100644
--- 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-error-codes.ts b/packages/taler-util/src/taler-error-codes.ts
index 9985e74b3..f77357407 100644
--- a/packages/taler-util/src/taler-error-codes.ts
+++ b/packages/taler-util/src/taler-error-codes.ts
@@ -354,7 +354,7 @@ export enum TalerErrorCode {
/**
* The backend could not locate a required template to generate an HTML reply. The system administrator should check if the resource files are installed in the correct location and are readable to the service.
- * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_ACCEPTABLE (406).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_FAILED_TO_LOAD_TEMPLATE = 74,
@@ -1945,7 +1945,7 @@ export enum TalerErrorCode {
/**
- * The payto-URI hash did not match. Hence the request was denied.
+ * The KYC authorization signature was invalid. Hence the request was denied.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
* (A value of 0 indicates that the error is generated client-side).
*/
@@ -2017,6 +2017,22 @@ export enum TalerErrorCode {
/**
+ * The exchange is unaware of the given requirement row.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_CHECK_REQUEST_UNKNOWN = 1939,
+
+
+ /**
+ * The exchange has no account public key to check the KYC authorization signature against. Hence the request was denied. The user should do a wire transfer to the exchange with the KYC authorization key in the subject.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_CHECK_AUTHORIZATION_KEY_UNKNOWN = 1940,
+
+
+ /**
* The exchange does not know a contract under the given contract public key.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
* (A value of 0 indicates that the error is generated client-side).
@@ -2105,6 +2121,14 @@ export enum TalerErrorCode {
/**
+ * The product category is not known to the backend.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_CATEGORY_UNKNOWN = 2003,
+
+
+ /**
* The proposal is not known to the backend.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
* (A value of 0 indicates that the error is generated client-side).
@@ -2561,6 +2585,14 @@ export enum TalerErrorCode {
/**
+ * Invalid token because it was already used, is expired or not yet valid.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_TOKEN_INVALID = 2183,
+
+
+ /**
* The contract hash does not match the given order ID.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
@@ -2921,6 +2953,14 @@ export enum TalerErrorCode {
/**
+ * A token family referenced in this order is either expired or not valid yet.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_NOT_VALID = 2534,
+
+
+ /**
* The exchange says it does not know this transfer.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
* (A value of 0 indicates that the error is generated client-side).
@@ -3057,6 +3097,14 @@ export enum TalerErrorCode {
/**
+ * A category with the same name exists already.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_CATEGORIES_CONFLICT_CATEGORY_EXISTS = 2651,
+
+
+ /**
* The update would have reduced the total amount of product lost, which is not allowed.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
* (A value of 0 indicates that the error is generated client-side).
@@ -3233,6 +3281,22 @@ export enum TalerErrorCode {
/**
+ * The auditor refused the connection due to a lack of authorization.
+ * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ AUDITOR_GENERIC_UNAUTHORIZED = 3001,
+
+
+ /**
+ * This method is not allowed here.
+ * Returned with an HTTP status code of #MHD_HTTP_METHOD_NOT_ALLOWED (405).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ AUDITOR_GENERIC_METHOD_NOT_ALLOWED = 3002,
+
+
+ /**
* The signature from the exchange on the deposit confirmation is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
* (A value of 0 indicates that the error is generated client-side).
@@ -3633,6 +3697,22 @@ export enum TalerErrorCode {
/**
+ * Specified amount will not work for this withdrawal.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_AMOUNT_DIFFERS = 5148,
+
+
+ /**
+ * The backend requires an amount to be specified.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_AMOUNT_REQUIRED = 5149,
+
+
+ /**
* The sync service failed find the account in its database.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
* (A value of 0 indicates that the error is generated client-side).
@@ -4049,6 +4129,14 @@ export enum TalerErrorCode {
/**
+ * A wallet-core request failed because the user needs to first accept the exchange's terms of service.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_EXCHANGE_TOS_NOT_ACCEPTED = 7037,
+
+
+ /**
* We encountered a timeout with our payment backend.
* Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504).
* (A value of 0 indicates that the error is generated client-side).
@@ -4609,6 +4697,62 @@ export enum TalerErrorCode {
/**
+ * The Donau is not aware of the donation unit requested for the operation.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DONAU_GENERIC_DONATION_UNIT_UNKNOWN = 8611,
+
+
+ /**
+ * The Donau failed to talk to the process responsible for its private donation unit keys or the helpers had no donation units (properly) configured.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DONAU_DONATION_UNIT_HELPER_UNAVAILABLE = 8612,
+
+
+ /**
+ * The Donau failed to talk to the process responsible for its private signing keys.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DONAU_SIGNKEY_HELPER_UNAVAILABLE = 8613,
+
+
+ /**
+ * The response from the online signing key helper process was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DONAU_SIGNKEY_HELPER_BUG = 8614,
+
+
+ /**
+ * The number of segments included in the URI does not match the number of segments expected by the endpoint.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DONAU_GENERIC_WRONG_NUMBER_OF_SEGMENTS = 8615,
+
+
+ /**
+ * The signature of the donation receipt is not valid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DONAU_DONATION_RECEIPT_SIGNATURE_INVALID = 8616,
+
+
+ /**
+ * The client re-used a unique donor identifier nonce, which is not allowed.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DONAU_DONOR_IDENTIFIER_NONCE_REUSE = 8617,
+
+
+ /**
* A generic error happened in the LibEuFin nexus. See the enclose details JSON for more information.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
diff --git a/packages/taler-util/src/taler-types.ts b/packages/taler-util/src/taler-types.ts
index 392e7149c..66f98ea9a 100644
--- a/packages/taler-util/src/taler-types.ts
+++ b/packages/taler-util/src/taler-types.ts
@@ -978,7 +978,7 @@ export class WithdrawOperationStatusResponse {
aborted: boolean;
- amount: string;
+ amount: string | undefined;
sender_wire?: string;
@@ -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;
@@ -1553,7 +1557,7 @@ export const codecForWithdrawOperationStatusResponse =
.property("selection_done", codecForBoolean())
.property("transfer_done", codecForBoolean())
.property("aborted", codecForBoolean())
- .property("amount", codecForString())
+ .property("amount", codecOptional(codecForString()))
.property("sender_wire", codecOptional(codecForString()))
.property("suggested_exchange", codecOptional(codecForString()))
.property("confirm_transfer_url", codecOptional(codecForString()))
diff --git a/packages/taler-util/src/taleruri.test.ts b/packages/taler-util/src/taleruri.test.ts
index 7f10d21fd..b92366fb3 100644
--- 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
index b4f9db6ef..54b7525e3 100644
--- 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/transactions-types.ts b/packages/taler-util/src/transactions-types.ts
index cee3de9fa..a6ac5aec6 100644
--- a/packages/taler-util/src/transactions-types.ts
+++ b/packages/taler-util/src/transactions-types.ts
@@ -105,8 +105,11 @@ export enum TransactionMajorState {
Done = "done",
Aborting = "aborting",
Aborted = "aborted",
- Suspended = "suspended",
Dialog = "dialog",
+ Finalizing = "finalizing",
+ // Plain suspended is always a suspended pending state.
+ Suspended = "suspended",
+ SuspendedFinalizing = "suspended-finalizing",
SuspendedAborting = "suspended-aborting",
Failed = "failed",
Expired = "expired",
@@ -324,7 +327,7 @@ export interface TransactionWithdrawal extends TransactionCommon {
/**
* Exchange of the withdrawal.
*/
- exchangeBaseUrl: string;
+ exchangeBaseUrl: string | undefined;
/**
* Amount that got subtracted from the reserve balance.
diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts
index ec6cb6f58..d23780145 100644
--- a/packages/taler-util/src/wallet-types.ts
+++ b/packages/taler-util/src/wallet-types.ts
@@ -48,12 +48,13 @@ import {
} from "./codec.js";
import {
CurrencySpecification,
+ TalerMerchantApi,
TemplateParams,
WithdrawalOperationStatus,
canonicalizeBaseUrl,
} from "./index.js";
import { VersionMatchResult } from "./libtool-version.js";
-import { PaytoUri } from "./payto.js";
+import { PaytoString, PaytoUri, codecForPaytoString } from "./payto.js";
import { AgeCommitmentProof } from "./taler-crypto.js";
import { TalerErrorCode } from "./taler-error-codes.js";
import {
@@ -228,11 +229,13 @@ interface GetPlanForWalletInitiatedOperation {
export interface ConvertAmountRequest {
amount: AmountString;
type: TransactionAmountMode;
+ depositPaytoUri: PaytoString;
}
export const codecForConvertAmountRequest =
buildCodecForObject<ConvertAmountRequest>()
.property("amount", codecForAmountString())
+ .property("depositPaytoUri", codecForPaytoString())
.property(
"type",
codecForEither(
@@ -487,6 +490,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 +525,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 {
@@ -972,9 +986,14 @@ export interface PreparePayResultAlreadyConfirmed {
export interface BankWithdrawDetails {
status: WithdrawalOperationStatus;
- amount: AmountJson;
+ currency: string;
+ amount: AmountJson | undefined;
+ editableAmount: boolean;
+ maxAmount: AmountJson | undefined;
+ wireFee: AmountJson | undefined;
senderWire?: string;
- suggestedExchange?: string;
+ exchange?: string;
+ editableExchange: boolean;
confirmTransferUrl?: string;
wireTypes: string[];
operationId: string;
@@ -1319,6 +1338,7 @@ export enum ExchangeTosStatus {
Pending = "pending",
Proposed = "proposed",
Accepted = "accepted",
+ MissingTos = "missing-tos",
}
export enum ExchangeEntryStatus {
@@ -1721,15 +1741,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,38 +1854,48 @@ export interface GetWithdrawalDetailsForAmountRequest {
export interface PrepareBankIntegratedWithdrawalRequest {
talerWithdrawUri: string;
- exchangeBaseUrl: string;
- forcedDenomSel?: ForcedDenomSel;
- restrictAge?: number;
}
export const codecForPrepareBankIntegratedWithdrawalRequest =
(): Codec<PrepareBankIntegratedWithdrawalRequest> =>
buildCodecForObject<PrepareBankIntegratedWithdrawalRequest>()
- .property("exchangeBaseUrl", codecForCanonBaseUrl())
.property("talerWithdrawUri", codecForString())
- .property("forcedDenomSel", codecForAny())
- .property("restrictAge", codecOptional(codecForNumber()))
.build("PrepareBankIntegratedWithdrawalRequest");
export interface PrepareBankIntegratedWithdrawalResponse {
- transactionId: string;
+ transactionId: TransactionIdStr;
+ 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 {
talerWithdrawUri: string;
exchangeBaseUrl: string;
forcedDenomSel?: ForcedDenomSel;
+ /**
+ * Amount to withdraw.
+ * If the bank's withdrawal operation uses a fixed amount,
+ * this field must either be left undefined or its value must match
+ * the amount from the withdrawal operation.
+ */
+ amount?: AmountString;
restrictAge?: number;
}
@@ -1878,6 +1905,7 @@ export const codecForAcceptBankIntegratedWithdrawalRequest =
.property("exchangeBaseUrl", codecForCanonBaseUrl())
.property("talerWithdrawUri", codecForString())
.property("forcedDenomSel", codecForAny())
+ .property("amount", codecOptional(codecForAmountString()))
.property("restrictAge", codecOptional(codecForNumber()))
.build("AcceptBankIntegratedWithdrawalRequest");
@@ -1931,6 +1959,9 @@ export const codecForApplyRefundFromPurchaseIdRequest =
export interface GetWithdrawalDetailsForUriRequest {
talerWithdrawUri: string;
+ /**
+ * @deprecated not used
+ */
restrictAge?: number;
}
@@ -2023,6 +2054,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;
@@ -2320,8 +2366,13 @@ export interface WithdrawUriInfoResponse {
operationId: string;
status: WithdrawalOperationStatus;
confirmTransferUrl?: string;
- amount: AmountString;
+ currency: string;
+ amount: AmountString | undefined;
+ editableAmount: boolean;
+ maxAmount: AmountString | undefined;
+ wireFee: AmountString | undefined;
defaultExchangeBaseUrl?: string;
+ editableExchange: boolean;
possibleExchanges: ExchangeListItem[];
}
@@ -2339,7 +2390,12 @@ export const codecForWithdrawUriInfoResponse =
codecForConstString("confirmed"),
),
)
- .property("amount", codecForAmountString())
+ .property("amount", codecOptional(codecForAmountString()))
+ .property("maxAmount", codecOptional(codecForAmountString()))
+ .property("wireFee", codecOptional(codecForAmountString()))
+ .property("currency", codecForString())
+ .property("editableAmount", codecForBoolean())
+ .property("editableExchange", codecForBoolean())
.property("defaultExchangeBaseUrl", codecOptional(codecForCanonBaseUrl()))
.property("possibleExchanges", codecForList(codecForExchangeListItem()))
.build("WithdrawUriInfoResponse");
@@ -3010,6 +3066,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 +3362,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/debian/changelog b/packages/taler-wallet-cli/debian/changelog
index e136caa61..5fa99e801 100644
--- a/packages/taler-wallet-cli/debian/changelog
+++ b/packages/taler-wallet-cli/debian/changelog
@@ -1,3 +1,27 @@
+taler-wallet-cli (0.11.4) unstable; urgency=low
+
+ * Release 0.11.4
+
+ -- Florian Dold <dold@taler.net> Mon, 10 Jun 2024 19:57:55 +0200
+
+taler-wallet-cli (0.11.3) unstable; urgency=low
+
+ * Release 0.11.3
+
+ -- Florian Dold <dold@taler.net> Fri, 07 Jun 2024 19:12:44 +0200
+
+taler-wallet-cli (0.11.2) unstable; urgency=low
+
+ * Release 0.11.2
+
+ -- Florian Dold <dold@taler.net> Wed, 05 Jun 2024 20:17:56 +0200
+
+taler-wallet-cli (0.11.1) unstable; urgency=low
+
+ * Release 0.11.1
+
+ -- Florian Dold <dold@taler.net> Mon, 27 May 2024 14:46:35 -0600
+
taler-wallet-cli (0.10.7) unstable; urgency=low
* Release 0.10.7
diff --git a/packages/taler-wallet-cli/package.json b/packages/taler-wallet-cli/package.json
index 922556749..ecc8252e6 100644
--- a/packages/taler-wallet-cli/package.json
+++ b/packages/taler-wallet-cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-wallet-cli",
- "version": "0.10.7",
+ "version": "0.11.4",
"description": "",
"engines": {
"node": ">=0.18.0"
diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts
index b915de538..5bde7db01 100644
--- 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,
{
@@ -1189,11 +1231,21 @@ advancedCli
});
advancedCli
+ .subcommand("resetAllRetries", "reset-all-retries", {
+ help: "Reset all retry counters.",
+ })
+ .action(async (args) => {
+ await withWallet(args, { lazyTaskLoop: true }, async (wallet) => {
+ await wallet.client.call(WalletApiOperation.TestingResetAllRetries, {});
+ });
+ });
+
+advancedCli
.subcommand("tasks", "tasks", {
help: "Show active wallet-core tasks.",
})
.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 +1296,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 +1349,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 +1365,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 +1416,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 +1430,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 +1447,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 +1468,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 +1489,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 +1510,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 +1528,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 +1538,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 +1549,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 +1582,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 +1596,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 +1610,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 +1626,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 +1643,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 +1668,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 +1692,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 +1710,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/package.json b/packages/taler-wallet-core/package.json
index 46b3cef4e..c710861d3 100644
--- a/packages/taler-wallet-core/package.json
+++ b/packages/taler-wallet-core/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-wallet-core",
- "version": "0.10.7",
+ "version": "0.11.4",
"description": "",
"engines": {
"node": ">=0.18.0"
diff --git a/packages/taler-wallet-core/src/backup/index.ts b/packages/taler-wallet-core/src/backup/index.ts
index 15904b470..09d5ae75d 100644
--- a/packages/taler-wallet-core/src/backup/index.ts
+++ b/packages/taler-wallet-core/src/backup/index.ts
@@ -805,9 +805,10 @@ async function backupRecoveryTheirs(
let backupStateEntry: ConfigRecord | undefined = await tx.config.get(
ConfigRecordKey.WalletBackupState,
);
- checkDbInvariant(!!backupStateEntry);
+ checkDbInvariant(!!backupStateEntry, `no backup entry`);
checkDbInvariant(
backupStateEntry.key === ConfigRecordKey.WalletBackupState,
+ `backup entry inconsistent`,
);
backupStateEntry.value.lastBackupNonce = undefined;
backupStateEntry.value.lastBackupTimestamp = undefined;
@@ -913,7 +914,10 @@ export async function provideBackupState(
},
);
if (bs) {
- checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState);
+ checkDbInvariant(
+ bs.key === ConfigRecordKey.WalletBackupState,
+ `backup entry inconsistent`,
+ );
return bs.value;
}
// We need to generate the key outside of the transaction
@@ -941,6 +945,7 @@ export async function provideBackupState(
}
checkDbInvariant(
backupStateEntry.key === ConfigRecordKey.WalletBackupState,
+ `backup entry inconsistent`,
);
return backupStateEntry.value;
});
@@ -952,7 +957,10 @@ export async function getWalletBackupState(
): Promise<WalletBackupConfState> {
const bs = await tx.config.get(ConfigRecordKey.WalletBackupState);
checkDbInvariant(!!bs, "wallet backup state should be in DB");
- checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState);
+ checkDbInvariant(
+ bs.key === ConfigRecordKey.WalletBackupState,
+ `backup entry inconsistent`,
+ );
return bs.value;
}
@@ -962,7 +970,7 @@ export async function setWalletDeviceId(
): Promise<void> {
await provideBackupState(wex);
await wex.db.runReadWriteTx({ storeNames: ["config"] }, async (tx) => {
- let backupStateEntry: ConfigRecord | undefined = await tx.config.get(
+ const backupStateEntry: ConfigRecord | undefined = await tx.config.get(
ConfigRecordKey.WalletBackupState,
);
if (
diff --git a/packages/taler-wallet-core/src/balance.ts b/packages/taler-wallet-core/src/balance.ts
index 5a805b477..381028906 100644
--- 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,
@@ -68,8 +69,8 @@ import { ExchangeRestrictionSpec, findMatchingWire } from "./coinSelection.js";
import {
DepositOperationStatus,
ExchangeEntryDbRecordStatus,
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
PeerPushDebitStatus,
RefreshGroupRecord,
RefreshOperationStatus,
@@ -303,8 +304,8 @@ export async function getBalancesInsideTransaction(
const balanceStore: BalancesStore = new BalancesStore(wex, tx);
const keyRangeActive = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
await tx.exchanges.iter().forEachAsync(async (ex) => {
@@ -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,71 @@ 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",
+ );
+ checkDbInvariant(
+ wg.exchangeBaseUrl !== undefined,
+ "wg in kyc state should have been initialized",
);
+ const currency = Amounts.currencyOf(wg.denomsSel.totalCoinValue);
+ await balanceStore.setFlagIncomingKyc(currency, wg.exchangeBaseUrl);
break;
+ }
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",
);
+ checkDbInvariant(
+ wg.exchangeBaseUrl !== undefined,
+ "wg in kyc state should have been initialized",
+ );
+ const currency = Amounts.currencyOf(wg.denomsSel.totalCoinValue);
+ await balanceStore.setFlagIncomingAml(currency, wg.exchangeBaseUrl);
+ break;
+ }
+ 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",
+ );
+ checkDbInvariant(
+ wg.exchangeBaseUrl !== undefined,
+ "wg in kyc 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/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts
index a60e41ecd..db6384c93 100644
--- a/packages/taler-wallet-core/src/coinSelection.ts
+++ b/packages/taler-wallet-core/src/coinSelection.ts
@@ -691,7 +691,7 @@ export function checkAccountRestriction(
switch (myRestriction.type) {
case "deny":
return { ok: false };
- case "regex":
+ case "regex": {
const regex = new RegExp(myRestriction.payto_regex);
if (!regex.test(paytoUri)) {
return {
@@ -700,6 +700,7 @@ export function checkAccountRestriction(
hintI18n: myRestriction.human_hint_i18n,
};
}
+ }
}
}
return {
@@ -909,7 +910,7 @@ async function selectPayCandidates(
coinAvail.exchangeBaseUrl,
coinAvail.denomPubHash,
]);
- checkDbInvariant(!!denom);
+ checkDbInvariant(!!denom, `denomination of a coin is missing hash: ${coinAvail.denomPubHash}`);
if (denom.isRevoked) {
logger.trace("denom is revoked");
continue;
diff --git a/packages/taler-wallet-core/src/common.ts b/packages/taler-wallet-core/src/common.ts
index edaba5ba4..13c875575 100644
--- a/packages/taler-wallet-core/src/common.ts
+++ b/packages/taler-wallet-core/src/common.ts
@@ -31,6 +31,8 @@ import {
ExchangeUpdateStatus,
Logger,
RefreshReason,
+ TalerError,
+ TalerErrorCode,
TalerErrorDetail,
TalerPreciseTimestamp,
TalerProtocolTimestamp,
@@ -57,11 +59,11 @@ import {
PurchaseRecord,
RecoupGroupRecord,
RefreshGroupRecord,
- RewardRecord,
WalletDbReadWriteTransaction,
WithdrawalGroupRecord,
timestampPreciseToDb,
} from "./db.js";
+import { ReadyExchangeSummary } from "./exchanges.js";
import { createRefreshGroup } from "./refresh.js";
import { WalletExecutionContext, getDenomInfo } from "./wallet.js";
@@ -121,7 +123,10 @@ export async function makeCoinAvailable(
coinRecord.exchangeBaseUrl,
coinRecord.denomPubHash,
]);
- checkDbInvariant(!!denom);
+ checkDbInvariant(
+ !!denom,
+ `denomination of a coin is missing hash: ${coinRecord.denomPubHash}`,
+ );
const ageRestriction = coinRecord.maxAge;
let car = await tx.coinAvailability.get([
coinRecord.exchangeBaseUrl,
@@ -175,13 +180,19 @@ export async function spendCoins(
coin.exchangeBaseUrl,
coin.denomPubHash,
);
- checkDbInvariant(!!denom);
+ checkDbInvariant(
+ !!denom,
+ `denomination of a coin is missing hash: ${coin.denomPubHash}`,
+ );
const coinAvailability = await tx.coinAvailability.get([
coin.exchangeBaseUrl,
coin.denomPubHash,
coin.maxAge,
]);
- checkDbInvariant(!!coinAvailability);
+ checkDbInvariant(
+ !!coinAvailability,
+ `age denom info is missing for ${coin.maxAge}`,
+ );
const contrib = csi.contributions[i];
if (coin.status !== CoinStatus.Fresh) {
const alloc = coin.spendAllocation;
@@ -213,7 +224,6 @@ export async function spendCoins(
amount: Amounts.stringify(remaining.amount),
coinPub: coin.coinPub,
});
- checkDbInvariant(!!coinAvailability);
if (coinAvailability.freshCoinCount === 0) {
throw Error(
`invalid coin count ${coinAvailability.freshCoinCount} in DB`,
@@ -258,6 +268,9 @@ export enum TombstoneTag {
export function getExchangeTosStatusFromRecord(
exchange: ExchangeEntryRecord,
): ExchangeTosStatus {
+ if (exchange.tosCurrentEtag == null) {
+ return ExchangeTosStatus.MissingTos;
+ }
if (!exchange.tosAcceptedEtag) {
return ExchangeTosStatus.Proposed;
}
@@ -558,6 +571,28 @@ export function getAutoRefreshExecuteThreshold(d: {
}
/**
+ * Type and schema definitions for pending tasks in the wallet.
+ *
+ * These are only used internally, and are not part of the stable public
+ * interface to the wallet.
+ */
+
+export enum PendingTaskType {
+ ExchangeUpdate = "exchange-update",
+ Purchase = "purchase",
+ Refresh = "refresh",
+ Recoup = "recoup",
+ RewardPickup = "reward-pickup",
+ Withdraw = "withdraw",
+ Deposit = "deposit",
+ Backup = "backup",
+ PeerPushDebit = "peer-push-debit",
+ PeerPullCredit = "peer-pull-credit",
+ PeerPushCredit = "peer-push-credit",
+ PeerPullDebit = "peer-pull-debit",
+}
+
+/**
* Parsed representation of task identifiers.
*/
export type ParsedTaskIdentifier =
@@ -660,9 +695,6 @@ export namespace TaskIdentifiers {
exchBaseUrl,
)}` as TaskIdStr;
}
- export function forTipPickup(tipRecord: RewardRecord): TaskIdStr {
- return `${PendingTaskType.RewardPickup}:${tipRecord.walletRewardId}` as TaskIdStr;
- }
export function forRefresh(
refreshGroupRecord: RefreshGroupRecord,
): TaskIdStr {
@@ -747,28 +779,6 @@ export interface TransactionContext {
deleteTransaction(): Promise<void>;
}
-/**
- * Type and schema definitions for pending tasks in the wallet.
- *
- * These are only used internally, and are not part of the stable public
- * interface to the wallet.
- */
-
-export enum PendingTaskType {
- ExchangeUpdate = "exchange-update",
- Purchase = "purchase",
- Refresh = "refresh",
- Recoup = "recoup",
- RewardPickup = "reward-pickup",
- Withdraw = "withdraw",
- Deposit = "deposit",
- Backup = "backup",
- PeerPushDebit = "peer-push-debit",
- PeerPullCredit = "peer-pull-credit",
- PeerPushCredit = "peer-push-credit",
- PeerPullDebit = "peer-pull-debit",
-}
-
declare const __taskIdStr: unique symbol;
export type TaskIdStr = string & { [__taskIdStr]: true };
@@ -799,7 +809,7 @@ export async function genericWaitForState(
flag.raise();
}
});
- const unregisterOnCancelled = wex.cancellationToken.onCancelled(() => {
+ const unregisterOnCancelled = wex.cancellationToken.onCancelled((reason) => {
cancelNotif();
flag.raise();
});
@@ -819,5 +829,25 @@ export async function genericWaitForState(
} catch (e) {
unregisterOnCancelled();
cancelNotif();
+ throw e;
+ }
+}
+
+export function requireExchangeTosAcceptedOrThrow(
+ exchange: ReadyExchangeSummary,
+): void {
+ switch (exchange.tosStatus) {
+ case ExchangeTosStatus.Accepted:
+ case ExchangeTosStatus.MissingTos:
+ break;
+ default:
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_EXCHANGE_TOS_NOT_ACCEPTED,
+ {
+ exchangeBaseUrl: exchange.exchangeBaseUrl,
+ currentEtag: exchange.tosCurrentEtag,
+ tosStatus: exchange.tosStatus,
+ },
+ );
}
}
diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
index 0745d70c4..2a2958a71 100644
--- 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
index b75e48c39..5c381eea7 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -248,6 +248,9 @@ export function timestampOptionalAbsoluteFromDb(
* 0x0103_nnnn: aborting
* 0x0110_nnnn: suspended
* 0x0113_nnnn: suspended-aborting
+ * a=2: finalizing
+ * 0x0200_nnnn: finalizing
+ * 0x0210_nnnn: suspended-finalizing
* a=5: final
* 0x0500_nnnn: done
* 0x0501_nnnn: failed
@@ -260,12 +263,12 @@ export function timestampOptionalAbsoluteFromDb(
/**
* First possible operation status in the active range (inclusive).
*/
-export const OPERATION_STATUS_ACTIVE_FIRST = 0x0100_0000;
+export const OPERATION_STATUS_NONFINAL_FIRST = 0x0100_0000;
/**
* LAST possible operation status in the active range (inclusive).
*/
-export const OPERATION_STATUS_ACTIVE_LAST = 0x0113_ffff;
+export const OPERATION_STATUS_NONFINAL_LAST = 0x0210_ffff;
/**
* Status of a withdrawal.
@@ -353,7 +356,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 +379,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 +396,10 @@ export interface ReserveBankInfo {
* Set to undefined if not confirmed yet.
*/
timestampBankConfirmed: DbPreciseTimestamp | undefined;
+
+ wireTypes: string[] | undefined;
+
+ currency: string | undefined;
}
/**
@@ -916,92 +923,6 @@ export interface CoinAllocation {
amount: AmountString;
}
-/**
- * Status of a reward we got from a merchant.
- */
-export interface RewardRecord {
- /**
- * Has the user accepted the tip? Only after the tip has been accepted coins
- * withdrawn from the tip may be used.
- */
- acceptedTimestamp: DbPreciseTimestamp | undefined;
-
- /**
- * The tipped amount.
- */
- rewardAmountRaw: AmountString;
-
- /**
- * Effect on the balance (including fees etc).
- */
- rewardAmountEffective: AmountString;
-
- /**
- * Timestamp, the tip can't be picked up anymore after this deadline.
- */
- rewardExpiration: DbProtocolTimestamp;
-
- /**
- * The exchange that will sign our coins, chosen by the merchant.
- */
- exchangeBaseUrl: string;
-
- /**
- * Base URL of the merchant that is giving us the tip.
- */
- merchantBaseUrl: string;
-
- /**
- * Denomination selection made by the wallet for picking up
- * this tip.
- *
- * FIXME: Put this into some DenomSelectionCacheRecord instead of
- * storing it here!
- */
- denomsSel: DenomSelectionState;
-
- denomSelUid: string;
-
- /**
- * Tip ID chosen by the wallet.
- */
- walletRewardId: string;
-
- /**
- * Secret seed used to derive planchets for this tip.
- */
- secretSeed: string;
-
- /**
- * The merchant's identifier for this reward.
- */
- merchantRewardId: string;
-
- createdTimestamp: DbPreciseTimestamp;
-
- /**
- * The url to be redirected after the tip is accepted.
- */
- next_url: string | undefined;
-
- /**
- * Timestamp for when the wallet finished picking up the tip
- * from the merchant.
- */
- pickedUpTimestamp: DbPreciseTimestamp | undefined;
-
- status: RewardRecordStatus;
-}
-
-export enum RewardRecordStatus {
- PendingPickup = 0x0100_0000,
- SuspendedPickup = 0x0110_0000,
- DialogAccept = 0x0101_0000,
- Done = 0x0500_0000,
- Aborted = 0x0500_0000,
- Failed = 0x0501_000,
-}
-
export enum RefreshCoinStatus {
Pending = 0x0100_0000,
Finished = 0x0500_0000,
@@ -1176,10 +1097,15 @@ export enum PurchaseStatus {
/**
* Query for refund (until auto-refund deadline is reached).
+ *
+ * Legacy state for compatibility.
*/
PendingQueryingAutoRefund = 0x0100_0004,
SuspendedQueryingAutoRefund = 0x0110_0004,
+ FinalizingQueryingAutoRefund = 0x0200_0001,
+ SuspendedFinalizingQueryingAutoRefund = 0x0210_0001,
+
PendingAcceptRefund = 0x0100_0005,
SuspendedPendingAcceptRefund = 0x0110_0005,
@@ -1195,11 +1121,6 @@ export enum PurchaseStatus {
DialogShared = 0x0101_0001,
/**
- * The user has rejected the proposal.
- */
- AbortedProposalRefused = 0x0503_0000,
-
- /**
* Downloading or processing the proposal has failed permanently.
*/
FailedClaim = 0x0501_0000,
@@ -1222,13 +1143,18 @@ export enum PurchaseStatus {
DoneRepurchaseDetected = 0x0500_0001,
/**
- * The payment has been aborted.
+ * The user has rejected the proposal.
*/
- AbortedIncompletePayment = 0x0503_0000,
+ AbortedProposalRefused = 0x0503_0000,
AbortedRefunded = 0x0503_0001,
AbortedOrderDeleted = 0x0503_0002,
+
+ /**
+ * The payment has been aborted.
+ */
+ AbortedIncompletePayment = 0x0503_0003,
}
/**
@@ -1437,6 +1363,7 @@ export interface WgInfoBankIntegrated {
* a Taler-integrated bank.
*/
bankInfo: ReserveBankInfo;
+
/**
* Info about withdrawal accounts, possibly including currency conversion.
*/
@@ -1528,7 +1455,7 @@ export interface WithdrawalGroupRecord {
* The exchange base URL that we're withdrawing from.
* (Redundantly stored, as the reserve record also has this info.)
*/
- exchangeBaseUrl: string;
+ exchangeBaseUrl?: string;
/**
* When was the withdrawal operation started started?
@@ -1562,7 +1489,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 +1506,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 +1514,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.
@@ -1974,7 +1901,7 @@ export enum PeerPullPaymentCreditStatus {
SuspendedCreatePurse = 0x0110_0000,
SuspendedReady = 0x0110_0001,
SuspendedMergeKycRequired = 0x0110_0002,
- SuspendedWithdrawing = 0x0110_0000,
+ SuspendedWithdrawing = 0x0110_0003,
SuspendedAbortingDeletePurse = 0x0113_0000,
@@ -2628,9 +2555,10 @@ export const WalletStoresV1 = {
]),
},
),
+ // Just a tombstone at this point.
rewards: describeStore(
"rewards",
- describeContents<RewardRecord>({ keyPath: "walletRewardId" }),
+ describeContents<any>({ keyPath: "walletRewardId" }),
{
byMerchantTipIdAndBaseUrl: describeIndex("byMerchantRewardIdAndBaseUrl", [
"merchantRewardId",
@@ -2677,7 +2605,9 @@ export const WalletStoresV1 = {
describeContents<BankWithdrawUriRecord>({
keyPath: "talerWithdrawUri",
}),
- {},
+ {
+ byGroup: describeIndex("byGroup", "withdrawalGroupId"),
+ },
),
backupProviders: describeStore(
"backupProviders",
@@ -2936,6 +2866,8 @@ export interface DbDump {
};
}
+const logger = new Logger("db.ts");
+
export async function exportSingleDb(
idb: IDBFactory,
dbName: string,
@@ -3077,8 +3009,6 @@ export interface FixupDescription {
*/
export const walletDbFixups: FixupDescription[] = [];
-const logger = new Logger("db.ts");
-
export async function applyFixups(
db: DbAccess<typeof WalletStoresV1>,
): Promise<void> {
diff --git a/packages/taler-wallet-core/src/dbless.ts b/packages/taler-wallet-core/src/dbless.ts
index dfefe6ef5..ec9655e6f 100644
--- 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();
@@ -125,7 +123,7 @@ export async function topupReserveWithBank(
);
const bankInfo = await getBankWithdrawalInfo(http, wopi.taler_withdraw_uri);
const bankStatusUrl = getBankStatusUrl(wopi.taler_withdraw_uri);
- if (!bankInfo.suggestedExchange) {
+ if (!bankInfo.exchange) {
throw Error("no suggested exchange");
}
const plainPaytoUris =
diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts
index dbba55247..2004c12cb 100644
--- a/packages/taler-wallet-core/src/deposits.ts
+++ b/packages/taler-wallet-core/src/deposits.ts
@@ -387,11 +387,19 @@ export function computeDepositTransactionActions(
case DepositOperationStatus.Finished:
return [TransactionAction.Delete];
case DepositOperationStatus.PendingDeposit:
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case DepositOperationStatus.SuspendedDeposit:
return [TransactionAction.Resume];
case DepositOperationStatus.Aborting:
- return [TransactionAction.Fail, TransactionAction.Suspend];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Fail,
+ TransactionAction.Suspend,
+ ];
case DepositOperationStatus.Aborted:
return [TransactionAction.Delete];
case DepositOperationStatus.Failed:
@@ -399,9 +407,17 @@ export function computeDepositTransactionActions(
case DepositOperationStatus.SuspendedAborting:
return [TransactionAction.Resume, TransactionAction.Fail];
case DepositOperationStatus.PendingKyc:
- return [TransactionAction.Suspend, TransactionAction.Fail];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Fail,
+ ];
case DepositOperationStatus.PendingTrack:
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case DepositOperationStatus.SuspendedKyc:
return [TransactionAction.Resume, TransactionAction.Fail];
case DepositOperationStatus.SuspendedTrack:
@@ -441,7 +457,7 @@ async function refundDepositGroup(
{ storeNames: ["coins"] },
async (tx) => {
const coinRecord = await tx.coins.get(coinPub);
- checkDbInvariant(!!coinRecord);
+ checkDbInvariant(!!coinRecord, `coin ${coinPub} not found in DB`);
return coinRecord.exchangeBaseUrl;
},
);
@@ -776,7 +792,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
index 6262ae4d3..dd88fa836 100644
--- a/packages/taler-wallet-core/src/exchanges.ts
+++ b/packages/taler-wallet-core/src/exchanges.ts
@@ -28,7 +28,6 @@ import {
AgeRestriction,
Amount,
Amounts,
- AsyncFlag,
CancellationToken,
CoinRefreshRequest,
CoinStatus,
@@ -53,6 +52,7 @@ import {
GetExchangeResourcesResponse,
GetExchangeTosResult,
GlobalFees,
+ HttpStatusCode,
LibtoolVersion,
Logger,
NotificationType,
@@ -79,6 +79,7 @@ import {
WireInfo,
assertUnreachable,
checkDbInvariant,
+ checkLogicInvariant,
codecForExchangeKeysJson,
durationMul,
encodeCrock,
@@ -93,6 +94,8 @@ import {
getExpiry,
readSuccessResponseJsonOrThrow,
readSuccessResponseTextOrThrow,
+ readTalerErrorResponse,
+ throwUnexpectedRequestError,
} from "@gnu-taler/taler-util/http";
import {
PendingTaskType,
@@ -103,6 +106,7 @@ import {
TransactionContext,
computeDbBackoff,
constructTaskIdentifier,
+ genericWaitForState,
getAutoRefreshExecuteThreshold,
getExchangeEntryStatusFromRecord,
getExchangeState,
@@ -861,6 +865,41 @@ async function downloadExchangeKeysInfo(
};
}
+type TosMetaResult = { type: "not-found" } | { type: "ok"; etag: string };
+
+/**
+ * Download metadata about an exchange's terms of service.
+ */
+async function downloadTosMeta(
+ wex: WalletExecutionContext,
+ exchangeBaseUrl: string,
+): Promise<TosMetaResult> {
+ logger.trace(`downloading exchange tos metadata for ${exchangeBaseUrl}`);
+ const reqUrl = new URL("terms", exchangeBaseUrl);
+
+ // FIXME: We can/should make a HEAD request here.
+ // Not sure if qtart supports it at the moment.
+ const resp = await wex.http.fetch(reqUrl.href, {
+ cancellationToken: wex.cancellationToken,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NotFound:
+ case HttpStatusCode.NotImplemented:
+ return { type: "not-found" };
+ case HttpStatusCode.Ok:
+ break;
+ default:
+ throwUnexpectedRequestError(resp, await readTalerErrorResponse(resp));
+ }
+
+ const etag = resp.headers.get("etag") || "unknown";
+ return {
+ type: "ok",
+ etag,
+ };
+}
+
async function downloadTosFromAcceptedFormat(
wex: WalletExecutionContext,
baseUrl: string,
@@ -977,9 +1016,7 @@ async function startUpdateExchangeEntry(
wex.ws.exchangeCache.clear();
await tx.exchanges.put(r);
const newExchangeState = getExchangeState(r);
- // Reset retries for updating the exchange entry.
const taskId = TaskIdentifiers.forExchangeUpdate(r);
- await tx.operationRetries.delete(taskId);
return { oldExchangeState, newExchangeState, taskId };
},
);
@@ -989,6 +1026,8 @@ async function startUpdateExchangeEntry(
newExchangeState: newExchangeState,
oldExchangeState: oldExchangeState,
});
+ logger.info(`start update ${exchangeBaseUrl} task ${taskId}`);
+
await wex.taskScheduler.resetTaskRetries(taskId);
}
@@ -1008,132 +1047,6 @@ export interface ReadyExchangeSummary {
scopeInfo: ScopeInfo;
}
-async function internalWaitReadyExchange(
- wex: WalletExecutionContext,
- canonUrl: string,
- exchangeNotifFlag: AsyncFlag,
- options: {
- cancellationToken?: CancellationToken;
- forceUpdate?: boolean;
- expectedMasterPub?: string;
- } = {},
-): Promise<ReadyExchangeSummary> {
- const operationId = constructTaskIdentifier({
- tag: PendingTaskType.ExchangeUpdate,
- exchangeBaseUrl: canonUrl,
- });
- while (true) {
- if (wex.cancellationToken.isCancelled) {
- throw Error("cancelled");
- }
- logger.info(`waiting for ready exchange ${canonUrl}`);
- const { exchange, exchangeDetails, retryInfo, scopeInfo } =
- await wex.db.runReadOnlyTx(
- {
- storeNames: [
- "exchanges",
- "exchangeDetails",
- "operationRetries",
- "globalCurrencyAuditors",
- "globalCurrencyExchanges",
- ],
- },
- async (tx) => {
- const exchange = await tx.exchanges.get(canonUrl);
- const exchangeDetails = await getExchangeRecordsInternal(
- tx,
- canonUrl,
- );
- const retryInfo = await tx.operationRetries.get(operationId);
- let scopeInfo: ScopeInfo | undefined = undefined;
- if (exchange && exchangeDetails) {
- scopeInfo = await internalGetExchangeScopeInfo(tx, exchangeDetails);
- }
- return { exchange, exchangeDetails, retryInfo, scopeInfo };
- },
- );
-
- if (!exchange) {
- throw Error("exchange entry does not exist anymore");
- }
-
- let ready = false;
-
- switch (exchange.updateStatus) {
- case ExchangeEntryDbUpdateStatus.Ready:
- ready = true;
- break;
- case ExchangeEntryDbUpdateStatus.ReadyUpdate:
- // If the update is forced,
- // we wait until we're in a full "ready" state,
- // as we're not happy with the stale information.
- if (!options.forceUpdate) {
- ready = true;
- }
- break;
- case ExchangeEntryDbUpdateStatus.UnavailableUpdate:
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_EXCHANGE_UNAVAILABLE,
- {
- exchangeBaseUrl: canonUrl,
- innerError: retryInfo?.lastError,
- },
- );
- default: {
- if (retryInfo) {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_EXCHANGE_UNAVAILABLE,
- {
- exchangeBaseUrl: canonUrl,
- innerError: retryInfo?.lastError,
- },
- );
- }
- }
- }
-
- if (!ready) {
- logger.info("waiting for exchange update notification");
- await exchangeNotifFlag.wait();
- logger.info("done waiting for exchange update notification");
- exchangeNotifFlag.reset();
- continue;
- }
-
- if (!exchangeDetails) {
- throw Error("invariant failed");
- }
-
- if (!scopeInfo) {
- throw Error("invariant failed");
- }
-
- const res: ReadyExchangeSummary = {
- currency: exchangeDetails.currency,
- exchangeBaseUrl: canonUrl,
- masterPub: exchangeDetails.masterPublicKey,
- tosStatus: getExchangeTosStatusFromRecord(exchange),
- tosAcceptedEtag: exchange.tosAcceptedEtag,
- wireInfo: exchangeDetails.wireInfo,
- protocolVersionRange: exchangeDetails.protocolVersionRange,
- tosCurrentEtag: exchange.tosCurrentEtag,
- tosAcceptedTimestamp: timestampOptionalPreciseFromDb(
- exchange.tosAcceptedTimestamp,
- ),
- scopeInfo,
- };
-
- if (options.expectedMasterPub) {
- if (res.masterPub !== options.expectedMasterPub) {
- throw Error(
- "public key of the exchange does not match expected public key",
- );
- }
- }
- return res;
- }
-}
-
/**
* Ensure that a fresh exchange entry exists for the given
* exchange base URL.
@@ -1152,11 +1065,11 @@ export async function fetchFreshExchange(
wex: WalletExecutionContext,
baseUrl: string,
options: {
- cancellationToken?: CancellationToken;
forceUpdate?: boolean;
- expectedMasterPub?: string;
} = {},
): Promise<ReadyExchangeSummary> {
+ logger.info(`fetch fresh ${baseUrl} forced ${options.forceUpdate}`);
+
if (!options.forceUpdate) {
const cachedResp = wex.ws.exchangeCache.get(baseUrl);
if (cachedResp) {
@@ -1186,39 +1099,131 @@ async function waitReadyExchange(
} = {},
): Promise<ReadyExchangeSummary> {
logger.trace(`waiting for exchange ${canonUrl} to become ready`);
- // FIXME: We should use Symbol.dispose magic here for cleanup!
- const exchangeNotifFlag = new AsyncFlag();
- // Raise exchangeNotifFlag whenever we get a notification
- // about our exchange.
- const cancelNotif = wex.ws.addNotificationListener((notif) => {
- if (
- notif.type === NotificationType.ExchangeStateTransition &&
- notif.exchangeBaseUrl === canonUrl
- ) {
- logger.info(`raising update notification: ${j2s(notif)}`);
- exchangeNotifFlag.raise();
- }
+ const operationId = constructTaskIdentifier({
+ tag: PendingTaskType.ExchangeUpdate,
+ exchangeBaseUrl: canonUrl,
});
- const unregisterOnCancelled = wex.cancellationToken.onCancelled(() => {
- cancelNotif();
- exchangeNotifFlag.raise();
+ let res: ReadyExchangeSummary | undefined = undefined;
+
+ await genericWaitForState(wex, {
+ filterNotification(notif): boolean {
+ return (
+ notif.type === NotificationType.ExchangeStateTransition &&
+ notif.exchangeBaseUrl === canonUrl
+ );
+ },
+ async checkState(): Promise<boolean> {
+ const { exchange, exchangeDetails, retryInfo, scopeInfo } =
+ await wex.db.runReadOnlyTx(
+ {
+ storeNames: [
+ "exchanges",
+ "exchangeDetails",
+ "operationRetries",
+ "globalCurrencyAuditors",
+ "globalCurrencyExchanges",
+ ],
+ },
+ async (tx) => {
+ const exchange = await tx.exchanges.get(canonUrl);
+ const exchangeDetails = await getExchangeRecordsInternal(
+ tx,
+ canonUrl,
+ );
+ const retryInfo = await tx.operationRetries.get(operationId);
+ let scopeInfo: ScopeInfo | undefined = undefined;
+ if (exchange && exchangeDetails) {
+ scopeInfo = await internalGetExchangeScopeInfo(
+ tx,
+ exchangeDetails,
+ );
+ }
+ return { exchange, exchangeDetails, retryInfo, scopeInfo };
+ },
+ );
+
+ if (!exchange) {
+ throw Error("exchange entry does not exist anymore");
+ }
+
+ let ready = false;
+
+ switch (exchange.updateStatus) {
+ case ExchangeEntryDbUpdateStatus.Ready:
+ ready = true;
+ break;
+ case ExchangeEntryDbUpdateStatus.ReadyUpdate:
+ // If the update is forced,
+ // we wait until we're in a full "ready" state,
+ // as we're not happy with the stale information.
+ if (!options.forceUpdate) {
+ ready = true;
+ }
+ break;
+ case ExchangeEntryDbUpdateStatus.UnavailableUpdate:
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_EXCHANGE_UNAVAILABLE,
+ {
+ exchangeBaseUrl: canonUrl,
+ innerError: retryInfo?.lastError,
+ },
+ );
+ default: {
+ if (retryInfo) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_EXCHANGE_UNAVAILABLE,
+ {
+ exchangeBaseUrl: canonUrl,
+ innerError: retryInfo?.lastError,
+ },
+ );
+ }
+ }
+ }
+
+ if (!ready) {
+ return false;
+ }
+
+ if (!exchangeDetails) {
+ throw Error("invariant failed");
+ }
+
+ if (!scopeInfo) {
+ throw Error("invariant failed");
+ }
+
+ const mySummary: ReadyExchangeSummary = {
+ currency: exchangeDetails.currency,
+ exchangeBaseUrl: canonUrl,
+ masterPub: exchangeDetails.masterPublicKey,
+ tosStatus: getExchangeTosStatusFromRecord(exchange),
+ tosAcceptedEtag: exchange.tosAcceptedEtag,
+ wireInfo: exchangeDetails.wireInfo,
+ protocolVersionRange: exchangeDetails.protocolVersionRange,
+ tosCurrentEtag: exchange.tosCurrentEtag,
+ tosAcceptedTimestamp: timestampOptionalPreciseFromDb(
+ exchange.tosAcceptedTimestamp,
+ ),
+ scopeInfo,
+ };
+
+ if (options.expectedMasterPub) {
+ if (mySummary.masterPub !== options.expectedMasterPub) {
+ throw Error(
+ "public key of the exchange does not match expected public key",
+ );
+ }
+ }
+ res = mySummary;
+ return true;
+ },
});
- try {
- const res = await internalWaitReadyExchange(
- wex,
- canonUrl,
- exchangeNotifFlag,
- options,
- );
- logger.info("done waiting for ready exchange");
- return res;
- } finally {
- unregisterOnCancelled();
- cancelNotif();
- }
+ checkLogicInvariant(!!res);
+ return res;
}
function checkPeerPaymentsDisabled(
@@ -1361,7 +1366,6 @@ export async function updateExchangeFromUrlHandler(
);
refreshCheckNecessary = false;
}
-
if (!(updateNecessary || refreshCheckNecessary)) {
logger.trace("update not necessary, running again later");
return TaskRunResult.runAgainAt(
@@ -1423,15 +1427,7 @@ export async function updateExchangeFromUrlHandler(
logger.trace("finished validating exchange /wire info");
- // We download the text/plain version here,
- // because that one needs to exist, and we
- // will get the current etag from the response.
- const tosDownload = await downloadTosFromAcceptedFormat(
- wex,
- exchangeBaseUrl,
- timeout,
- ["text/plain"],
- );
+ const tosMeta = await downloadTosMeta(wex, exchangeBaseUrl);
logger.trace("updating exchange info in database");
@@ -1524,7 +1520,14 @@ export async function updateExchangeFromUrlHandler(
};
r.noFees = noFees;
r.peerPaymentsDisabled = peerPaymentsDisabled;
- r.tosCurrentEtag = tosDownload.tosEtag;
+ switch (tosMeta.type) {
+ case "not-found":
+ r.tosCurrentEtag = undefined;
+ break;
+ case "ok":
+ r.tosCurrentEtag = tosMeta.etag;
+ break;
+ }
if (existingDetails?.rowId) {
newDetails.rowId = existingDetails.rowId;
}
@@ -1550,7 +1553,10 @@ export async function updateExchangeFromUrlHandler(
r.cachebreakNextUpdate = false;
await tx.exchanges.put(r);
const drRowId = await tx.exchangeDetails.put(newDetails);
- checkDbInvariant(typeof drRowId.key === "number");
+ checkDbInvariant(
+ typeof drRowId.key === "number",
+ "exchange details key is not a number",
+ );
for (const sk of keysInfo.signingKeys) {
// FIXME: validate signing keys before inserting them
@@ -2229,10 +2235,12 @@ export async function markExchangeUsed(
logger.info(`marking exchange ${exchangeBaseUrl} as used`);
const exch = await tx.exchanges.get(exchangeBaseUrl);
if (!exch) {
+ logger.info(`exchange ${exchangeBaseUrl} NOT found`);
return {
notif: undefined,
};
}
+
const oldExchangeState = getExchangeState(exch);
switch (exch.entryStatus) {
case ExchangeEntryDbRecordStatus.Ephemeral:
diff --git a/packages/taler-wallet-core/src/instructedAmountConversion.ts b/packages/taler-wallet-core/src/instructedAmountConversion.ts
index 1f7d95959..5b399a0a7 100644
--- a/packages/taler-wallet-core/src/instructedAmountConversion.ts
+++ b/packages/taler-wallet-core/src/instructedAmountConversion.ts
@@ -283,7 +283,7 @@ async function getAvailableDenoms(
coinAvail.exchangeBaseUrl,
coinAvail.denomPubHash,
]);
- checkDbInvariant(!!denom);
+ checkDbInvariant(!!denom, `denomination of a coin is missing hash: ${coinAvail.denomPubHash}`);
if (denom.isRevoked || !denom.isOffered) {
continue;
}
@@ -472,7 +472,7 @@ export async function getMaxDepositAmount(
export function getMaxDepositAmountForAvailableCoins(
denoms: AvailableCoins,
currency: string,
-) {
+): AmountWithFee {
const zero = Amounts.zeroOfCurrency(currency);
if (!denoms.list.length) {
// no coins in the database
@@ -663,8 +663,13 @@ function rankDenominationForWithdrawals(
//different exchanges may have different wireFee
//ranking should take the relative contribution in the exchange
//which is (value - denomFee / fixedFee)
- const rate1 = Amounts.divmod(d1.value, d1.denomWithdraw).quotient;
- const rate2 = Amounts.divmod(d2.value, d2.denomWithdraw).quotient;
+
+ const rate1 = Amounts.isZero(d1.denomWithdraw)
+ ? Number.MIN_SAFE_INTEGER
+ : Amounts.divmod(d1.value, d1.denomWithdraw).quotient;
+ const rate2 = Amounts.isZero(d2.denomWithdraw)
+ ? Number.MIN_SAFE_INTEGER
+ : Amounts.divmod(d2.value, d2.denomWithdraw).quotient;
const contribCmp = rate1 === rate2 ? 0 : rate1 < rate2 ? 1 : -1;
return (
contribCmp ||
@@ -719,8 +724,13 @@ function rankDenominationForDeposit(
//different exchanges may have different wireFee
//ranking should take the relative contribution in the exchange
//which is (value - denomFee / fixedFee)
- const rate1 = Amounts.divmod(d1.value, d1.denomDeposit).quotient;
- const rate2 = Amounts.divmod(d2.value, d2.denomDeposit).quotient;
+ const rate1 = Amounts.isZero(d1.denomDeposit)
+ ? Number.MIN_SAFE_INTEGER
+ : Amounts.divmod(d1.value, d1.denomDeposit).quotient;
+ const rate2 = Amounts.isZero(d2.denomDeposit)
+ ? Number.MIN_SAFE_INTEGER
+ : Amounts.divmod(d2.value, d2.denomDeposit).quotient;
+
const contribCmp = rate1 === rate2 ? 0 : rate1 < rate2 ? 1 : -1;
return (
contribCmp ||
diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts
index 49ebc282e..ee154252f 100644
--- a/packages/taler-wallet-core/src/pay-merchant.ts
+++ b/packages/taler-wallet-core/src/pay-merchant.ts
@@ -34,13 +34,16 @@ import {
assertUnreachable,
AsyncFlag,
checkDbInvariant,
+ CheckPayTemplateReponse,
+ CheckPayTemplateRequest,
codecForAbortResponse,
codecForMerchantContractTerms,
codecForMerchantOrderStatusPaid,
codecForMerchantPayResponse,
- codecForMerchantPostOrderResponse,
+ codecForPostOrderResponse,
codecForProposal,
codecForWalletRefundResponse,
+ codecForWalletTemplateDetails,
CoinDepositPermission,
CoinRefreshRequest,
ConfirmPayResult,
@@ -76,6 +79,8 @@ import {
TalerError,
TalerErrorCode,
TalerErrorDetail,
+ TalerMerchantApi,
+ TalerMerchantInstanceHttpClient,
TalerPreciseTimestamp,
TalerProtocolViolationError,
TalerUriAction,
@@ -336,7 +341,6 @@ export class PayMerchantTransactionContext implements TransactionContext {
return;
}
await tx.purchases.put(purchase);
- await tx.operationRetries.delete(this.taskId);
const newTxState = computePayMerchantTransactionState(purchase);
return { oldTxState, newTxState };
},
@@ -1022,11 +1026,17 @@ async function storeFirstPaySuccess(
purchase.merchantPaySig = payResponse.sig;
purchase.posConfirmation = payResponse.pos_confirmation;
const dl = purchase.download;
- checkDbInvariant(!!dl);
+ checkDbInvariant(
+ !!dl,
+ `purchase ${purchase.orderId} without ct downloaded`,
+ );
const contractTermsRecord = await tx.contractTerms.get(
dl.contractTermsHash,
);
- checkDbInvariant(!!contractTermsRecord);
+ checkDbInvariant(
+ !!contractTermsRecord,
+ `no contract terms found for purchase ${purchase.orderId}`,
+ );
const contractData = extractContractData(
contractTermsRecord.contractTermsRaw,
dl.contractTermsHash,
@@ -1578,39 +1588,95 @@ 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);
+ }
+
+ // FIXME: Put body.currencies *and* body.currency in the set of
+ // supported currencies.
+
+ 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 +1687,7 @@ export async function preparePayForTemplate(
});
const resp = await readSuccessResponseJsonOrThrow(
httpReq,
- codecForMerchantPostOrderResponse(),
+ codecForPostOrderResponse(),
);
const payUri = stringifyPayUri({
@@ -2027,6 +2093,7 @@ export async function processPurchase(
case PurchaseStatus.PendingPayingReplay:
return processPurchasePay(wex, proposalId);
case PurchaseStatus.PendingQueryingRefund:
+ case PurchaseStatus.FinalizingQueryingAutoRefund:
return processPurchaseQueryRefund(wex, purchase);
case PurchaseStatus.PendingQueryingAutoRefund:
return processPurchaseAutoRefund(wex, purchase);
@@ -2051,6 +2118,7 @@ export async function processPurchase(
case PurchaseStatus.SuspendedPendingAcceptRefund:
case PurchaseStatus.SuspendedQueryingAutoRefund:
case PurchaseStatus.SuspendedQueryingRefund:
+ case PurchaseStatus.SuspendedFinalizingQueryingAutoRefund:
case PurchaseStatus.FailedAbort:
case PurchaseStatus.FailedPaidByOther:
return TaskRunResult.finished();
@@ -2096,7 +2164,7 @@ async function processPurchasePay(
logger.trace(`paying with session ID ${sessionId}`);
const payInfo = purchase.payInfo;
- checkDbInvariant(!!payInfo, "payInfo");
+ checkDbInvariant(!!payInfo, `purchase ${purchase.orderId} without payInfo`);
const download = await expectProposalDownload(wex, purchase);
@@ -2428,6 +2496,9 @@ const transitionSuspend: {
[PurchaseStatus.PendingQueryingAutoRefund]: {
next: PurchaseStatus.SuspendedQueryingAutoRefund,
},
+ [PurchaseStatus.FinalizingQueryingAutoRefund]: {
+ next: PurchaseStatus.SuspendedFinalizingQueryingAutoRefund,
+ },
};
const transitionResume: {
@@ -2450,6 +2521,9 @@ const transitionResume: {
[PurchaseStatus.SuspendedQueryingAutoRefund]: {
next: PurchaseStatus.PendingQueryingAutoRefund,
},
+ [PurchaseStatus.SuspendedFinalizingQueryingAutoRefund]: {
+ next: PurchaseStatus.FinalizingQueryingAutoRefund,
+ },
};
export function computePayMerchantTransactionState(
@@ -2578,6 +2652,16 @@ export function computePayMerchantTransactionState(
major: TransactionMajorState.Failed,
minor: TransactionMinorState.PaidByOther,
};
+ case PurchaseStatus.FinalizingQueryingAutoRefund:
+ return {
+ major: TransactionMajorState.Finalizing,
+ minor: TransactionMinorState.AutoRefund,
+ };
+ case PurchaseStatus.SuspendedFinalizingQueryingAutoRefund:
+ return {
+ major: TransactionMajorState.SuspendedFinalizing,
+ minor: TransactionMinorState.AutoRefund,
+ };
default:
assertUnreachable(purchaseRecord.purchaseStatus);
}
@@ -2589,21 +2673,45 @@ export function computePayMerchantTransactionActions(
switch (purchaseRecord.purchaseStatus) {
// Pending States
case PurchaseStatus.PendingDownloadingProposal:
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case PurchaseStatus.PendingPaying:
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case PurchaseStatus.PendingPayingReplay:
// Special "abort" since it goes back to "done".
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case PurchaseStatus.PendingQueryingAutoRefund:
// Special "abort" since it goes back to "done".
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case PurchaseStatus.PendingQueryingRefund:
// Special "abort" since it goes back to "done".
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case PurchaseStatus.PendingAcceptRefund:
// Special "abort" since it goes back to "done".
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
// Suspended Pending States
case PurchaseStatus.SuspendedDownloadingProposal:
return [TransactionAction.Resume, TransactionAction.Abort];
@@ -2623,14 +2731,18 @@ export function computePayMerchantTransactionActions(
return [TransactionAction.Resume, TransactionAction.Abort];
// Aborting States
case PurchaseStatus.AbortingWithRefund:
- return [TransactionAction.Fail, TransactionAction.Suspend];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Fail,
+ TransactionAction.Suspend,
+ ];
case PurchaseStatus.SuspendedAbortingWithRefund:
return [TransactionAction.Fail, TransactionAction.Resume];
// Dialog States
case PurchaseStatus.DialogProposed:
- return [];
+ return [TransactionAction.Retry];
case PurchaseStatus.DialogShared:
- return [];
+ return [TransactionAction.Retry];
// Final States
case PurchaseStatus.AbortedProposalRefused:
case PurchaseStatus.AbortedOrderDeleted:
@@ -2648,6 +2760,14 @@ export function computePayMerchantTransactionActions(
return [TransactionAction.Delete];
case PurchaseStatus.FailedPaidByOther:
return [TransactionAction.Delete];
+ case PurchaseStatus.FinalizingQueryingAutoRefund:
+ return [
+ TransactionAction.Suspend,
+ TransactionAction.Retry,
+ TransactionAction.Delete,
+ ];
+ case PurchaseStatus.SuspendedFinalizingQueryingAutoRefund:
+ return [TransactionAction.Resume, TransactionAction.Delete];
default:
assertUnreachable(purchaseRecord.purchaseStatus);
}
@@ -2850,8 +2970,12 @@ async function processPurchaseAutoRefund(
logger.warn("purchase does not exist anymore");
return;
}
- if (p.purchaseStatus !== PurchaseStatus.PendingQueryingAutoRefund) {
- return;
+ switch (p.purchaseStatus) {
+ case PurchaseStatus.PendingQueryingAutoRefund:
+ case PurchaseStatus.FinalizingQueryingAutoRefund:
+ break;
+ default:
+ return;
}
const oldTxState = computePayMerchantTransactionState(p);
p.purchaseStatus = PurchaseStatus.Done;
@@ -2875,7 +2999,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, {
@@ -2898,8 +3021,12 @@ async function processPurchaseAutoRefund(
logger.warn("purchase does not exist anymore");
return;
}
- if (p.purchaseStatus !== PurchaseStatus.PendingQueryingAutoRefund) {
- return;
+ switch (p.purchaseStatus) {
+ case PurchaseStatus.PendingQueryingAutoRefund:
+ case PurchaseStatus.FinalizingQueryingAutoRefund:
+ break;
+ default:
+ return;
}
const oldTxState = computePayMerchantTransactionState(p);
p.purchaseStatus = PurchaseStatus.PendingAcceptRefund;
@@ -2939,7 +3066,7 @@ async function processPurchaseAbortingRefund(
for (let i = 0; i < payCoinSelection.coinPubs.length; i++) {
const coinPub = payCoinSelection.coinPubs[i];
const coin = await tx.coins.get(coinPub);
- checkDbInvariant(!!coin, "expected coin to be present");
+ checkDbInvariant(!!coin, `coin not found for ${coinPub}`);
abortingCoins.push({
coin_pub: coinPub,
contribution: Amounts.stringify(payCoinSelection.coinContributions[i]),
@@ -3443,7 +3570,8 @@ async function storeRefunds(
if (isAborting) {
myPurchase.purchaseStatus = PurchaseStatus.AbortedRefunded;
} else if (shouldCheckAutoRefund) {
- myPurchase.purchaseStatus = PurchaseStatus.PendingQueryingAutoRefund;
+ myPurchase.purchaseStatus =
+ PurchaseStatus.FinalizingQueryingAutoRefund;
} else {
myPurchase.purchaseStatus = PurchaseStatus.Done;
}
diff --git a/packages/taler-wallet-core/src/pay-peer-common.ts b/packages/taler-wallet-core/src/pay-peer-common.ts
index bfd39b657..a1729ced7 100644
--- a/packages/taler-wallet-core/src/pay-peer-common.ts
+++ b/packages/taler-wallet-core/src/pay-peer-common.ts
@@ -140,10 +140,10 @@ export async function getMergeReserveInfo(
{ storeNames: ["exchanges", "reserves"] },
async (tx) => {
const ex = await tx.exchanges.get(req.exchangeBaseUrl);
- checkDbInvariant(!!ex);
+ checkDbInvariant(!!ex, `no exchange record for ${req.exchangeBaseUrl}`);
if (ex.currentMergeReserveRowId != null) {
const reserve = await tx.reserves.get(ex.currentMergeReserveRowId);
- checkDbInvariant(!!reserve);
+ checkDbInvariant(!!reserve, `reserver ${ex.currentMergeReserveRowId} missing in db`);
return reserve;
}
const reserve: ReserveRecord = {
@@ -151,7 +151,7 @@ export async function getMergeReserveInfo(
reservePub: newReservePair.pub,
};
const insertResp = await tx.reserves.put(reserve);
- checkDbInvariant(typeof insertResp.key === "number");
+ checkDbInvariant(typeof insertResp.key === "number", `reserve key is not a number`);
reserve.rowId = insertResp.key;
ex.currentMergeReserveRowId = reserve.rowId;
await tx.exchanges.put(ex);
diff --git a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts
index 840c244d0..d3c4f0d7f 100644
--- a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts
@@ -59,6 +59,7 @@ import {
TombstoneTag,
TransactionContext,
constructTaskIdentifier,
+ requireExchangeTosAcceptedOrThrow,
} from "./common.js";
import {
KycPendingInfo,
@@ -1021,7 +1022,8 @@ export async function initiatePeerPullPayment(
const exchangeBaseUrl = maybeExchangeBaseUrl;
- await fetchFreshExchange(wex, exchangeBaseUrl);
+ const exchange = await fetchFreshExchange(wex, exchangeBaseUrl);
+ requireExchangeTosAcceptedOrThrow(exchange);
const mergeReserveInfo = await getMergeReserveInfo(wex, {
exchangeBaseUrl: exchangeBaseUrl,
@@ -1039,7 +1041,10 @@ export async function initiatePeerPullPayment(
const withdrawalGroupId = encodeCrock(getRandomBytes(32));
const mergeReserveRowId = mergeReserveInfo.rowId;
- checkDbInvariant(!!mergeReserveRowId);
+ checkDbInvariant(
+ !!mergeReserveRowId,
+ `merge reserve for ${exchangeBaseUrl} without rowid`,
+ );
const contractEncNonce = encodeCrock(getRandomBytes(24));
@@ -1184,15 +1189,31 @@ export function computePeerPullCreditTransactionActions(
): TransactionAction[] {
switch (pullCreditRecord.status) {
case PeerPullPaymentCreditStatus.PendingCreatePurse:
- return [TransactionAction.Abort, TransactionAction.Suspend];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Abort,
+ TransactionAction.Suspend,
+ ];
case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
- return [TransactionAction.Abort, TransactionAction.Suspend];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Abort,
+ TransactionAction.Suspend,
+ ];
case PeerPullPaymentCreditStatus.PendingReady:
- return [TransactionAction.Abort, TransactionAction.Suspend];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Abort,
+ TransactionAction.Suspend,
+ ];
case PeerPullPaymentCreditStatus.Done:
return [TransactionAction.Delete];
case PeerPullPaymentCreditStatus.PendingWithdrawing:
- return [TransactionAction.Abort, TransactionAction.Suspend];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Abort,
+ TransactionAction.Suspend,
+ ];
case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
return [TransactionAction.Resume, TransactionAction.Abort];
case PeerPullPaymentCreditStatus.SuspendedReady:
@@ -1204,7 +1225,11 @@ export function computePeerPullCreditTransactionActions(
case PeerPullPaymentCreditStatus.Aborted:
return [TransactionAction.Delete];
case PeerPullPaymentCreditStatus.AbortingDeletePurse:
- return [TransactionAction.Suspend, TransactionAction.Fail];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Fail,
+ ];
case PeerPullPaymentCreditStatus.Failed:
return [TransactionAction.Delete];
case PeerPullPaymentCreditStatus.Expired:
diff --git a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
index 0355b58ad..e9be15026 100644
--- a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
@@ -1000,7 +1000,7 @@ export function computePeerPullDebitTransactionActions(
): TransactionAction[] {
switch (pullDebitRecord.status) {
case PeerPullDebitRecordStatus.DialogProposed:
- return [];
+ return [TransactionAction.Retry, TransactionAction.Delete];
case PeerPullDebitRecordStatus.PendingDeposit:
return [TransactionAction.Abort, TransactionAction.Suspend];
case PeerPullDebitRecordStatus.Done:
diff --git a/packages/taler-wallet-core/src/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/pay-peer-push-credit.ts
index 93f1a63a7..ae2372eeb 100644
--- a/packages/taler-wallet-core/src/pay-peer-push-credit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-push-credit.ts
@@ -61,6 +61,7 @@ import {
TombstoneTag,
TransactionContext,
constructTaskIdentifier,
+ requireExchangeTosAcceptedOrThrow,
} from "./common.js";
import {
KycPendingInfo,
@@ -407,7 +408,8 @@ export async function preparePeerPushCredit(
const exchangeBaseUrl = uri.exchangeBaseUrl;
- await fetchFreshExchange(wex, exchangeBaseUrl);
+ const exchange = await fetchFreshExchange(wex, exchangeBaseUrl);
+ requireExchangeTosAcceptedOrThrow(exchange);
const contractPriv = uri.contractPriv;
const contractPub = encodeCrock(eddsaGetPublic(decodeCrock(contractPriv)));
@@ -872,7 +874,10 @@ export async function processPeerPushCredit(
`processing peerPushCredit in state ${peerInc.status.toString(16)}`,
);
- checkDbInvariant(!!contractTerms);
+ checkDbInvariant(
+ !!contractTerms,
+ `not contract terms for peer push ${peerPushCreditId}`,
+ );
switch (peerInc.status) {
case PeerPushCreditStatus.PendingMergeKycRequired: {
@@ -1011,15 +1016,27 @@ export function computePeerPushCreditTransactionActions(
): TransactionAction[] {
switch (pushCreditRecord.status) {
case PeerPushCreditStatus.DialogProposed:
- return [TransactionAction.Delete];
+ return [TransactionAction.Retry, TransactionAction.Delete];
case PeerPushCreditStatus.PendingMerge:
- return [TransactionAction.Abort, TransactionAction.Suspend];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Abort,
+ TransactionAction.Suspend,
+ ];
case PeerPushCreditStatus.Done:
return [TransactionAction.Delete];
case PeerPushCreditStatus.PendingMergeKycRequired:
- return [TransactionAction.Abort, TransactionAction.Suspend];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Abort,
+ TransactionAction.Suspend,
+ ];
case PeerPushCreditStatus.PendingWithdrawing:
- return [TransactionAction.Suspend, TransactionAction.Fail];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Fail,
+ ];
case PeerPushCreditStatus.SuspendedMerge:
return [TransactionAction.Resume, TransactionAction.Abort];
case PeerPushCreditStatus.SuspendedMergeKycRequired:
diff --git a/packages/taler-wallet-core/src/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/pay-peer-push-debit.ts
index 6452407ff..f8e6adb3c 100644
--- a/packages/taler-wallet-core/src/pay-peer-push-debit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-push-debit.ts
@@ -406,7 +406,10 @@ async function handlePurseCreationConflict(
const instructedAmount = Amounts.parseOrThrow(peerPushInitiation.amount);
const sel = peerPushInitiation.coinSel;
- checkDbInvariant(!!sel);
+ checkDbInvariant(
+ !!sel,
+ `no coin selected for peer push initiation ${peerPushInitiation.pursePub}`,
+ );
const repair: PreviousPayCoins = [];
@@ -1218,17 +1221,37 @@ export function computePeerPushDebitTransactionActions(
): TransactionAction[] {
switch (ppiRecord.status) {
case PeerPushDebitStatus.PendingCreatePurse:
- return [TransactionAction.Abort, TransactionAction.Suspend];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Abort,
+ TransactionAction.Suspend,
+ ];
case PeerPushDebitStatus.PendingReady:
- return [TransactionAction.Abort, TransactionAction.Suspend];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Abort,
+ TransactionAction.Suspend,
+ ];
case PeerPushDebitStatus.Aborted:
return [TransactionAction.Delete];
case PeerPushDebitStatus.AbortingDeletePurse:
- return [TransactionAction.Suspend, TransactionAction.Fail];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Fail,
+ ];
case PeerPushDebitStatus.AbortingRefreshDeleted:
- return [TransactionAction.Suspend, TransactionAction.Fail];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Fail,
+ ];
case PeerPushDebitStatus.AbortingRefreshExpired:
- return [TransactionAction.Suspend, TransactionAction.Fail];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Fail,
+ ];
case PeerPushDebitStatus.SuspendedAbortingRefreshExpired:
return [TransactionAction.Resume, TransactionAction.Fail];
case PeerPushDebitStatus.SuspendedAbortingDeletePurse:
diff --git a/packages/taler-wallet-core/src/recoup.ts b/packages/taler-wallet-core/src/recoup.ts
index 6a09f9a0e..be5731b0b 100644
--- a/packages/taler-wallet-core/src/recoup.ts
+++ b/packages/taler-wallet-core/src/recoup.ts
@@ -199,8 +199,8 @@ async function recoupRefreshCoin(
revokedCoin.exchangeBaseUrl,
revokedCoin.denomPubHash,
);
- checkDbInvariant(!!oldCoinDenom);
- checkDbInvariant(!!revokedCoinDenom);
+ checkDbInvariant(!!oldCoinDenom, `no denom for coin, hash ${oldCoin.denomPubHash}`);
+ checkDbInvariant(!!revokedCoinDenom, `no revoked denom for coin, hash ${revokedCoin.denomPubHash}`);
revokedCoin.status = CoinStatus.Dormant;
if (!revokedCoin.spendAllocation) {
// We don't know what happened to this coin
diff --git a/packages/taler-wallet-core/src/refresh.ts b/packages/taler-wallet-core/src/refresh.ts
index 7800967e6..05c65f6b6 100644
--- a/packages/taler-wallet-core/src/refresh.ts
+++ b/packages/taler-wallet-core/src/refresh.ts
@@ -29,7 +29,6 @@ import {
Amounts,
amountToPretty,
assertUnreachable,
- AsyncFlag,
checkDbInvariant,
codecForCoinHistoryResponse,
codecForExchangeMeltResponse,
@@ -68,12 +67,14 @@ import {
WalletNotification,
} from "@gnu-taler/taler-util";
import {
+ HttpResponse,
readSuccessResponseJsonOrThrow,
readTalerErrorResponse,
throwUnexpectedRequestError,
} from "@gnu-taler/taler-util/http";
import {
constructTaskIdentifier,
+ genericWaitForState,
makeCoinsVisible,
PendingTaskType,
TaskIdStr,
@@ -386,7 +387,6 @@ async function getCoinAvailabilityForDenom(
denom: DenominationInfo,
ageRestriction: number,
): Promise<CoinAvailabilityRecord> {
- checkDbInvariant(!!denom);
let car = await tx.coinAvailability.get([
denom.exchangeBaseUrl,
denom.denomPubHash,
@@ -537,7 +537,10 @@ async function destroyRefreshSession(
denom,
oldCoin.maxAge,
);
- checkDbInvariant(car.pendingRefreshOutputCount != null);
+ checkDbInvariant(
+ car.pendingRefreshOutputCount != null,
+ `no pendingRefreshOutputCount for denom ${dph}`,
+ );
car.pendingRefreshOutputCount =
car.pendingRefreshOutputCount - refreshSession.newDenoms[i].count;
await tx.coinAvailability.put(car);
@@ -693,7 +696,7 @@ async function refreshMelt(
switch (resp.status) {
case HttpStatusCode.NotFound: {
const errDetail = await readTalerErrorResponse(resp);
- await handleRefreshMeltNotFound(ctx, coinIndex, errDetail);
+ await handleRefreshMeltNotFound(ctx, coinIndex, resp, errDetail);
return;
}
case HttpStatusCode.Gone: {
@@ -898,9 +901,18 @@ async function handleRefreshMeltConflict(
async function handleRefreshMeltNotFound(
ctx: RefreshTransactionContext,
coinIndex: number,
+ resp: HttpResponse,
errDetails: TalerErrorDetail,
): Promise<void> {
- // FIXME: Validate the exchange's error response
+ // Make sure that we only act on a 404 that indicates a final problem
+ // with the coin.
+ switch (errDetails.code) {
+ case TalerErrorCode.EXCHANGE_GENERIC_COIN_UNKNOWN:
+ case TalerErrorCode.EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN:
+ break;
+ default:
+ throwUnexpectedRequestError(resp, errDetails);
+ }
await ctx.wex.db.runReadWriteTx(
{
storeNames: [
@@ -1242,7 +1254,10 @@ async function refreshReveal(
coin.exchangeBaseUrl,
coin.denomPubHash,
);
- checkDbInvariant(!!denomInfo);
+ checkDbInvariant(
+ !!denomInfo,
+ `no denom with hash ${coin.denomPubHash}`,
+ );
const car = await getCoinAvailabilityForDenom(
wex,
tx,
@@ -1252,6 +1267,7 @@ async function refreshReveal(
checkDbInvariant(
car.pendingRefreshOutputCount != null &&
car.pendingRefreshOutputCount > 0,
+ `no pendingRefreshOutputCount for denom ${coin.denomPubHash} age ${coin.maxAge}`,
);
car.pendingRefreshOutputCount--;
car.freshCoinCount++;
@@ -1559,9 +1575,22 @@ async function applyRefreshToOldCoins(
coin.denomPubHash,
coin.maxAge,
]);
- checkDbInvariant(!!coinAv);
- checkDbInvariant(coinAv.freshCoinCount > 0);
+ checkDbInvariant(
+ !!coinAv,
+ `no denom info for ${coin.denomPubHash} age ${coin.maxAge}`,
+ );
+ checkDbInvariant(
+ coinAv.freshCoinCount > 0,
+ `no fresh coins for ${coin.denomPubHash}`,
+ );
coinAv.freshCoinCount--;
+ if (coin.visible) {
+ if (!coinAv.visibleCoinCount) {
+ logger.error("coin availability inconsistent");
+ } else {
+ coinAv.visibleCoinCount--;
+ }
+ }
await tx.coinAvailability.put(coinAv);
break;
}
@@ -1770,7 +1799,7 @@ export async function forceRefresh(
],
},
async (tx) => {
- let coinPubs: CoinRefreshRequest[] = [];
+ const coinPubs: CoinRefreshRequest[] = [];
for (const c of req.refreshCoinSpecs) {
const coin = await tx.coins.get(c.coinPub);
if (!coin) {
@@ -1782,7 +1811,7 @@ export async function forceRefresh(
coin.exchangeBaseUrl,
coin.denomPubHash,
);
- checkDbInvariant(!!denom);
+ checkDbInvariant(!!denom, `no denom hash: ${coin.denomPubHash}`);
coinPubs.push({
coinPub: c.coinPub,
amount: c.amount ?? denom.value,
@@ -1818,66 +1847,38 @@ export async function waitRefreshFinal(
const ctx = new RefreshTransactionContext(wex, refreshGroupId);
wex.taskScheduler.startShepherdTask(ctx.taskId);
- // FIXME: Clean up using the new JS "using" / Symbol.dispose syntax.
- const refreshNotifFlag = new AsyncFlag();
- // Raise purchaseNotifFlag whenever we get a notification
- // about our refresh.
- const cancelNotif = wex.ws.addNotificationListener((notif) => {
- if (
- notif.type === NotificationType.TransactionStateTransition &&
- notif.transactionId === ctx.transactionId
- ) {
- refreshNotifFlag.raise();
- }
- });
- const unregisterOnCancelled = wex.cancellationToken.onCancelled(() => {
- cancelNotif();
- refreshNotifFlag.raise();
+ await genericWaitForState(wex, {
+ async checkState(): Promise<boolean> {
+ // Check if refresh is final
+ const res = await ctx.wex.db.runReadOnlyTx(
+ { storeNames: ["refreshGroups"] },
+ async (tx) => {
+ return {
+ rg: await tx.refreshGroups.get(ctx.refreshGroupId),
+ };
+ },
+ );
+ const { rg } = res;
+ if (!rg) {
+ // Must've been deleted, we consider that final.
+ return true;
+ }
+ switch (rg.operationStatus) {
+ case RefreshOperationStatus.Failed:
+ case RefreshOperationStatus.Finished:
+ // Transaction is final
+ return true;
+ case RefreshOperationStatus.Pending:
+ case RefreshOperationStatus.Suspended:
+ break;
+ }
+ return false;
+ },
+ filterNotification(notif): boolean {
+ return (
+ notif.type === NotificationType.TransactionStateTransition &&
+ notif.transactionId === ctx.transactionId
+ );
+ },
});
-
- try {
- await internalWaitRefreshFinal(ctx, refreshNotifFlag);
- } catch (e) {
- unregisterOnCancelled();
- cancelNotif();
- }
-}
-
-async function internalWaitRefreshFinal(
- ctx: RefreshTransactionContext,
- flag: AsyncFlag,
-): Promise<void> {
- while (true) {
- if (ctx.wex.cancellationToken.isCancelled) {
- throw Error("cancelled");
- }
-
- // Check if refresh is final
- const res = await ctx.wex.db.runReadOnlyTx(
- { storeNames: ["refreshGroups", "operationRetries"] },
- async (tx) => {
- return {
- rg: await tx.refreshGroups.get(ctx.refreshGroupId),
- };
- },
- );
- const { rg } = res;
- if (!rg) {
- // Must've been deleted, we consider that final.
- return;
- }
- switch (rg.operationStatus) {
- case RefreshOperationStatus.Failed:
- case RefreshOperationStatus.Finished:
- // Transaction is final
- return;
- case RefreshOperationStatus.Pending:
- case RefreshOperationStatus.Suspended:
- break;
- }
-
- // Wait for the next transition
- await flag.wait();
- flag.reset();
- }
}
diff --git a/packages/taler-wallet-core/src/shepherd.ts b/packages/taler-wallet-core/src/shepherd.ts
index 3b160d97f..470f45aff 100644
--- a/packages/taler-wallet-core/src/shepherd.ts
+++ b/packages/taler-wallet-core/src/shepherd.ts
@@ -50,12 +50,13 @@ import {
parseTaskIdentifier,
} from "./common.js";
import {
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
OperationRetryRecord,
WalletDbAllStoresReadOnlyTransaction,
WalletDbReadOnlyTransaction,
timestampAbsoluteFromDb,
+ timestampPreciseToDb,
} from "./db.js";
import {
computeDepositTransactionStatus,
@@ -113,6 +114,8 @@ const logger = new Logger("shepherd.ts");
*/
interface ShepherdInfo {
cts: CancellationToken.Source;
+ latch?: Promise<void>;
+ stopped: boolean;
}
/**
@@ -256,29 +259,36 @@ export class TaskSchedulerImpl implements TaskScheduler {
async reload(): Promise<void> {
await this.ensureRunning();
const tasksIds = [...this.sheps.keys()];
- logger.info(`reloading sheperd with ${tasksIds.length} tasks`);
+ logger.info(`reloading shepherd with ${tasksIds.length} tasks`);
for (const taskId of tasksIds) {
- this.stopShepherdTask(taskId);
+ await this.stopShepherdTask(taskId);
}
for (const taskId of tasksIds) {
this.startShepherdTask(taskId);
}
}
-
private async internalStartShepherdTask(taskId: TaskIdStr): Promise<void> {
logger.trace(`Starting to shepherd task ${taskId}`);
const oldShep = this.sheps.get(taskId);
if (oldShep) {
- logger.trace(`Already have a shepherd for ${taskId}`);
- return;
+ if (!oldShep.stopped) {
+ logger.trace(`Already have a shepherd for ${taskId}`);
+ return;
+ }
+ logger.trace(
+ `Waiting old task to complete the loop in cancel mode ${taskId}`,
+ );
+ await oldShep.latch;
}
logger.trace(`Creating new shepherd for ${taskId}`);
const newShep: ShepherdInfo = {
cts: CancellationToken.create(),
+ stopped: false,
};
this.sheps.set(taskId, newShep);
try {
- await this.internalShepherdTask(taskId, newShep);
+ newShep.latch = this.internalShepherdTask(taskId, newShep);
+ await newShep.latch;
} finally {
logger.trace(`Done shepherding ${taskId}`);
this.sheps.delete(taskId);
@@ -291,8 +301,8 @@ export class TaskSchedulerImpl implements TaskScheduler {
const oldShep = this.sheps.get(taskId);
if (oldShep) {
logger.trace(`Cancelling old shepherd for ${taskId}`);
- oldShep.cts.cancel();
- this.sheps.delete(taskId);
+ oldShep.cts.cancel(`stopping task ${taskId}`);
+ oldShep.stopped = true;
this.iterCond.trigger();
}
}
@@ -306,6 +316,7 @@ export class TaskSchedulerImpl implements TaskScheduler {
const maybeNotification = await this.ws.db.runAllStoresReadWriteTx(
{},
async (tx) => {
+ logger.trace(`storing task [reset] for ${taskId}`);
await tx.operationRetries.delete(taskId);
return taskToRetryNotification(this.ws, tx, taskId, undefined);
},
@@ -325,7 +336,13 @@ export class TaskSchedulerImpl implements TaskScheduler {
try {
await info.cts.token.racePromise(this.ws.timerGroup.resolveAfter(delay));
} catch (e) {
- logger.info(`waiting for ${taskId} interrupted`);
+ if (e instanceof CancellationToken.CancellationError) {
+ logger.info(
+ `waiting for ${taskId} interrupted: ${e.message} ${j2s(e.reason)}`,
+ );
+ } else {
+ logger.info(`waiting for ${taskId} interrupted: ${e}`);
+ }
}
}
@@ -363,13 +380,14 @@ export class TaskSchedulerImpl implements TaskScheduler {
try {
res = await callOperationHandlerForTaskId(wex, taskId);
} catch (e) {
+ logger.trace(`Shepherd error ${taskId} saving response ${e}`);
res = {
type: TaskRunResultType.Error,
errorDetail: getErrorDetailFromException(e),
};
}
if (info.cts.token.isCancelled) {
- logger.trace("task cancelled, not processing result");
+ logger.trace(`task ${taskId} cancelled, not processing result`);
return;
}
if (this.ws.stopped) {
@@ -382,7 +400,9 @@ export class TaskSchedulerImpl implements TaskScheduler {
});
switch (res.type) {
case TaskRunResultType.Error: {
- logger.trace(`Shepherd for ${taskId} got error result.`);
+ logger.trace(
+ `Shepherd for ${taskId} got error result: ${j2s(res.errorDetail)}`,
+ );
const retryRecord = await storePendingTaskError(
this.ws,
taskId,
@@ -412,8 +432,13 @@ export class TaskSchedulerImpl implements TaskScheduler {
}
case TaskRunResultType.ScheduleLater: {
logger.trace(`Shepherd for ${taskId} got schedule-later result.`);
- await storeTaskProgress(this.ws, taskId);
- const delay = AbsoluteTime.remaining(res.runAt);
+ const retryRecord = await storePendingTaskPending(
+ this.ws,
+ taskId,
+ res.runAt,
+ );
+ const t = timestampAbsoluteFromDb(retryRecord.retryInfo.nextRetry);
+ const delay = AbsoluteTime.remaining(t);
logger.trace(`Waiting for ${delay.d_ms} ms`);
await this.wait(taskId, info, delay);
break;
@@ -451,7 +476,7 @@ async function storePendingTaskError(
pendingTaskId: string,
e: TalerErrorDetail,
): Promise<OperationRetryRecord> {
- logger.info(`storing pending task error for ${pendingTaskId}`);
+ logger.trace(`storing task [pending] with ERROR for ${pendingTaskId}`);
const res = await ws.db.runAllStoresReadWriteTx({}, async (tx) => {
let retryRecord = await tx.operationRetries.get(pendingTaskId);
if (!retryRecord) {
@@ -483,6 +508,7 @@ async function storeTaskProgress(
ws: InternalWalletState,
pendingTaskId: string,
): Promise<void> {
+ logger.trace(`storing task [progress] for ${pendingTaskId}`);
await ws.db.runReadWriteTx(
{ storeNames: ["operationRetries"] },
async (tx) => {
@@ -494,7 +520,9 @@ async function storeTaskProgress(
async function storePendingTaskPending(
ws: InternalWalletState,
pendingTaskId: string,
+ schedTime?: AbsoluteTime,
): Promise<OperationRetryRecord> {
+ logger.trace(`storing task [pending] for ${pendingTaskId}`);
const res = await ws.db.runAllStoresReadWriteTx({}, async (tx) => {
let retryRecord = await tx.operationRetries.get(pendingTaskId);
let hadError = false;
@@ -510,6 +538,11 @@ async function storePendingTaskPending(
delete retryRecord.lastError;
retryRecord.retryInfo = DbRetryInfo.increment(retryRecord.retryInfo);
}
+ if (schedTime) {
+ retryRecord.retryInfo.nextRetry = timestampPreciseToDb(
+ AbsoluteTime.toPreciseTimestamp(schedTime),
+ );
+ }
await tx.operationRetries.put(retryRecord);
let notification: WalletNotification | undefined = undefined;
if (hadError) {
@@ -535,6 +568,7 @@ async function storePendingTaskFinished(
ws: InternalWalletState,
pendingTaskId: string,
): Promise<void> {
+ logger.trace(`storing task [finished] for ${pendingTaskId}`);
await ws.db.runReadWriteTx(
{ storeNames: ["operationRetries"] },
async (tx) => {
@@ -978,8 +1012,8 @@ export async function getActiveTaskIds(
},
async (tx) => {
const active = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
// Withdrawals
diff --git a/packages/taler-wallet-core/src/transactions.ts b/packages/taler-wallet-core/src/transactions.ts
index f6216d641..7782d09ba 100644
--- a/packages/taler-wallet-core/src/transactions.ts
+++ b/packages/taler-wallet-core/src/transactions.ts
@@ -62,8 +62,8 @@ import {
DenomLossEventRecord,
DepositElementStatus,
DepositGroupRecord,
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
OperationRetryRecord,
PeerPullCreditRecord,
PeerPullDebitRecordStatus,
@@ -243,21 +243,29 @@ export async function getTransactionById(
const opId = TaskIdentifiers.forWithdrawal(withdrawalGroupRecord);
const ort = await tx.operationRetries.get(opId);
+ const exchangeDetails =
+ withdrawalGroupRecord.exchangeBaseUrl === undefined
+ ? undefined
+ : 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,
+ checkDbInvariant(
+ exchangeDetails !== undefined,
+ "manual withdrawal without exchange",
);
- if (!exchangeDetails) throw Error("not exchange details");
-
return buildTransactionForManualWithdraw(
withdrawalGroupRecord,
exchangeDetails,
@@ -402,7 +410,10 @@ export async function getTransactionById(
const debit = await tx.peerPushDebit.get(parsedTx.pursePub);
if (!debit) throw Error("not found");
const ct = await tx.contractTerms.get(debit.contractTermsHash);
- checkDbInvariant(!!ct);
+ checkDbInvariant(
+ !!ct,
+ `no contract terms for p2p push ${parsedTx.pursePub}`,
+ );
return buildTransactionForPushPaymentDebit(
debit,
ct.contractTermsRaw,
@@ -426,7 +437,10 @@ export async function getTransactionById(
const pushInc = await tx.peerPushCredit.get(peerPushCreditId);
if (!pushInc) throw Error("not found");
const ct = await tx.contractTerms.get(pushInc.contractTermsHash);
- checkDbInvariant(!!ct);
+ checkDbInvariant(
+ !!ct,
+ `no contract terms for p2p push ${peerPushCreditId}`,
+ );
let wg: WithdrawalGroupRecord | undefined = undefined;
let wgOrt: OperationRetryRecord | undefined = undefined;
@@ -438,7 +452,7 @@ export async function getTransactionById(
}
}
const pushIncOpId = TaskIdentifiers.forPeerPushCredit(pushInc);
- let pushIncOrt = await tx.operationRetries.get(pushIncOpId);
+ const pushIncOrt = await tx.operationRetries.get(pushIncOpId);
return buildTransactionForPeerPushCredit(
pushInc,
@@ -466,7 +480,7 @@ export async function getTransactionById(
const pushInc = await tx.peerPullCredit.get(pursePub);
if (!pushInc) throw Error("not found");
const ct = await tx.contractTerms.get(pushInc.contractTermsHash);
- checkDbInvariant(!!ct);
+ checkDbInvariant(!!ct, `no contract terms for p2p push ${pursePub}`);
let wg: WithdrawalGroupRecord | undefined = undefined;
let wgOrt: OperationRetryRecord | undefined = undefined;
@@ -589,6 +603,9 @@ function buildTransactionForPeerPullCredit(
);
});
const txState = computePeerPullCreditTransactionState(pullCredit);
+ checkDbInvariant(wsr.instructedAmount !== undefined, "wg uninitialized");
+ checkDbInvariant(wsr.denomsSel !== undefined, "wg uninitialized");
+ checkDbInvariant(wsr.exchangeBaseUrl !== undefined, "wg uninitialized");
return {
type: TransactionType.PeerPullCredit,
txState,
@@ -654,13 +671,16 @@ 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");
+ checkDbInvariant(wg.exchangeBaseUrl !== undefined, "wg uninitialized");
const txState = computePeerPushCreditTransactionState(pushInc);
return {
@@ -668,15 +688,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 +732,49 @@ function buildTransactionForPeerPushCredit(
}
function buildTransactionForBankIntegratedWithdraw(
- wgRecord: WithdrawalGroupRecord,
+ wg: WithdrawalGroupRecord,
+ exchangeDetails: ExchangeWireDetails | undefined,
ort?: OperationRetryRecord,
): TransactionWithdrawal {
- if (wgRecord.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated)
+ if (wg.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) {
throw Error("");
-
- const txState = computeWithdrawalTransactionStatus(wgRecord);
+ }
+ const instructedCurrency =
+ wg.instructedAmount === undefined
+ ? undefined
+ : Amounts.currencyOf(wg.instructedAmount);
+ const currency = wg.wgInfo.bankInfo.currency ?? instructedCurrency;
+ checkDbInvariant(currency !== undefined, "wg uninitialized (missing currency)");
+ const txState = computeWithdrawalTransactionStatus(wg);
+
+ const zero = Amounts.stringify(Amounts.zeroOfCurrency(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 +791,50 @@ 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");
+ checkDbInvariant(wg.exchangeBaseUrl !== 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 +1015,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,
@@ -1016,8 +1054,14 @@ async function buildTransactionForPurchase(
}));
const timestamp = purchaseRecord.timestampAccept;
- checkDbInvariant(!!timestamp);
- checkDbInvariant(!!purchaseRecord.payInfo);
+ checkDbInvariant(
+ !!timestamp,
+ `purchase ${purchaseRecord.orderId} without accepted time`,
+ );
+ checkDbInvariant(
+ !!purchaseRecord.payInfo,
+ `purchase ${purchaseRecord.orderId} without payinfo`,
+ );
const txState = computePayMerchantTransactionState(purchaseRecord);
return {
@@ -1071,24 +1115,30 @@ export async function getWithdrawalTransactionByUri(
if (!withdrawalGroupRecord) {
return undefined;
}
+ if (withdrawalGroupRecord.exchangeBaseUrl === undefined) {
+ // prepared and unconfirmed withdrawals are hidden
+ return undefined;
+ }
const opId = TaskIdentifiers.forWithdrawal(withdrawalGroupRecord);
const ort = await tx.operationRetries.get(opId);
+ 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,
@@ -1155,7 +1205,7 @@ export async function getTransactions(
return;
}
const ct = await tx.contractTerms.get(pi.contractTermsHash);
- checkDbInvariant(!!ct);
+ checkDbInvariant(!!ct, `no contract terms for p2p push ${pi.pursePub}`);
transactions.push(
buildTransactionForPushPaymentDebit(pi, ct.contractTermsRaw),
);
@@ -1229,9 +1279,9 @@ export async function getTransactions(
}
}
const pushIncOpId = TaskIdentifiers.forPeerPushCredit(pi);
- let pushIncOrt = await tx.operationRetries.get(pushIncOpId);
+ const pushIncOrt = await tx.operationRetries.get(pushIncOpId);
- checkDbInvariant(!!ct);
+ checkDbInvariant(!!ct, `no contract terms for p2p push ${pi.pursePub}`);
transactions.push(
buildTransactionForPeerPushCredit(
pi,
@@ -1263,9 +1313,9 @@ export async function getTransactions(
}
}
const pushIncOpId = TaskIdentifiers.forPeerPullPaymentInitiation(pi);
- let pushIncOrt = await tx.operationRetries.get(pushIncOpId);
+ const pushIncOrt = await tx.operationRetries.get(pushIncOpId);
- checkDbInvariant(!!ct);
+ checkDbInvariant(!!ct, `no contract terms for p2p push ${pi.pursePub}`);
transactions.push(
buildTransactionForPeerPullCredit(
pi,
@@ -1331,6 +1381,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 +1417,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 +1446,6 @@ export async function getTransactions(
// FIXME: report somehow
return;
}
-
transactions.push(
buildTransactionForManualWithdraw(wsr, exchangeDetails, ort),
);
@@ -1475,7 +1546,7 @@ export async function getTransactions(
);
transactions.push(
- await buildTransactionForPurchase(
+ buildTransactionForPurchase(
purchase,
contractData,
refunds,
@@ -1726,7 +1797,18 @@ 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);
+ }
+}
+
+/**
+ * Reset the task retry counter for all tasks.
+ */
+export async function retryAll(wex: WalletExecutionContext): Promise<void> {
+ await wex.taskScheduler.ensureRunning();
+ const tasks = wex.taskScheduler.getActiveTasks();
+ for (const task of tasks) {
+ await wex.taskScheduler.resetTaskRetries(task);
}
}
@@ -1886,8 +1968,8 @@ async function iterRecordsForWithdrawal(
let withdrawalGroupRecords: WithdrawalGroupRecord[];
if (filter.onlyState === "nonfinal") {
const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
withdrawalGroupRecords =
await tx.withdrawalGroups.indexes.byStatus.getAll(keyRange);
@@ -1908,8 +1990,8 @@ async function iterRecordsForDeposit(
let dgs: DepositGroupRecord[];
if (filter.onlyState === "nonfinal") {
const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
dgs = await tx.depositGroups.indexes.byStatus.getAll(keyRange);
} else {
@@ -1929,8 +2011,8 @@ async function iterRecordsForDenomLoss(
let dgs: DenomLossEventRecord[];
if (filter.onlyState === "nonfinal") {
const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
dgs = await tx.denomLossEvents.indexes.byStatus.getAll(keyRange);
} else {
@@ -1949,8 +2031,8 @@ async function iterRecordsForRefund(
): Promise<void> {
if (filter.onlyState === "nonfinal") {
const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
await tx.refundGroups.indexes.byStatus.iter(keyRange).forEachAsync(f);
} else {
@@ -1965,8 +2047,8 @@ async function iterRecordsForPurchase(
): Promise<void> {
if (filter.onlyState === "nonfinal") {
const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
await tx.purchases.indexes.byStatus.iter(keyRange).forEachAsync(f);
} else {
@@ -1981,8 +2063,8 @@ async function iterRecordsForPeerPullCredit(
): Promise<void> {
if (filter.onlyState === "nonfinal") {
const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
await tx.peerPullCredit.indexes.byStatus.iter(keyRange).forEachAsync(f);
} else {
@@ -1997,8 +2079,8 @@ async function iterRecordsForPeerPullDebit(
): Promise<void> {
if (filter.onlyState === "nonfinal") {
const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
await tx.peerPullDebit.indexes.byStatus.iter(keyRange).forEachAsync(f);
} else {
@@ -2013,8 +2095,8 @@ async function iterRecordsForPeerPushDebit(
): Promise<void> {
if (filter.onlyState === "nonfinal") {
const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
await tx.peerPushDebit.indexes.byStatus.iter(keyRange).forEachAsync(f);
} else {
@@ -2029,8 +2111,8 @@ async function iterRecordsForPeerPushCredit(
): Promise<void> {
if (filter.onlyState === "nonfinal") {
const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
await tx.peerPushCredit.indexes.byStatus.iter(keyRange).forEachAsync(f);
} else {
diff --git a/packages/taler-wallet-core/src/versions.ts b/packages/taler-wallet-core/src/versions.ts
index d33a23cdd..8b4b24351 100644
--- a/packages/taler-wallet-core/src/versions.ts
+++ b/packages/taler-wallet-core/src/versions.ts
@@ -29,13 +29,6 @@ export const WALLET_EXCHANGE_PROTOCOL_VERSION = "17:0:0";
export const WALLET_MERCHANT_PROTOCOL_VERSION = "5:0:1";
/**
- * Protocol version spoken with the bank (bank integration API).
- *
- * Uses libtool's current:revision:age versioning.
- */
-export const WALLET_BANK_INTEGRATION_PROTOCOL_VERSION = "1:0:0";
-
-/**
* Protocol version spoken with the bank (corebank API).
*
* Uses libtool's current:revision:age versioning.
@@ -52,7 +45,7 @@ export const WALLET_BANK_CONVERSION_API_PROTOCOL_VERSION = "2:0:0";
/**
* Libtool version of the wallet-core API.
*/
-export const WALLET_CORE_API_PROTOCOL_VERSION = "5:0:0";
+export const WALLET_CORE_API_PROTOCOL_VERSION = "7:0:0";
/**
* Libtool rules:
diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts
index ed882708c..aa88331ea 100644
--- 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,
@@ -164,6 +167,7 @@ export enum WalletApiOperation {
WithdrawTestBalance = "withdrawTestBalance",
PreparePayForUri = "preparePayForUri",
SharePayment = "sharePayment",
+ CheckPayForTemplate = "checkPayForTemplate",
PreparePayForTemplate = "preparePayForTemplate",
GetContractTermsDetails = "getContractTermsDetails",
RunIntegrationTest = "runIntegrationTest",
@@ -260,6 +264,7 @@ export enum WalletApiOperation {
RemoveGlobalCurrencyAuditor = "removeGlobalCurrencyAuditor",
ListAssociatedRefreshes = "listAssociatedRefreshes",
Shutdown = "shutdown",
+ HintNetworkAvailability = "hintNetworkAvailability",
CanonicalizeBaseUrl = "canonicalizeBaseUrl",
TestingWaitTransactionsFinal = "testingWaitTransactionsFinal",
TestingWaitRefreshesFinal = "testingWaitRefreshesFinal",
@@ -270,6 +275,8 @@ export enum WalletApiOperation {
TestingListTaskForTransaction = "testingListTasksForTransaction",
TestingGetDenomStats = "testingGetDenomStats",
TestingPing = "testingPing",
+ TestingGetReserveHistory = "testingGetReserveHistory",
+ TestingResetAllRetries = "testingResetAllRetries",
}
// 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,22 @@ export type TestingPingOp = {
response: EmptyObject;
};
+export type TestingGetReserveHistoryOp = {
+ op: WalletApiOperation.TestingGetReserveHistory;
+ request: EmptyObject;
+ response: any;
+};
+
+/**
+ * Reset all task/transaction retries,
+ * resulting in immediate re-try of all operations.
+ */
+export type TestingResetAllRetriesOp = {
+ op: WalletApiOperation.TestingResetAllRetries;
+ request: EmptyObject;
+ response: EmptyObject;
+};
+
/**
* Get stats about an exchange denomination.
*/
@@ -1222,6 +1257,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 +1365,9 @@ export type WalletOperations = {
[WalletApiOperation.PrepareBankIntegratedWithdrawal]: PrepareBankIntegratedWithdrawalOp;
[WalletApiOperation.ConfirmWithdrawal]: ConfirmWithdrawalOp;
[WalletApiOperation.CanonicalizeBaseUrl]: CanonicalizeBaseUrlOp;
+ [WalletApiOperation.TestingGetReserveHistory]: TestingGetReserveHistoryOp;
+ [WalletApiOperation.TestingResetAllRetries]: TestingResetAllRetriesOp;
+ [WalletApiOperation.HintNetworkAvailability]: HintNetworkAvailabilityOp;
};
export type WalletCoreRequestType<
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index ea47ffad7..7a69fcb21 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -56,6 +56,7 @@ import {
PrepareWithdrawExchangeResponse,
RecoverStoredBackupRequest,
StoredBackupList,
+ TalerBankIntegrationHttpClient,
TalerError,
TalerErrorCode,
TalerProtocolTimestamp,
@@ -84,6 +85,7 @@ import {
codecForAny,
codecForApplyDevExperiment,
codecForCanonicalizeBaseUrlRequest,
+ codecForCheckPayTemplateRequest,
codecForCheckPeerPullPaymentRequest,
codecForCheckPeerPushDebitRequest,
codecForConfirmPayRequest,
@@ -106,6 +108,7 @@ import {
codecForGetExchangeTosRequest,
codecForGetWithdrawalDetailsForAmountRequest,
codecForGetWithdrawalDetailsForUri,
+ codecForHintNetworkAvailabilityRequest,
codecForImportDbRequest,
codecForInitRequest,
codecForInitiatePeerPullPaymentRequest,
@@ -134,6 +137,7 @@ import {
codecForSuspendTransaction,
codecForTestPayArgs,
codecForTestingGetDenomStatsRequest,
+ codecForTestingGetReserveHistoryRequest,
codecForTestingListTasksForTransactionRequest,
codecForTestingSetTimetravelRequest,
codecForTransactionByIdRequest,
@@ -154,7 +158,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 +231,7 @@ import {
observeTalerCrypto,
} from "./observable-wrappers.js";
import {
+ checkPayForTemplate,
confirmPay,
getContractTermsDetails,
preparePayForTemplate,
@@ -259,6 +267,7 @@ import {
TaskScheduler,
TaskSchedulerImpl,
convertTaskToTransactionId,
+ getActiveTaskIds,
listTaskForTransactionId,
} from "./shepherd.js";
import {
@@ -281,12 +290,12 @@ import {
getWithdrawalTransactionByUri,
parseTransactionIdentifier,
resumeTransaction,
+ retryAll,
retryTransaction,
suspendTransaction,
} from "./transactions.js";
import {
WALLET_BANK_CONVERSION_API_PROTOCOL_VERSION,
- WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
WALLET_COREBANK_API_PROTOCOL_VERSION,
WALLET_CORE_API_PROTOCOL_VERSION,
WALLET_EXCHANGE_PROTOCOL_VERSION,
@@ -470,7 +479,10 @@ async function setCoinSuspended(
c.denomPubHash,
c.maxAge,
]);
- checkDbInvariant(!!coinAvailability);
+ checkDbInvariant(
+ !!coinAvailability,
+ `no denom info for ${c.denomPubHash} age ${c.maxAge}`,
+ );
if (suspended) {
if (c.status !== CoinStatus.Fresh) {
return;
@@ -656,9 +668,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) {
@@ -718,12 +727,11 @@ async function dispatchRequestInternal(
case WalletApiOperation.InitWallet: {
const req = codecForInitRequest().decode(payload);
- logger.info(`init request: ${j2s(req)}`);
-
- if (wex.ws.initCalled) {
- logger.info("initializing wallet (repeat initialization)");
- } else {
- logger.info("initializing wallet (first initialization)");
+ if (logger.shouldLogTrace()) {
+ const initType = wex.ws.initCalled
+ ? "repeat initialization"
+ : "first initialization";
+ logger.trace(`init request (${initType}): ${j2s(req)}`);
}
// Write to the DB to make sure that we're failing early in
@@ -741,7 +749,6 @@ async function dispatchRequestInternal(
innerError: getErrorDetailFromException(e),
});
}
-
wex.ws.initWithConfig(applyRunConfigDefaults(req.config));
if (wex.ws.config.testing.skipDefaults) {
@@ -754,8 +761,11 @@ async function dispatchRequestInternal(
versionInfo: getVersion(wex),
};
- // After initialization, task loop should run.
- await wex.taskScheduler.ensureRunning();
+ if (req.config?.lazyTaskLoop) {
+ logger.trace("lazily starting task loop");
+ } else {
+ await wex.taskScheduler.ensureRunning();
+ }
wex.ws.initCalled = true;
return resp;
@@ -818,9 +828,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 +913,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);
@@ -968,21 +1003,17 @@ async function dispatchRequestInternal(
talerWithdrawUri: req.talerWithdrawUri,
forcedDenomSel: req.forcedDenomSel,
restrictAge: req.restrictAge,
+ amount: req.amount,
});
}
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,
- });
+ return prepareBankIntegratedWithdrawal(wex, req);
}
case WalletApiOperation.GetExchangeTos: {
const req = codecForGetExchangeTosRequest().decode(payload);
@@ -1020,6 +1051,10 @@ async function dispatchRequestInternal(
const req = codecForPrepareWithdrawExchangeRequest().decode(payload);
return handlePrepareWithdrawExchange(wex, req);
}
+ case WalletApiOperation.CheckPayForTemplate: {
+ const req = codecForCheckPayTemplateRequest().decode(payload);
+ return await checkPayForTemplate(wex, req);
+ }
case WalletApiOperation.PreparePayForUri: {
const req = codecForPreparePayRequest().decode(payload);
return await preparePayForUri(wex, req.talerPayUri);
@@ -1055,7 +1090,7 @@ async function dispatchRequestInternal(
return {};
}
case WalletApiOperation.GetActiveTasks: {
- const allTasksId = wex.taskScheduler.getActiveTasks();
+ const allTasksId = (await getActiveTaskIds(wex.ws)).taskIds;
const tasksInfo = await Promise.all(
allTasksId.map(async (id) => {
@@ -1204,10 +1239,16 @@ async function dispatchRequestInternal(
await loadBackupRecovery(wex, req);
return {};
}
- // case WalletApiOperation.GetPlanForOperation: {
- // const req = codecForGetPlanForOperationRequest().decode(payload);
- // return await getPlanForOperation(ws, req);
- // }
+ case WalletApiOperation.HintNetworkAvailability: {
+ const req = codecForHintNetworkAvailabilityRequest().decode(payload);
+ if (req.isNetworkAvailable) {
+ await retryAll(wex);
+ } else {
+ // We're not doing anything right now, but we could stop showing
+ // certain errors!
+ }
+ return {};
+ }
case WalletApiOperation.ConvertDepositAmount: {
const req = codecForConvertAmountRequest.decode(payload);
return await convertDepositAmount(wex, req);
@@ -1358,7 +1399,10 @@ async function dispatchRequestInternal(
return;
}
wex.ws.exchangeCache.clear();
- checkDbInvariant(!!existingRec.id);
+ checkDbInvariant(
+ !!existingRec.id,
+ `no global exchange for ${j2s(key)}`,
+ );
await tx.globalCurrencyExchanges.delete(existingRec.id);
},
);
@@ -1391,6 +1435,9 @@ async function dispatchRequestInternal(
await waitTasksDone(wex);
return {};
}
+ case WalletApiOperation.TestingResetAllRetries:
+ await retryAll(wex);
+ return {};
case WalletApiOperation.RemoveGlobalCurrencyAuditor: {
const req = codecForRemoveGlobalCurrencyAuditorRequest().decode(payload);
await wex.db.runReadWriteTx(
@@ -1404,7 +1451,10 @@ async function dispatchRequestInternal(
if (!existingRec) {
return;
}
- checkDbInvariant(!!existingRec.id);
+ checkDbInvariant(
+ !!existingRec.id,
+ `no global currency for ${j2s(key)}`,
+ );
await tx.globalCurrencyAuditors.delete(existingRec.id);
wex.ws.exchangeCache.clear();
},
@@ -1539,9 +1589,9 @@ export function getVersion(wex: WalletExecutionContext): WalletCoreVersion {
exchange: WALLET_EXCHANGE_PROTOCOL_VERSION,
merchant: WALLET_MERCHANT_PROTOCOL_VERSION,
bankConversionApiRange: WALLET_BANK_CONVERSION_API_PROTOCOL_VERSION,
- bankIntegrationApiRange: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
+ bankIntegrationApiRange: TalerBankIntegrationHttpClient.PROTOCOL_VERSION,
corebankApiRange: WALLET_COREBANK_API_PROTOCOL_VERSION,
- bank: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
+ bank: TalerBankIntegrationHttpClient.PROTOCOL_VERSION,
devMode: wex.ws.config.testing.devModeActive,
};
return result;
@@ -1595,6 +1645,14 @@ async function handleCoreApiRequest(
id: string,
payload: unknown,
): Promise<CoreApiResponse> {
+ if (operation !== WalletApiOperation.InitWallet) {
+ if (!ws.initCalled) {
+ throw Error("init must be called first");
+ }
+ }
+
+ await ws.ensureWalletDbOpen();
+
let wex: WalletExecutionContext;
let oc: ObservabilityContext;
@@ -1683,6 +1741,7 @@ export function applyRunConfigDefaults(
skipDefaults: wcp?.testing?.skipDefaults ?? false,
emitObservabilityEvents: wcp?.testing?.emitObservabilityEvents ?? false,
},
+ lazyTaskLoop: wcp?.lazyTaskLoop ?? false,
};
}
@@ -1793,7 +1852,7 @@ class WalletDbTriggerSpec implements TriggerSpec {
if (info.mode !== "readwrite") {
return;
}
- logger.info(
+ logger.trace(
`in after commit callback for readwrite, modified ${j2s([
...info.modifiedStores,
])}`,
@@ -1885,8 +1944,6 @@ export class InternalWalletState {
initWithConfig(newConfig: WalletRunConfig): void {
this._config = newConfig;
- logger.info(`setting new config to ${j2s(newConfig)}`);
-
this._http = this.httpFactory(newConfig);
if (this.config.testing.devModeActive) {
diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts
index 81e104014..d7a1c6d24 100644
--- 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,
@@ -43,6 +44,8 @@ import {
Duration,
EddsaPrivateKeyString,
ExchangeBatchWithdrawRequest,
+ ExchangeListItem,
+ ExchangeTosStatus,
ExchangeUpdateStatus,
ExchangeWireAccount,
ExchangeWithdrawBatchResponse,
@@ -113,8 +116,10 @@ import {
TransitionResult,
TransitionResultType,
constructTaskIdentifier,
+ genericWaitForState,
makeCoinAvailable,
makeCoinsVisible,
+ requireExchangeTosAcceptedOrThrow,
} from "./common.js";
import { EddsaKeypair } from "./crypto/cryptoImplementation.js";
import {
@@ -148,6 +153,7 @@ import {
getExchangePaytoUri,
getExchangeWireDetailsInTx,
listExchanges,
+ lookupExchangeByUri,
markExchangeUsed,
} from "./exchanges.js";
import { DbAccess } from "./query.js";
@@ -158,10 +164,7 @@ import {
notifyTransition,
parseTransactionIdentifier,
} from "./transactions.js";
-import {
- WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
- WALLET_EXCHANGE_PROTOCOL_VERSION,
-} from "./versions.js";
+import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "./versions.js";
import { WalletExecutionContext, getDenomInfo } from "./wallet.js";
/**
@@ -194,6 +197,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 +236,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,
@@ -325,9 +345,11 @@ export class WithdrawTransactionContext implements TransactionContext {
"exchanges" as const,
"exchangeDetails" as const,
];
- let stores = opts.extraStores
+ const stores = opts.extraStores
? [...baseStores, ...opts.extraStores]
: baseStores;
+
+ let errorThrown: Error | undefined;
const transitionInfo = await this.wex.db.runReadWriteTx(
{ storeNames: stores },
async (tx) => {
@@ -340,7 +362,17 @@ export class WithdrawTransactionContext implements TransactionContext {
major: TransactionMajorState.None,
};
}
- const res = await f(wgRec, tx);
+ let res: TransitionResult<WithdrawalGroupRecord> | undefined;
+ try {
+ res = await f(wgRec, tx);
+ } catch (error) {
+ if (error instanceof Error) {
+ errorThrown = error;
+ }
+ return undefined;
+ }
+
+ // const res = await f(wgRec, tx);
switch (res.type) {
case TransitionResultType.Transition: {
await tx.withdrawalGroups.put(res.rec);
@@ -365,6 +397,9 @@ export class WithdrawTransactionContext implements TransactionContext {
}
},
);
+ if (errorThrown) {
+ throw errorThrown;
+ }
notifyTransition(this.wex, this.transactionId, transitionInfo);
return transitionInfo;
}
@@ -697,15 +732,35 @@ export function computeWithdrawalTransactionActions(
case WithdrawalGroupStatus.Done:
return [TransactionAction.Delete];
case WithdrawalGroupStatus.PendingRegisteringBank:
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case WithdrawalGroupStatus.PendingReady:
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case WithdrawalGroupStatus.PendingQueryingStatus:
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case WithdrawalGroupStatus.PendingWaitConfirmBank:
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case WithdrawalGroupStatus.AbortingBank:
- return [TransactionAction.Suspend, TransactionAction.Fail];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Fail,
+ ];
case WithdrawalGroupStatus.SuspendedAbortingBank:
return [TransactionAction.Resume, TransactionAction.Fail];
case WithdrawalGroupStatus.SuspendedQueryingStatus:
@@ -717,9 +772,17 @@ export function computeWithdrawalTransactionActions(
case WithdrawalGroupStatus.SuspendedReady:
return [TransactionAction.Resume, TransactionAction.Abort];
case WithdrawalGroupStatus.PendingAml:
- return [TransactionAction.Resume, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Resume,
+ TransactionAction.Abort,
+ ];
case WithdrawalGroupStatus.PendingKyc:
- return [TransactionAction.Resume, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Resume,
+ TransactionAction.Abort,
+ ];
case WithdrawalGroupStatus.SuspendedAml:
return [TransactionAction.Resume, TransactionAction.Abort];
case WithdrawalGroupStatus.SuspendedKyc:
@@ -824,7 +887,7 @@ export async function getBankWithdrawalInfo(
TalerErrorCode.WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE,
{
bankProtocolVersion: config.version,
- walletProtocolVersion: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
+ walletProtocolVersion: bankApi.PROTOCOL_VERSION,
},
"bank integration protocol version not compatible with wallet",
);
@@ -839,13 +902,48 @@ export async function getBankWithdrawalInfo(
}
const { body: status } = resp;
+ const maxAmount =
+ status.max_amount === undefined
+ ? undefined
+ : Amounts.parseOrThrow(status.max_amount);
+
+ let amount: AmountJson | undefined;
+ let editableAmount = false;
+ if (status.amount !== undefined) {
+ amount = Amounts.parseOrThrow(status.amount);
+ } else {
+ amount =
+ status.suggested_amount === undefined
+ ? undefined
+ : Amounts.parseOrThrow(status.suggested_amount);
+ editableAmount = true;
+ }
+
+ let wireFee: AmountJson | undefined;
+ if (status.card_fees) {
+ wireFee = Amounts.parseOrThrow(status.card_fees);
+ }
+
+ let exchange: string | undefined = undefined;
+ let editableExchange = false;
+ if (status.required_exchange !== undefined) {
+ exchange = status.required_exchange;
+ } else {
+ exchange = status.suggested_exchange;
+ editableExchange = true;
+ }
return {
operationId: uriResult.withdrawalOperationId,
apiBaseUrl: uriResult.bankIntegrationApiBaseUrl,
- amount: Amounts.parseOrThrow(status.amount),
+ currency: config.currency,
+ amount,
+ wireFee,
confirmTransferUrl: status.confirm_transfer_url,
senderWire: status.sender_wire,
- suggestedExchange: status.suggested_exchange,
+ exchange,
+ editableAmount,
+ editableExchange,
+ maxAmount,
wireTypes: status.wire_types,
status: status.status,
};
@@ -895,6 +993,15 @@ async function processPlanchetGenerate(
withdrawalGroup: WithdrawalGroupRecord,
coinIdx: number,
): Promise<void> {
+ checkDbInvariant(
+ withdrawalGroup.denomsSel !== undefined,
+ "can't process uninitialized exchange",
+ );
+ checkDbInvariant(
+ withdrawalGroup.exchangeBaseUrl !== undefined,
+ "can't get funding uri from uninitialized wg",
+ );
+ const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl;
let planchet = await wex.db.runReadOnlyTx(
{ storeNames: ["planchets"] },
async (tx) => {
@@ -932,15 +1039,10 @@ 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);
+ checkDbInvariant(!!denom, `no denom info for ${denomPubHash}`);
const r = await wex.cryptoApi.createPlanchet({
denomPub: denom.denomPub,
feeWithdraw: Amounts.parseOrThrow(denom.feeWithdraw),
@@ -1058,7 +1160,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],
]);
@@ -1103,6 +1205,11 @@ async function processPlanchetExchangeBatchRequest(
logger.info(
`processing planchet exchange batch request ${withdrawalGroup.withdrawalGroupId}, start=${args.coinStartIndex}, len=${args.batchSize}`,
);
+ checkDbInvariant(
+ withdrawalGroup.exchangeBaseUrl !== undefined,
+ "can't get funding uri from uninitialized wg",
+ );
+ const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl;
const batchReq: ExchangeBatchWithdrawRequest = { planchets: [] };
// Indices of coins that are included in the batch request
@@ -1117,7 +1224,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,
]);
@@ -1134,7 +1241,7 @@ async function processPlanchetExchangeBatchRequest(
const denom = await getDenomInfo(
wex,
tx,
- withdrawalGroup.exchangeBaseUrl,
+ exchangeBaseUrl,
planchet.denomPubHash,
);
@@ -1168,7 +1275,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,
]);
@@ -1237,11 +1344,17 @@ async function processPlanchetVerifyAndStoreCoin(
resp: ExchangeWithdrawResponse,
): Promise<void> {
const withdrawalGroup = wgContext.wgRecord;
+ checkDbInvariant(
+ withdrawalGroup.exchangeBaseUrl !== undefined,
+ "can't get funding uri from uninitialized wg",
+ );
+ const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl;
+
logger.trace(`checking and storing planchet idx=${coinIdx}`);
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,
]);
@@ -1255,7 +1368,7 @@ async function processPlanchetVerifyAndStoreCoin(
const denomInfo = await getDenomInfo(
wex,
tx,
- withdrawalGroup.exchangeBaseUrl,
+ exchangeBaseUrl,
planchet.denomPubHash,
);
if (!denomInfo) {
@@ -1264,7 +1377,7 @@ async function processPlanchetVerifyAndStoreCoin(
return {
planchet,
denomInfo,
- exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl,
+ exchangeBaseUrl: exchangeBaseUrl,
};
},
);
@@ -1285,7 +1398,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");
}
@@ -1304,7 +1417,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,
]);
@@ -1483,6 +1596,19 @@ async function processQueryReserve(
if (withdrawalGroup.status !== WithdrawalGroupStatus.PendingQueryingStatus) {
return TaskRunResult.backoff();
}
+ checkDbInvariant(
+ withdrawalGroup.exchangeBaseUrl !== undefined,
+ "can't get funding uri from uninitialized wg",
+ );
+ 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(
@@ -1518,15 +1644,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();
@@ -1672,6 +1835,14 @@ async function redenominateWithdrawal(
if (!wg) {
return;
}
+ checkDbInvariant(
+ wg.exchangeBaseUrl !== undefined,
+ "can't get funding uri from uninitialized wg",
+ );
+ checkDbInvariant(
+ wg.denomsSel !== undefined,
+ "can't process uninitialized exchange",
+ );
const currency = Amounts.currencyOf(wg.denomsSel.totalWithdrawCost);
const exchangeBaseUrl = wg.exchangeBaseUrl;
@@ -1688,13 +1859,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];
@@ -1706,7 +1877,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,
);
@@ -1807,8 +1978,16 @@ async function processWithdrawalGroupPendingReady(
const { withdrawalGroupId } = withdrawalGroup;
const ctx = new WithdrawTransactionContext(wex, withdrawalGroupId);
+ checkDbInvariant(
+ withdrawalGroup.denomsSel !== undefined,
+ "can't process uninitialized exchange",
+ );
+ checkDbInvariant(
+ withdrawalGroup.exchangeBaseUrl !== undefined,
+ "can't get funding uri from uninitialized wg",
+ );
const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl;
-
+ logger.trace(`updating exchange beofre processing wg`);
await fetchFreshExchange(wex, withdrawalGroup.exchangeBaseUrl);
if (withdrawalGroup.denomsSel.selectedDenoms.length === 0) {
@@ -1910,7 +2089,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(
@@ -1931,7 +2109,6 @@ async function processWithdrawalGroupPendingReady(
numActive++;
break;
case PlanchetStatus.WithdrawalDone:
- numDone++;
break;
}
if (x.lastError) {
@@ -2045,9 +2222,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();
@@ -2174,43 +2349,56 @@ 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);
logger.trace(`got bank info`);
- if (info.suggestedExchange) {
+ if (info.exchange) {
try {
// If the exchange entry doesn't exist yet,
// it'll be created as an ephemeral entry.
- await fetchFreshExchange(wex, info.suggestedExchange);
+ await fetchFreshExchange(wex, info.exchange);
} catch (e) {
// We still continued if it failed, as other exchanges might be available.
// We don't want to fail if the bank-suggested exchange is broken/offline.
logger.trace(
- `querying bank-suggested exchange (${info.suggestedExchange}) failed`,
+ `querying bank-suggested exchange (${info.exchange}) failed`,
);
}
}
- const currency = Amounts.currencyOf(info.amount);
+ const currency = info.currency;
- const listExchangesResp = await listExchanges(wex);
- const possibleExchanges = listExchangesResp.exchanges.filter((x) => {
- return (
- x.currency === currency &&
- (x.exchangeUpdateStatus === ExchangeUpdateStatus.Ready ||
- x.exchangeUpdateStatus === ExchangeUpdateStatus.ReadyUpdate)
- );
- });
+ let possibleExchanges: ExchangeListItem[];
+ if (!info.editableExchange && info.exchange !== undefined) {
+ const ex: ExchangeListItem = await lookupExchangeByUri(wex, {
+ exchangeBaseUrl: info.exchange,
+ });
+ possibleExchanges = [ex];
+ } else {
+ const listExchangesResp = await listExchanges(wex);
+
+ possibleExchanges = listExchangesResp.exchanges.filter((x) => {
+ return (
+ x.currency === currency &&
+ (x.exchangeUpdateStatus === ExchangeUpdateStatus.Ready ||
+ x.exchangeUpdateStatus === ExchangeUpdateStatus.ReadyUpdate)
+ );
+ });
+ }
return {
operationId: info.operationId,
confirmTransferUrl: info.confirmTransferUrl,
status: info.status,
- amount: Amounts.stringify(info.amount),
- defaultExchangeBaseUrl: info.suggestedExchange,
+ currency,
+ editableAmount: info.editableAmount,
+ editableExchange: info.editableExchange,
+ maxAmount: info.maxAmount ? Amounts.stringify(info.maxAmount) : undefined,
+ amount: info.amount ? Amounts.stringify(info.amount) : undefined,
+ defaultExchangeBaseUrl: info.exchange,
possibleExchanges,
+ wireFee: info.wireFee ? Amounts.stringify(info.wireFee) : undefined,
};
}
@@ -2222,7 +2410,7 @@ export function augmentPaytoUrisForWithdrawal(
return plainPaytoUris.map((x) =>
addPaytoQueryParams(x, {
amount: Amounts.stringify(instructedAmount),
- message: `Taler Withdrawal ${reservePub}`,
+ message: `Taler ${reservePub}`,
}),
);
}
@@ -2237,7 +2425,15 @@ export async function getFundingPaytoUris(
withdrawalGroupId: string,
): Promise<string[]> {
const withdrawalGroup = await tx.withdrawalGroups.get(withdrawalGroupId);
- checkDbInvariant(!!withdrawalGroup);
+ checkDbInvariant(!!withdrawalGroup, `no withdrawal for ${withdrawalGroupId}`);
+ checkDbInvariant(
+ withdrawalGroup.exchangeBaseUrl !== undefined,
+ "can't get funding uri from uninitialized wg",
+ );
+ checkDbInvariant(
+ withdrawalGroup.instructedAmount !== undefined,
+ "can't get funding uri from uninitialized wg",
+ );
const exchangeDetails = await getExchangeWireDetailsInTx(
tx,
withdrawalGroup.exchangeBaseUrl,
@@ -2306,6 +2502,7 @@ export function getBankAbortUrl(talerWithdrawUri: string): string {
async function registerReserveWithBank(
wex: WalletExecutionContext,
withdrawalGroupId: string,
+ isFlexibleAmount: boolean,
): Promise<void> {
const withdrawalGroup = await wex.db.runReadOnlyTx(
{ storeNames: ["withdrawalGroups"] },
@@ -2334,7 +2531,11 @@ async function registerReserveWithBank(
const reqBody = {
reserve_pub: withdrawalGroup.reservePub,
selected_exchange: bankInfo.exchangePaytoUri,
- };
+ } as any;
+ if (isFlexibleAmount) {
+ reqBody.amount = withdrawalGroup.instructedAmount;
+ }
+ logger.trace(`isFlexibleAmount: ${isFlexibleAmount}`);
logger.info(`registering reserve with bank: ${j2s(reqBody)}`);
const httpResp = await wex.http.fetch(bankStatusUrl, {
method: "POST",
@@ -2443,7 +2644,9 @@ async function processBankRegisterReserve(
// FIXME: Put confirm transfer URL in the DB!
- await registerReserveWithBank(wex, withdrawalGroupId);
+ const isFlexibleAmount = status.amount == null;
+
+ await registerReserveWithBank(wex, withdrawalGroupId, isFlexibleAmount);
return TaskRunResult.progress();
}
@@ -2480,6 +2683,7 @@ async function processReserveBankStatus(
uriResult.bankIntegrationApiBaseUrl,
);
bankStatusUrl.searchParams.set("long_poll_ms", "30000");
+ bankStatusUrl.searchParams.set("old_state", "selected");
logger.info(`long-polling for withdrawal operation at ${bankStatusUrl.href}`);
const statusResp = await wex.http.fetch(bankStatusUrl.href, {
@@ -2548,12 +2752,41 @@ 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;
- exchangeBaseUrl: string;
+ amount?: AmountJson;
+ exchangeBaseUrl: string | undefined;
forcedWithdrawalGroupId?: string;
forcedDenomSel?: ForcedDenomSel;
reserveKeyPair?: EddsaKeypair;
@@ -2567,9 +2800,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;
@@ -2592,39 +2824,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 && exchangeBaseUrl !== 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,
@@ -2636,7 +2858,10 @@ export async function internalPrepareCreateWithdrawalGroup(
wgInfo: args.wgInfo,
};
- await fetchFreshExchange(wex, exchangeBaseUrl);
+ if (exchangeBaseUrl !== undefined) {
+ await fetchFreshExchange(wex, exchangeBaseUrl);
+ }
+
const transactionId = constructTransactionIdentifier({
tag: TransactionType.Withdrawal,
withdrawalGroupId: withdrawalGroup.withdrawalGroupId,
@@ -2645,10 +2870,13 @@ export async function internalPrepareCreateWithdrawalGroup(
return {
withdrawalGroup,
transactionId,
- creationInfo: {
- canonExchange: exchangeBaseUrl,
- amount,
- },
+ creationInfo:
+ !amount || !exchangeBaseUrl
+ ? undefined
+ : {
+ amount,
+ canonExchange: exchangeBaseUrl,
+ },
};
}
@@ -2672,21 +2900,14 @@ 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,
);
if (existingWg) {
return {
withdrawalGroup: existingWg,
- exchangeNotif: undefined,
transitionInfo: undefined,
+ exchangeNotif: undefined,
};
}
await tx.withdrawalGroups.add(withdrawalGroup);
@@ -2695,7 +2916,28 @@ export async function internalPerformCreateWithdrawalGroup(
reservePriv: withdrawalGroup.reservePriv,
});
- const exchange = await tx.exchanges.get(withdrawalGroup.exchangeBaseUrl);
+ if (!prep.creationInfo) {
+ return {
+ withdrawalGroup,
+ transitionInfo: undefined,
+ exchangeNotif: undefined,
+ };
+ }
+ return internalPerformExchangeWasUsed(
+ wex,
+ tx,
+ prep.creationInfo.canonExchange,
+ withdrawalGroup,
+ );
+}
+
+export async function internalPerformExchangeWasUsed(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadWriteTransaction<["exchanges"]>,
+ canonExchange: string,
+ withdrawalGroup: WithdrawalGroupRecord,
+): Promise<PerformCreateWithdrawalGroupResult> {
+ const exchange = await tx.exchanges.get(canonExchange);
if (exchange) {
exchange.lastWithdrawal = timestampPreciseToDb(TalerPreciseTimestamp.now());
await tx.exchanges.put(exchange);
@@ -2711,11 +2953,7 @@ export async function internalPerformCreateWithdrawalGroup(
newTxState,
};
- const exchangeUsedRes = await markExchangeUsed(
- wex,
- tx,
- prep.withdrawalGroup.exchangeBaseUrl,
- );
+ const exchangeUsedRes = await markExchangeUsed(wex, tx, canonExchange);
const ctx = new WithdrawTransactionContext(
wex,
@@ -2743,8 +2981,8 @@ export async function internalCreateWithdrawalGroup(
wex: WalletExecutionContext,
args: {
reserveStatus: WithdrawalGroupStatus;
- amount: AmountJson;
- exchangeBaseUrl: string;
+ exchangeBaseUrl: string | undefined;
+ amount?: AmountJson;
forcedWithdrawalGroupId?: string;
forcedDenomSel?: ForcedDenomSel;
reserveKeyPair?: EddsaKeypair;
@@ -2789,9 +3027,6 @@ export async function prepareBankIntegratedWithdrawal(
wex: WalletExecutionContext,
req: {
talerWithdrawUri: string;
- selectedExchange: string;
- forcedDenomSel?: ForcedDenomSel;
- restrictAge?: number;
},
): Promise<PrepareBankIntegratedWithdrawalResponse> {
const existingWithdrawalGroup = await wex.db.runReadOnlyTx(
@@ -2804,59 +3039,42 @@ 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);
+ /**
+ * 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: undefined,
wgInfo: {
withdrawalType: WithdrawalRecordType.BankIntegrated,
- exchangeCreditAccounts: withdrawalAccountList,
bankInfo: {
- exchangePaytoUri,
talerWithdrawUri: req.talerWithdrawUri,
confirmUrl: withdrawInfo.confirmTransferUrl,
timestampBankConfirmed: undefined,
timestampReserveInfoPosted: undefined,
+ wireTypes: withdrawInfo.wireTypes,
+ currency: withdrawInfo.currency,
},
},
- restrictAge: req.restrictAge,
- forcedDenomSel: req.forcedDenomSel,
reserveStatus: WithdrawalGroupStatus.DialogProposed,
});
@@ -2868,14 +3086,18 @@ 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);
+ const selectedExchange = req.exchangeBaseUrl;
+ const instructedAmount = Amounts.parseOrThrow(req.amount);
+
if (parsedTx?.tag !== TransactionType.Withdrawal) {
throw Error("invalid withdrawal transaction ID");
}
@@ -2890,26 +3112,143 @@ export async function confirmWithdrawal(
throw Error("withdrawal group not found");
}
+ if (
+ withdrawalGroup.wgInfo.withdrawalType !==
+ WithdrawalRecordType.BankIntegrated
+ ) {
+ throw Error("not a bank integrated withdrawal");
+ }
+
+ const exchange = await fetchFreshExchange(wex, selectedExchange);
+ requireExchangeTosAcceptedOrThrow(exchange);
+
+ const talerWithdrawUri = withdrawalGroup.wgInfo.bankInfo.talerWithdrawUri;
+ const confirmUrl = withdrawalGroup.wgInfo.bankInfo.confirmUrl;
+
+ /**
+ * The only reason this could be undefined is because it is an old wallet
+ * database before adding the prepareWithdrawal feature
+ */
+ let bankWireTypes: string[];
+ let bankCurrency: string;
+ if (
+ withdrawalGroup.wgInfo.bankInfo.wireTypes === undefined ||
+ withdrawalGroup.wgInfo.bankInfo.currency === undefined
+ ) {
+ const withdrawInfo = await getBankWithdrawalInfo(
+ wex.http,
+ talerWithdrawUri,
+ );
+ bankWireTypes = withdrawInfo.wireTypes;
+ bankCurrency = withdrawInfo.currency;
+ } else {
+ bankWireTypes = withdrawalGroup.wgInfo.bankInfo.wireTypes;
+ bankCurrency = withdrawalGroup.wgInfo.bankInfo.currency;
+ }
+
+ const exchangePaytoUri = await getExchangePaytoUri(
+ wex,
+ selectedExchange,
+ bankWireTypes,
+ );
+
+ const withdrawalAccountList = await fetchWithdrawalAccountInfo(
+ wex,
+ {
+ exchange,
+ instructedAmount,
+ },
+ wex.cancellationToken,
+ );
+
const ctx = new WithdrawTransactionContext(
wex,
withdrawalGroup.withdrawalGroupId,
);
- ctx.transition({}, async (rec) => {
+ const initalDenoms = await getInitialDenomsSelection(
+ wex,
+ exchange.exchangeBaseUrl,
+ instructedAmount,
+ req.forcedDenomSel,
+ );
+
+ let pending = false;
+ await ctx.transition({}, async (rec) => {
if (!rec) {
return TransitionResult.stay();
}
switch (rec.status) {
+ case WithdrawalGroupStatus.PendingWaitConfirmBank: {
+ pending = true;
+ return TransitionResult.stay();
+ }
+ case WithdrawalGroupStatus.AbortedOtherWallet: {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
+ {},
+ );
+ }
case WithdrawalGroupStatus.DialogProposed: {
+ rec.exchangeBaseUrl = exchange.exchangeBaseUrl;
+ rec.instructedAmount = req.amount;
+ rec.restrictAge = req.restrictAge;
+ rec.denomsSel = initalDenoms;
+ rec.rawWithdrawalAmount = initalDenoms.totalWithdrawCost;
+ rec.effectiveWithdrawalAmount = initalDenoms.totalCoinValue;
+
+ rec.wgInfo = {
+ withdrawalType: WithdrawalRecordType.BankIntegrated,
+ exchangeCreditAccounts: withdrawalAccountList,
+ bankInfo: {
+ exchangePaytoUri,
+ talerWithdrawUri,
+ confirmUrl: confirmUrl,
+ timestampBankConfirmed: undefined,
+ timestampReserveInfoPosted: undefined,
+ wireTypes: bankWireTypes,
+ currency: bankCurrency,
+ },
+ };
+ pending = true;
rec.status = WithdrawalGroupStatus.PendingRegisteringBank;
return TransitionResult.transition(rec);
}
- default:
- throw Error("unable to confirm withdrawal in current state");
+ default: {
+ throw Error(
+ `unable to confirm withdrawal in current state: ${rec.status}`,
+ );
+ }
}
});
await wex.taskScheduler.resetTaskRetries(ctx.taskId);
- wex.taskScheduler.startShepherdTask(ctx.taskId);
+
+ wex.ws.notify({
+ type: NotificationType.BalanceChange,
+ hintTransactionId: ctx.transactionId,
+ });
+
+ const res = await wex.db.runReadWriteTx(
+ {
+ storeNames: ["exchanges"],
+ },
+ async (tx) => {
+ const r = await internalPerformExchangeWasUsed(
+ wex,
+ tx,
+ exchange.exchangeBaseUrl,
+ withdrawalGroup,
+ );
+ return r;
+ },
+ );
+ if (res.exchangeNotif) {
+ wex.ws.notify(res.exchangeNotif);
+ }
+
+ if (pending) {
+ await waitWithdrawalRegistered(wex, ctx);
+ }
}
/**
@@ -2929,180 +3268,119 @@ export async function acceptWithdrawalFromUri(
selectedExchange: string;
forcedDenomSel?: ForcedDenomSel;
restrictAge?: number;
+ amount?: AmountLike;
},
): Promise<AcceptWithdrawalResponse> {
const selectedExchange = req.selectedExchange;
logger.info(
- `accepting withdrawal via ${req.talerWithdrawUri}, canonicalized selected exchange ${selectedExchange}`,
- );
- const existingWithdrawalGroup = await wex.db.runReadOnlyTx(
- { storeNames: ["withdrawalGroups"] },
- async (tx) => {
- return await tx.withdrawalGroups.indexes.byTalerWithdrawUri.get(
- req.talerWithdrawUri,
- );
- },
+ `preparing withdrawal via ${req.talerWithdrawUri}, canonicalized selected exchange ${selectedExchange}`,
);
- if (existingWithdrawalGroup) {
- let url: string | undefined;
- if (
- existingWithdrawalGroup.wgInfo.withdrawalType ===
- WithdrawalRecordType.BankIntegrated
- ) {
- url = existingWithdrawalGroup.wgInfo.bankInfo.confirmUrl;
+ const p = await prepareBankIntegratedWithdrawal(wex, {
+ talerWithdrawUri: req.talerWithdrawUri,
+ });
+
+ let amount: AmountString;
+ if (p.info.amount == null) {
+ if (req.amount == null) {
+ throw Error(
+ "amount required, as withdrawal operation has flexible amount",
+ );
}
- return {
- reservePub: existingWithdrawalGroup.reservePub,
- confirmTransferUrl: url,
- transactionId: constructTransactionIdentifier({
- tag: TransactionType.Withdrawal,
- withdrawalGroupId: existingWithdrawalGroup.withdrawalGroupId,
- }),
- };
+ amount = req.amount as AmountString;
+ } else {
+ if (req.amount != null && Amounts.cmp(req.amount, p.info.amount) != 0) {
+ throw Error(
+ "mismatched amount, amount is fixed by bank but client provided different amount",
+ );
+ }
+ amount = p.info.amount;
}
- const exchange = await fetchFreshExchange(wex, selectedExchange);
- const withdrawInfo = await getBankWithdrawalInfo(
- wex.http,
- req.talerWithdrawUri,
- );
- const exchangePaytoUri = await getExchangePaytoUri(
- wex,
- selectedExchange,
- withdrawInfo.wireTypes,
- );
-
- const withdrawalAccountList = await fetchWithdrawalAccountInfo(
- wex,
- {
- exchange,
- instructedAmount: withdrawInfo.amount,
- },
- CancellationToken.CONTINUE,
- );
-
- const withdrawalGroup = await internalCreateWithdrawalGroup(wex, {
- amount: withdrawInfo.amount,
- exchangeBaseUrl: req.selectedExchange,
- wgInfo: {
- withdrawalType: WithdrawalRecordType.BankIntegrated,
- exchangeCreditAccounts: withdrawalAccountList,
- bankInfo: {
- exchangePaytoUri,
- talerWithdrawUri: req.talerWithdrawUri,
- confirmUrl: withdrawInfo.confirmTransferUrl,
- timestampBankConfirmed: undefined,
- timestampReserveInfoPosted: undefined,
- },
- },
+ logger.info(`confirming withdrawal with tx ${p.transactionId}`);
+ await confirmWithdrawal(wex, {
+ amount: Amounts.stringify(amount),
+ exchangeBaseUrl: selectedExchange,
+ transactionId: p.transactionId,
restrictAge: req.restrictAge,
forcedDenomSel: req.forcedDenomSel,
- reserveStatus: WithdrawalGroupStatus.PendingRegisteringBank,
- });
-
- const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;
-
- const ctx = new WithdrawTransactionContext(wex, withdrawalGroupId);
-
- wex.ws.notify({
- type: NotificationType.BalanceChange,
- hintTransactionId: ctx.transactionId,
});
- await waitWithdrawalRegistered(wex, ctx);
+ const newWithdrawralGroup = await wex.db.runReadOnlyTx(
+ { storeNames: ["withdrawalGroups"] },
+ async (tx) => {
+ return await tx.withdrawalGroups.indexes.byTalerWithdrawUri.get(
+ req.talerWithdrawUri,
+ );
+ },
+ );
- wex.taskScheduler.startShepherdTask(ctx.taskId);
+ checkDbInvariant(
+ newWithdrawralGroup !== undefined,
+ "withdrawal don't exist after confirm",
+ );
return {
- reservePub: withdrawalGroup.reservePub,
- confirmTransferUrl: withdrawInfo.confirmTransferUrl,
- transactionId: ctx.transactionId,
+ reservePub: newWithdrawralGroup.reservePub,
+ confirmTransferUrl: p.info.confirmTransferUrl,
+ transactionId: p.transactionId,
};
}
-async function internalWaitWithdrawalRegistered(
+async function waitWithdrawalRegistered(
wex: WalletExecutionContext,
ctx: WithdrawTransactionContext,
- withdrawalNotifFlag: AsyncFlag,
): Promise<void> {
- while (true) {
- const { withdrawalRec, retryRec } = await wex.db.runReadOnlyTx(
- { storeNames: ["withdrawalGroups", "operationRetries"] },
- async (tx) => {
- return {
- withdrawalRec: await tx.withdrawalGroups.get(ctx.withdrawalGroupId),
- retryRec: await tx.operationRetries.get(ctx.taskId),
- };
- },
- );
+ await genericWaitForState(wex, {
+ async checkState(): Promise<boolean> {
+ const { withdrawalRec, retryRec } = await wex.db.runReadOnlyTx(
+ { storeNames: ["withdrawalGroups", "operationRetries"] },
+ async (tx) => {
+ return {
+ withdrawalRec: await tx.withdrawalGroups.get(ctx.withdrawalGroupId),
+ retryRec: await tx.operationRetries.get(ctx.taskId),
+ };
+ },
+ );
- if (!withdrawalRec) {
- throw Error("withdrawal not found anymore");
- }
+ if (!withdrawalRec) {
+ throw Error("withdrawal not found anymore");
+ }
- switch (withdrawalRec.status) {
- case WithdrawalGroupStatus.FailedBankAborted:
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
- {},
- );
- case WithdrawalGroupStatus.PendingKyc:
- case WithdrawalGroupStatus.PendingAml:
- case WithdrawalGroupStatus.PendingQueryingStatus:
- case WithdrawalGroupStatus.PendingReady:
- case WithdrawalGroupStatus.Done:
- case WithdrawalGroupStatus.PendingWaitConfirmBank:
- return;
- case WithdrawalGroupStatus.PendingRegisteringBank:
- break;
- default: {
- if (retryRec) {
- if (retryRec.lastError) {
- throw TalerError.fromUncheckedDetail(retryRec.lastError);
- } else {
- throw Error("withdrawal unexpectedly pending");
+ switch (withdrawalRec.status) {
+ case WithdrawalGroupStatus.FailedBankAborted:
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
+ {},
+ );
+ case WithdrawalGroupStatus.PendingKyc:
+ case WithdrawalGroupStatus.PendingAml:
+ case WithdrawalGroupStatus.PendingQueryingStatus:
+ case WithdrawalGroupStatus.PendingReady:
+ case WithdrawalGroupStatus.Done:
+ case WithdrawalGroupStatus.PendingWaitConfirmBank:
+ return true;
+ case WithdrawalGroupStatus.PendingRegisteringBank:
+ break;
+ default: {
+ if (retryRec) {
+ if (retryRec.lastError) {
+ throw TalerError.fromUncheckedDetail(retryRec.lastError);
+ } else {
+ throw Error("withdrawal unexpectedly pending");
+ }
}
}
}
- }
-
- await withdrawalNotifFlag.wait();
- withdrawalNotifFlag.reset();
- }
-}
-
-async function waitWithdrawalRegistered(
- wex: WalletExecutionContext,
- ctx: WithdrawTransactionContext,
-): Promise<void> {
- // FIXME: Doesn't support cancellation yet
- // FIXME: We should use Symbol.dispose magic here for cleanup!
-
- const withdrawalNotifFlag = new AsyncFlag();
- // Raise exchangeNotifFlag whenever we get a notification
- // about our exchange.
- const cancelNotif = wex.ws.addNotificationListener((notif) => {
- if (
- notif.type === NotificationType.TransactionStateTransition &&
- notif.transactionId === ctx.transactionId
- ) {
- logger.info(`raising update notification: ${j2s(notif)}`);
- withdrawalNotifFlag.raise();
- }
+ return false;
+ },
+ filterNotification(notif) {
+ return (
+ notif.type === NotificationType.TransactionStateTransition &&
+ notif.transactionId === ctx.transactionId
+ );
+ },
});
-
- try {
- const res = await internalWaitWithdrawalRegistered(
- wex,
- ctx,
- withdrawalNotifFlag,
- );
- logger.info("done waiting for ready exchange");
- return res;
- } finally {
- cancelNotif();
- }
}
async function fetchAccount(
@@ -3164,7 +3442,7 @@ async function fetchAccount(
});
if (reservePub != null) {
paytoUri = addPaytoQueryParams(paytoUri, {
- message: `Taler Withdrawal ${reservePub}`,
+ message: `Taler ${reservePub}`,
});
}
const acctInfo: WithdrawalExchangeAccountDetails = {
@@ -3270,7 +3548,7 @@ export async function createManualWithdrawal(
);
const withdrawalGroup = await internalCreateWithdrawalGroup(wex, {
- amount: Amounts.jsonifyAmount(req.amount),
+ amount: amount,
wgInfo: {
withdrawalType: WithdrawalRecordType.BankManual,
exchangeCreditAccounts: withdrawalAccountsList,
@@ -3355,7 +3633,7 @@ async function internalWaitWithdrawalFinal(
// Check if refresh is final
const res = await ctx.wex.db.runReadOnlyTx(
- { storeNames: ["withdrawalGroups", "operationRetries"] },
+ { storeNames: ["withdrawalGroups"] },
async (tx) => {
return {
wg: await tx.withdrawalGroups.get(ctx.withdrawalGroupId),
@@ -3398,7 +3676,7 @@ export async function getWithdrawalDetailsForAmount(
type: ObservabilityEventType.Message,
contents: `Cancelling previous key ${clientCancelKey}`,
});
- prevCts.cancel();
+ prevCts.cancel(`getting details amount`);
} else {
wex.oc.observe({
type: ObservabilityEventType.Message,
diff --git a/packages/taler-wallet-embedded/package.json b/packages/taler-wallet-embedded/package.json
index ee9efafdd..fe64396fb 100644
--- a/packages/taler-wallet-embedded/package.json
+++ b/packages/taler-wallet-embedded/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-wallet-embedded",
- "version": "0.10.7",
+ "version": "0.11.4",
"description": "",
"engines": {
"node": ">=0.18.0"
diff --git a/packages/taler-wallet-embedded/src/wallet-qjs-tests.ts b/packages/taler-wallet-embedded/src/wallet-qjs-tests.ts
new file mode 100644
index 000000000..ca4eb28c0
--- /dev/null
+++ b/packages/taler-wallet-embedded/src/wallet-qjs-tests.ts
@@ -0,0 +1,118 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+ (C) 2024 Taler Systems SA
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { userIdentifierDerive } from "@gnu-taler/anastasis-core/lib/crypto.js";
+import { AmountString, j2s } from "@gnu-taler/taler-util";
+import {
+ WalletApiOperation,
+ createNativeWalletHost2,
+} from "@gnu-taler/taler-wallet-core";
+
+export async function testWithGv() {
+ const w = await createNativeWalletHost2({});
+ await w.wallet.client.call(WalletApiOperation.InitWallet, {
+ config: {
+ features: {
+ allowHttp: true,
+ },
+ },
+ });
+ await w.wallet.client.call(WalletApiOperation.RunIntegrationTest, {
+ amountToSpend: "KUDOS:1" as AmountString,
+ amountToWithdraw: "KUDOS:3" as AmountString,
+ corebankApiBaseUrl: "https://bank.demo.taler.net/",
+ exchangeBaseUrl: "https://exchange.demo.taler.net/",
+ merchantBaseUrl: "https://backend.demo.taler.net/",
+ merchantAuthToken: "secret-token:sandbox",
+ });
+ await w.wallet.client.call(WalletApiOperation.TestingWaitTasksDone, {});
+ await w.wallet.client.call(WalletApiOperation.Shutdown, {});
+}
+
+export async function testWithFdold() {
+ const w = await createNativeWalletHost2({});
+ await w.wallet.client.call(WalletApiOperation.InitWallet, {
+ config: {
+ features: {
+ allowHttp: true,
+ },
+ },
+ });
+ await w.wallet.client.call(WalletApiOperation.RunIntegrationTest, {
+ amountToSpend: "TESTKUDOS:1" as AmountString,
+ amountToWithdraw: "TESTKUDOS:3" as AmountString,
+ corebankApiBaseUrl: "https://bank.taler.fdold.eu/",
+ exchangeBaseUrl: "https://exchange.taler.fdold.eu/",
+ merchantBaseUrl: "https://merchant.taler.fdold.eu/",
+ });
+ await w.wallet.client.call(WalletApiOperation.TestingWaitTasksDone, {});
+ await w.wallet.client.call(WalletApiOperation.Shutdown, {});
+}
+
+export async function testWithLocal(path: string) {
+ console.log("running local test");
+ const w = await createNativeWalletHost2({
+ persistentStoragePath: path ?? "walletdb.json",
+ });
+ console.log("created wallet");
+ await w.wallet.client.call(WalletApiOperation.InitWallet, {
+ config: {
+ features: {
+ allowHttp: true,
+ },
+ testing: {
+ skipDefaults: true,
+ },
+ },
+ });
+ console.log("initialized wallet");
+ await w.wallet.client.call(WalletApiOperation.RunIntegrationTest, {
+ amountToSpend: "TESTKUDOS:1" as AmountString,
+ amountToWithdraw: "TESTKUDOS:3" as AmountString,
+ corebankApiBaseUrl: "http://localhost:8082/taler-bank-access/",
+ exchangeBaseUrl: "http://localhost:8081/",
+ merchantBaseUrl: "http://localhost:8083/",
+ });
+ console.log("started integration test");
+ await w.wallet.client.call(WalletApiOperation.TestingWaitTasksDone, {});
+ console.log("done with task loop");
+ await w.wallet.client.call(WalletApiOperation.Shutdown, {});
+ console.log("DB stats:", j2s(w.getDbStats()));
+}
+
+export async function testArgon2id() {
+ const userIdVector = {
+ input_id_data: {
+ name: "Fleabag",
+ ssn: "AB123",
+ },
+ input_server_salt: "FZ48EFS7WS3R2ZR4V53A3GFFY4",
+ output_id:
+ "YS45R6CGJV84K1NN7T14ZBCPVTZ6H15XJSM1FV0R748MHPV82SM0126EBZKBAAGCR34Q9AFKPEW1HRT2Q9GQ5JRA3642AB571DKZS18",
+ };
+
+ if (
+ (await userIdentifierDerive(
+ userIdVector.input_id_data,
+ userIdVector.input_server_salt,
+ )) != userIdVector.output_id
+ ) {
+ throw Error("argon2id is not working!");
+ }
+
+ console.log("argon2id is working!");
+}
diff --git a/packages/taler-wallet-embedded/src/wallet-qjs.ts b/packages/taler-wallet-embedded/src/wallet-qjs.ts
index 98b73fc44..2780a3cab 100644
--- a/packages/taler-wallet-embedded/src/wallet-qjs.ts
+++ b/packages/taler-wallet-embedded/src/wallet-qjs.ts
@@ -1,6 +1,7 @@
/*
This file is part of GNU Taler
(C) 2019 GNUnet e.V.
+ (C) 2024 Taler Systems SA
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -28,30 +29,27 @@ import {
mergeDiscoveryAggregate,
reduceAction,
} from "@gnu-taler/anastasis-core";
-import { userIdentifierDerive } from "@gnu-taler/anastasis-core/lib/crypto.js";
import {
- AmountString,
CoreApiMessageEnvelope,
CoreApiResponse,
CoreApiResponseSuccess,
Logger,
- PartialWalletRunConfig,
WalletNotification,
enableNativeLogging,
getErrorDetailFromException,
- j2s,
openPromise,
performanceNow,
setGlobalLogLevelFromString,
} from "@gnu-taler/taler-util";
import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
import { qjsOs } from "@gnu-taler/taler-util/qtart";
+import { Wallet, createNativeWalletHost2 } from "@gnu-taler/taler-wallet-core";
import {
- DefaultNodeWalletArgs,
- Wallet,
- WalletApiOperation,
- createNativeWalletHost2,
-} from "@gnu-taler/taler-wallet-core";
+ testArgon2id,
+ testWithFdold,
+ testWithGv,
+ testWithLocal,
+} from './wallet-qjs-tests.js';
setGlobalLogLevelFromString("trace");
@@ -68,9 +66,6 @@ function sendNativeMessage(ev: CoreApiMessageEnvelope): void {
}
class NativeWalletMessageHandler {
- walletArgs: DefaultNodeWalletArgs | undefined;
- walletConfig: PartialWalletRunConfig | undefined;
- maybeWallet: Wallet | undefined;
wp = openPromise<Wallet>();
httpLib = createPlatformHttpLib();
@@ -91,23 +86,9 @@ class NativeWalletMessageHandler {
};
};
- let initResponse: any = {};
-
- const reinit = async () => {
- logger.info("in reinit");
- const wR = await createNativeWalletHost2(this.walletArgs);
- const w = wR.wallet;
- this.maybeWallet = w;
- const resp = await w.handleCoreApiRequest("initWallet", "native-init", {
- config: this.walletConfig,
- });
- initResponse = resp.type == "response" ? resp.result : resp.error;
- this.wp.resolve(w);
- };
-
switch (operation) {
case "init": {
- this.walletArgs = {
+ const wR = await createNativeWalletHost2({
notifyHandler: async (notification: WalletNotification) => {
sendNativeMessage({ type: "notification", payload: notification });
},
@@ -115,38 +96,29 @@ class NativeWalletMessageHandler {
httpLib: this.httpLib,
cryptoWorkerType: args.cryptoWorkerType,
...args,
- };
- this.walletConfig = args.config ?? {};
- const logLevel = args.logLevel;
- if (logLevel) {
- setGlobalLogLevelFromString(logLevel);
+ });
+
+ if (args.logLevel) {
+ setGlobalLogLevelFromString(args.logLevel);
}
- const nativeLogging = args.useNativeLogging ?? false;
- if (nativeLogging) {
+
+ if (args.useNativeLogging === true) {
enableNativeLogging();
}
- await reinit();
+
+ const resp = await wR.wallet.handleCoreApiRequest("initWallet", "native-init", {
+ config: args.config ?? {},
+ });
+
+ let initResponse: any = resp.type == "response" ? resp.result : resp.error;
+
+ this.wp.resolve(wR.wallet);
+
return wrapSuccessResponse({
...initResponse,
});
}
- case "startTunnel": {
- // this.httpLib.useNfcTunnel = true;
- throw Error("not implemented");
- }
- case "stopTunnel": {
- // this.httpLib.useNfcTunnel = false;
- throw Error("not implemented");
- }
- case "tunnelResponse": {
- // httpLib.handleTunnelResponse(msg.args);
- throw Error("not implemented");
- }
- case "reset": {
- throw Error(
- "reset not supported anymore, please use the clearDb wallet-core request",
- );
- }
+
default: {
const wallet = await this.wp.promise;
return await wallet.handleCoreApiRequest(operation, id, args);
@@ -175,17 +147,22 @@ async function handleAnastasisRequest(
let req = args ?? {};
switch (operation) {
- case "anastasisReduce":
- // TODO: do some input validation here
+ case "anastasisReduce": {
let reduceRes = await reduceAction(req.state, req.action, req.args ?? {});
// For now, this will return "success" even if the wrapped Anastasis
// response is a ReducerStateError.
return wrapSuccessResponse(reduceRes);
- case "anastasisStartBackup":
+ }
+
+ case "anastasisStartBackup": {
return wrapSuccessResponse(await getBackupStartState());
- case "anastasisStartRecovery":
+ }
+
+ case "anastasisStartRecovery": {
return wrapSuccessResponse(await getRecoveryStartState());
- case "anastasisDiscoverPolicies":
+ }
+
+ case "anastasisDiscoverPolicies": {
let discoverRes = await discoverPolicies(req.state, req.cursor);
let aggregatedPolicies = mergeDiscoveryAggregate(
discoverRes.policies ?? [],
@@ -199,19 +176,25 @@ async function handleAnastasisRequest(
cursor: discoverRes.cursor,
},
});
- default:
+ }
+
+ default: {
throw Error("unsupported anastasis operation");
+ }
}
}
export function installNativeWalletListener(): void {
setGlobalLogLevelFromString("trace");
+
const handler = new NativeWalletMessageHandler();
+
const onMessage = async (msgStr: any): Promise<void> => {
if (typeof msgStr !== "string") {
logger.error("expected string as message");
return;
}
+
const msg = JSON.parse(msgStr);
const operation = msg.operation;
if (typeof operation !== "string") {
@@ -220,28 +203,31 @@ export function installNativeWalletListener(): void {
);
return;
}
+
const id = msg.id;
logger.info(`native listener: got request for ${operation} (${id})`);
- const startTimeNs = performanceNow();
-
+ const startTimeMs = performanceNow();
let respMsg: CoreApiResponse;
+
try {
if (msg.operation.startsWith("anastasis")) {
+ // Entry point for Anastasis
respMsg = await handleAnastasisRequest(operation, id, msg.args ?? {});
} else if (msg.operation === "testing-dangerously-eval") {
// Eval code, used only for testing. No client may rely on this.
logger.info(`evaluating ${msg.args.jscode}`);
const f = new Function(msg.args.jscode);
f();
+
respMsg = {
type: "response",
result: {},
operation: "testing-dangerously-eval",
id: msg.id,
};
- }
- {
+ } else {
+ // Entry point for wallet-core
respMsg = await handler.handleMessage(operation, id, msg.args ?? {});
}
} catch (e) {
@@ -252,10 +238,12 @@ export function installNativeWalletListener(): void {
error: getErrorDetailFromException(e),
};
}
- const endTimeNs = performanceNow();
+
+ const endTimeMs = performanceNow();
const requestDurationMs = Math.round(
- Number((endTimeNs - startTimeNs) / 1000n / 1000n),
+ Number((endTimeMs - startTimeMs) / 1000n / 1000n),
);
+
logger.info(
`native listener: sending back ${respMsg.type} message for operation ${operation} (${id}) after ${requestDurationMs} ms`,
);
@@ -269,102 +257,6 @@ export function installNativeWalletListener(): void {
// @ts-ignore
globalThis.installNativeWalletListener = installNativeWalletListener;
-
-export async function testWithGv() {
- const w = await createNativeWalletHost2({});
- await w.wallet.client.call(WalletApiOperation.InitWallet, {
- config: {
- features: {
- allowHttp: true,
- },
- },
- });
- await w.wallet.client.call(WalletApiOperation.RunIntegrationTest, {
- amountToSpend: "KUDOS:1" as AmountString,
- amountToWithdraw: "KUDOS:3" as AmountString,
- corebankApiBaseUrl: "https://bank.demo.taler.net/",
- exchangeBaseUrl: "https://exchange.demo.taler.net/",
- merchantBaseUrl: "https://backend.demo.taler.net/",
- merchantAuthToken: "secret-token:sandbox",
- });
- await w.wallet.client.call(WalletApiOperation.TestingWaitTasksDone, {});
- await w.wallet.client.call(WalletApiOperation.Shutdown, {});
-}
-
-export async function testWithFdold() {
- const w = await createNativeWalletHost2({});
- await w.wallet.client.call(WalletApiOperation.InitWallet, {
- config: {
- features: {
- allowHttp: true,
- },
- },
- });
- await w.wallet.client.call(WalletApiOperation.RunIntegrationTest, {
- amountToSpend: "TESTKUDOS:1" as AmountString,
- amountToWithdraw: "TESTKUDOS:3" as AmountString,
- corebankApiBaseUrl: "https://bank.taler.fdold.eu/",
- exchangeBaseUrl: "https://exchange.taler.fdold.eu/",
- merchantBaseUrl: "https://merchant.taler.fdold.eu/",
- });
- await w.wallet.client.call(WalletApiOperation.TestingWaitTasksDone, {});
- await w.wallet.client.call(WalletApiOperation.Shutdown, {});
-}
-
-export async function testWithLocal(path: string) {
- console.log("running local test");
- const w = await createNativeWalletHost2({
- persistentStoragePath: path ?? "walletdb.json",
- });
- console.log("created wallet");
- await w.wallet.client.call(WalletApiOperation.InitWallet, {
- config: {
- features: {
- allowHttp: true,
- },
- testing: {
- skipDefaults: true,
- },
- },
- });
- console.log("initialized wallet");
- await w.wallet.client.call(WalletApiOperation.RunIntegrationTest, {
- amountToSpend: "TESTKUDOS:1" as AmountString,
- amountToWithdraw: "TESTKUDOS:3" as AmountString,
- corebankApiBaseUrl: "http://localhost:8082/taler-bank-access/",
- exchangeBaseUrl: "http://localhost:8081/",
- merchantBaseUrl: "http://localhost:8083/",
- });
- console.log("started integration test");
- await w.wallet.client.call(WalletApiOperation.TestingWaitTasksDone, {});
- console.log("done with task loop");
- await w.wallet.client.call(WalletApiOperation.Shutdown, {});
- console.log("DB stats:", j2s(w.getDbStats()));
-}
-
-export async function testArgon2id() {
- const userIdVector = {
- input_id_data: {
- name: "Fleabag",
- ssn: "AB123",
- },
- input_server_salt: "FZ48EFS7WS3R2ZR4V53A3GFFY4",
- output_id:
- "YS45R6CGJV84K1NN7T14ZBCPVTZ6H15XJSM1FV0R748MHPV82SM0126EBZKBAAGCR34Q9AFKPEW1HRT2Q9GQ5JRA3642AB571DKZS18",
- };
-
- if (
- (await userIdentifierDerive(
- userIdVector.input_id_data,
- userIdVector.input_server_salt,
- )) != userIdVector.output_id
- ) {
- throw Error("argon2id is not working!");
- }
-
- console.log("argon2id is working!");
-}
-
// @ts-ignore
globalThis.testWithGv = testWithGv;
// @ts-ignore
diff --git a/packages/taler-wallet-webextension/manifest-common.json b/packages/taler-wallet-webextension/manifest-common.json
index 32bd5267f..88f152d50 100644
--- a/packages/taler-wallet-webextension/manifest-common.json
+++ b/packages/taler-wallet-webextension/manifest-common.json
@@ -2,7 +2,7 @@
"name": "GNU Taler Wallet (git)",
"description": "Privacy preserving and transparent payments",
"author": "GNU Taler Developers",
- "version": "0.10.7",
+ "version": "0.11.4",
"icons": {
"16": "static/img/taler-logo-16.png",
"19": "static/img/taler-logo-19.png",
@@ -14,5 +14,5 @@
"256": "static/img/taler-logo-256.png",
"512": "static/img/taler-logo-512.png"
},
- "version_name": "0.10.7"
+ "version_name": "0.11.4"
}
diff --git a/packages/taler-wallet-webextension/package.json b/packages/taler-wallet-webextension/package.json
index bf063d76e..90679cfdd 100644
--- a/packages/taler-wallet-webextension/package.json
+++ b/packages/taler-wallet-webextension/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-wallet-webextension",
- "version": "0.10.7",
+ "version": "0.11.4",
"description": "GNU Taler Wallet browser extension",
"main": "./build/index.js",
"types": "./build/index.d.ts",
diff --git a/packages/taler-wallet-webextension/src/components/HistoryItem.tsx b/packages/taler-wallet-webextension/src/components/HistoryItem.tsx
index 9be9326b2..8e48a2e9f 100644
--- a/packages/taler-wallet-webextension/src/components/HistoryItem.tsx
+++ b/packages/taler-wallet-webextension/src/components/HistoryItem.tsx
@@ -26,7 +26,7 @@ import {
DenomLossEventType,
parsePaytoUri,
} from "@gnu-taler/taler-util";
-import { h, VNode } from "preact";
+import { Fragment, h, VNode } from "preact";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Avatar } from "../mui/Avatar.js";
import { Pages } from "../NavigationBar.js";
@@ -49,6 +49,8 @@ export function HistoryItem(props: { tx: Transaction }): VNode {
*/
switch (tx.type) {
case TransactionType.Withdrawal:
+ //withdrawal that has not been confirmed are hidden
+ if (!tx.exchangeBaseUrl) return <Fragment />
return (
<Layout
id={tx.transactionId}
diff --git a/packages/taler-wallet-webextension/src/components/Part.tsx b/packages/taler-wallet-webextension/src/components/Part.tsx
index b95bbf3b7..2fb03308b 100644
--- 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
index 41b0c5c76..f29d0b0f7 100644
--- a/packages/taler-wallet-webextension/src/components/WalletActivity.tsx
+++ b/packages/taler-wallet-webextension/src/components/WalletActivity.tsx
@@ -15,7 +15,6 @@
*/
import {
AbsoluteTime,
- ExchangeStateTransitionNotification,
NotificationType,
ObservabilityEventType,
RequestProgressNotification,
@@ -34,12 +33,12 @@ 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";
-import { TextField } from "../mui/TextField.js";
-import { WalletActivityTrack } from "../wxBackend.js";
const OPEN_ACTIVITY_HEIGHT_PX = 250;
const CLOSE_ACTIVITY_HEIGHT_PX = 40;
@@ -84,7 +83,9 @@ export function WalletActivity(): VNode {
cursor: "pointer",
}}
>
- click here to open
+ <i18n.Translate>
+ Click here to open the wallet activity tab.
+ </i18n.Translate>
</div>
</div>
);
diff --git a/packages/taler-wallet-webextension/src/components/styled/index.tsx b/packages/taler-wallet-webextension/src/components/styled/index.tsx
index 89678c74a..739b71064 100644
--- 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/InvoicePay/views.tsx b/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx
index 547d5ac9a..0d8035136 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx
@@ -24,12 +24,21 @@ import {
InvoicePaymentDetails,
} from "../../wallet/Transaction.js";
import { State } from "./index.js";
+import { AbsoluteTime, Duration } from "@gnu-taler/taler-util";
export function ReadyView(
state: State.Ready | State.NoBalanceForCurrency | State.NoEnoughBalance,
): VNode {
const { i18n } = useTranslationContext();
const { summary, effective, raw, expiration, uri, status, payStatus } = state;
+
+ const inFiveMinutes = AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ minutes: 5 }),
+ );
+ const willExpireSoon =
+ expiration && AbsoluteTime.cmp(expiration, inFiveMinutes) === -1;
+
return (
<Fragment>
<section style={{ textAlign: "left" }}>
@@ -42,11 +51,13 @@ export function ReadyView(
/>
}
/>
- <Part
- title={i18n.str`Valid until`}
- text={<Time timestamp={expiration} format="dd MMMM yyyy, HH:mm" />}
- kind="neutral"
- />
+ {willExpireSoon && (
+ <Part
+ title={i18n.str`Expires at`}
+ text={<Time timestamp={expiration} format="HH:mm" />}
+ kind="neutral"
+ />
+ )}
</section>
<PaymentButtons
amount={effective}
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
index 8bbb8dac2..b1eee85ec 100644
--- a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
@@ -18,6 +18,7 @@ import {
AbsoluteTime,
Amounts,
MerchantContractTerms as ContractTerms,
+ Duration,
PreparePayResultType,
TranslatedString,
} from "@gnu-taler/taler-util";
@@ -27,7 +28,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";
@@ -50,13 +55,39 @@ export function BaseView(state: SupportedStates): VNode {
: Amounts.zeroOfCurrency(state.amount.currency)
: state.amount;
+ const expiration = !contractTerms.pay_deadline
+ ? undefined
+ : AbsoluteTime.fromProtocolTimestamp(contractTerms.pay_deadline);
+ const inFiveMinutes = AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ minutes: 5 }),
+ );
+ const willExpireSoon =
+ !expiration || expiration.t_ms === "never"
+ ? undefined
+ : AbsoluteTime.cmp(expiration, inFiveMinutes) === -1;
return (
<Fragment>
<ShowImportantMessage state={state} />
<section style={{ textAlign: "left" }}>
<Part
- title={i18n.str`Purchase`}
+ title={
+ contractTerms.minimum_age ? (
+ <Fragment>
+ <i18n.Translate>Purchase</i18n.Translate>
+ &nbsp;
+ <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"
/>
@@ -65,17 +96,10 @@ export function BaseView(state: SupportedStates): VNode {
text={<MerchantDetails merchant={contractTerms.merchant} />}
kind="neutral"
/>
- {contractTerms.pay_deadline && (
+ {willExpireSoon && (
<Part
- title={i18n.str`Valid until`}
- text={
- <Time
- timestamp={AbsoluteTime.fromProtocolTimestamp(
- contractTerms.pay_deadline,
- )}
- format="dd MMMM yyyy, HH:mm"
- />
- }
+ title={i18n.str`Expires at`}
+ text={<Time timestamp={expiration} format="HH:mm" />}
kind="neutral"
/>
)}
diff --git a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/index.ts b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/index.ts
index f5a8c8814..1e903fe46 100644
--- 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
index 6b4584fea..ba854a93c 100644
--- 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,38 @@ 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) {
+ if (!requireMoreInfo) {
payStatus = await api.wallet.call(
WalletApiOperation.PreparePayForTemplate,
- {
- talerPayTemplateUri: talerTemplateUri,
- templateParams: {},
- },
+ { talerPayTemplateUri: talerTemplateUri },
);
}
const balance = await api.wallet.call(WalletApiOperation.GetBalances, {});
- return { payStatus, balance, uri: talerTemplateUri };
+ return { payStatus, balance, uri: talerTemplateUri, templateP };
}, []);
if (!hook) {
@@ -108,60 +104,99 @@ 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;
+
+ const defaultAmount =
+ def?.amount !== undefined ? Amounts.parseOrThrow(def.amount) : undefined;
+ const defaultSummary = def?.summary;
+
+ const zero = fixedAmount
+ ? Amounts.zeroOfAmount(fixedAmount)
+ : cfg.currency !== undefined
+ ? Amounts.zeroOfCurrency(cfg.currency)
+ : defaultAmount !== undefined
+ ? Amounts.zeroOfAmount(defaultAmount)
+ : def?.currency !== undefined
+ ? Amounts.zeroOfCurrency(def.currency)
+ : Amounts.zeroOfCurrency(
+ hook.response.templateP.supportedCurrencies[0],
+ );
+
+ const [amount, setAmount] = useState(defaultAmount ?? fixedAmount ?? zero);
+ const [summary, setSummary] = useState(defaultSummary ?? fixedSummary ?? "");
+
+ async function createOrder() {
+ try {
+ 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
- ? ({
- onInput: (a) => {
- setAmount(a);
- },
- value: amount,
- error: errors?.amount,
- } as AmountFieldHandler)
- : undefined,
- summary:
- summary !== undefined
- ? ({
- onInput: (t) => {
- setSummary(t);
- },
- value: summary,
- error: errors?.summary,
- } as TextFieldHandler)
- : undefined,
- onCreate: {
- onClick: errors
- ? undefined
- : safely("create order for pay template", createOrder),
- },
+
+ 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: {
+ onInput:
+ fixedAmount !== undefined
+ ? undefined
+ : (a) => {
+ setAmount(a);
+ },
+ value: amount,
+ error: errors?.amount,
+ } as AmountFieldHandler,
+ summary: {
+ onInput:
+ fixedSummary !== undefined
+ ? undefined
+ : (t) => {
+ setSummary(t);
+ },
+ value: summary,
+ error: errors?.summary,
+ } as TextFieldHandler,
+ onCreate: {
+ onClick: errors
+ ? undefined
+ : safely("create order for pay template", createOrder),
+ },
+ };
};
}
diff --git a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx
index 88658b5e1..4a1cfe3ac 100644
--- 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 {
@@ -33,24 +33,11 @@ export function ReadyView({
return (
<Fragment>
<section style={{ textAlign: "left" }}>
- {/* <Part
- title={
- <div
- style={{
- display: "flex",
- alignItems: "center",
- }}
- >
- <i18n.Translate>Merchant</i18n.Translate>
- </div>
- }
- text={<ExchangeDetails exchange={exchangeUrl} />}
- kind="neutral"
- big
- /> */}
{!amount ? undefined : (
<p>
- <AmountField label={i18n.str`Amount`} handler={amount} />
+ <AmountField label={i18n.str`Amount`}
+ handler={amount}
+ />
</p>
)}
{!summary ? undefined : (
@@ -60,6 +47,7 @@ export function ReadyView({
variant="filled"
required
fullWidth
+ disabled={summary.onInput === undefined}
error={summary.error}
value={summary.value}
onChange={summary.onInput}
@@ -67,6 +55,12 @@ export function ReadyView({
</p>
)}
</section>
+ {minAge ? (
+ <section>
+ <AgeSign size={25}>{minAge}+</AgeSign>
+ <i18n.Translate>This purchase is age restricted.</i18n.Translate>
+ </section>
+ ) : undefined}
<section>
<Button onClick={onCreate.onClick} variant="contained" color="success">
<i18n.Translate>Review order</i18n.Translate>
diff --git a/packages/taler-wallet-webextension/src/cta/TransferPickup/views.tsx b/packages/taler-wallet-webextension/src/cta/TransferPickup/views.tsx
index caa1b485a..e82c4fbd2 100644
--- a/packages/taler-wallet-webextension/src/cta/TransferPickup/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/TransferPickup/views.tsx
@@ -26,6 +26,7 @@ import {
} from "../../wallet/Transaction.js";
import { State } from "./index.js";
import { TermsOfService } from "../../components/TermsOfService/index.js";
+import { AbsoluteTime, Duration } from "@gnu-taler/taler-util";
export function ReadyView({
accept,
@@ -36,6 +37,12 @@ export function ReadyView({
raw,
}: State.Ready): VNode {
const { i18n } = useTranslationContext();
+ const inFiveMinutes = AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ minutes: 5 }),
+ );
+ const willExpireSoon =
+ expiration && AbsoluteTime.cmp(expiration, inFiveMinutes) === -1;
return (
<Fragment>
<section style={{ textAlign: "left" }}>
@@ -49,15 +56,16 @@ export function ReadyView({
/>
}
/>
-
- <Part
- title={i18n.str`Valid until`}
- text={<Time timestamp={expiration} format="dd MMMM yyyy, HH:mm" />}
- kind="neutral"
- />
+ {willExpireSoon && (
+ <Part
+ title={i18n.str`Expires at`}
+ text={<Time timestamp={expiration} format="HH:mm" />}
+ kind="neutral"
+ />
+ )}
</section>
<section>
- <TermsOfService key="terms" exchangeUrl={exchangeBaseUrl} >
+ <TermsOfService key="terms" exchangeUrl={exchangeBaseUrl}>
<Button variant="contained" color="success" onClick={accept.onClick}>
<i18n.Translate>
Receive &nbsp; {<Amount value={effective} />}
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts
index 1f8745a5d..418fef505 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts
@@ -19,7 +19,6 @@ import {
AmountString,
CurrencySpecification,
ExchangeListItem,
- WithdrawalExchangeAccountDetails,
} from "@gnu-taler/taler-util";
import { Loading } from "../../components/Loading.js";
import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js";
@@ -84,7 +83,9 @@ export namespace State {
export interface AlreadyCompleted {
status: "already-completed";
operationState: "confirmed" | "aborted" | "selected";
- confirmTransferUrl?: string,
+ thisWallet: boolean;
+ redirectToTx: () => void;
+ confirmTransferUrl?: string;
error: undefined;
}
@@ -94,20 +95,26 @@ export namespace State {
currentExchange: ExchangeListItem;
- chosenAmount: AmountJson;
- withdrawalFee: AmountJson;
+ amount: AmountFieldHandler;
+ editableAmount: boolean;
+
+ bankFee: AmountJson;
toBeReceived: AmountJson;
+ toBeSent: AmountJson;
doWithdrawal: ButtonHandler;
doSelectExchange: ButtonHandler;
+ editableExchange: boolean;
chooseCurrencies: string[];
selectedCurrency: string;
changeCurrency: (s: string) => void;
- conversionInfo: {
- spec: CurrencySpecification,
- amount: AmountJson,
- } | undefined;
+ conversionInfo:
+ | {
+ spec: CurrencySpecification;
+ amount: AmountJson;
+ }
+ | undefined;
ageRestriction?: SelectFieldHandler;
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts
index 044f2434f..0541bbf3f 100644
--- 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,
},
);
@@ -181,9 +185,17 @@ export function useComponentStateFromParams({
cancel,
onSuccess,
undefined,
- chosenAmount,
- exchangeList,
- exchangeByTalerUri,
+ {
+ amount: chosenAmount,
+ currency: chosenAmount.currency,
+ maxAmount: Amounts.zeroOfCurrency(chosenAmount.currency),
+ bankFee: Amounts.zeroOfCurrency(chosenAmount.currency),
+ editableAmount: true,
+ editableExchange: true,
+ exchange: exchangeByTalerUri,
+ exchangeList: exchangeList,
+ },
+ setUpdatedExchangeByUser,
);
}
@@ -194,6 +206,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,45 +218,43 @@ export function useComponentStateFromURI({
: maybeTalerUri;
const uriInfo = await api.wallet.call(
- WalletApiOperation.GetWithdrawalDetailsForUri,
+ WalletApiOperation.PrepareBankIntegratedWithdrawal,
+ { talerWithdrawUri },
+ );
+ const { status } = uriInfo.info;
+ const txInfo = await api.wallet.call(
+ WalletApiOperation.GetTransactionById,
{
- talerWithdrawUri,
- // notifyChangeFromPendingTimeoutMs: 30 * 1000,
+ transactionId: uriInfo.transactionId,
},
);
- const {
- amount,
- defaultExchangeBaseUrl,
- possibleExchanges,
- operationId,
- confirmTransferUrl,
- status,
- } = uriInfo;
- const transaction = await api.wallet.call(
- WalletApiOperation.GetWithdrawalTransactionByUri,
- { talerWithdrawUri },
- );
return {
talerWithdrawUri,
- operationId,
status,
- transaction,
- confirmTransferUrl,
- amount: Amounts.parseOrThrow(amount),
- thisExchange: defaultExchangeBaseUrl,
- exchanges: possibleExchanges,
+ transactionId: uriInfo.transactionId,
+ bankWithdrawalInfo: uriInfo.info,
+ txInfo: txInfo,
};
});
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,39 +272,55 @@ export function useComponentStateFromURI({
}
const uri = uriInfoHook.response.talerWithdrawUri;
- const chosenAmount = uriInfoHook.response.amount;
- const defaultExchange = uriInfoHook.response.thisExchange;
- const exchangeList = uriInfoHook.response.exchanges;
+ const txId = uriInfoHook.response.transactionId;
+ const bwi = uriInfoHook.response.bankWithdrawalInfo;
+
+ const amount =
+ bwi.amount === undefined
+ ? Amounts.zeroOfCurrency(bwi.currency)
+ : Amounts.parseOrThrow(bwi.amount);
+
+ const maxAmount =
+ bwi.maxAmount === undefined
+ ? Amounts.zeroOfCurrency(bwi.currency)
+ : Amounts.parseOrThrow(bwi.maxAmount);
+
+ const bankFee =
+ bwi.wireFee === undefined
+ ? Amounts.zeroOfCurrency(bwi.currency)
+ : Amounts.parseOrThrow(bwi.wireFee);
async function doManagedWithdraw(
exchange: string,
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,
- },
- );
+ 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,
+ confirmTransferUrl: bwi.confirmTransferUrl,
+ thisWallet: info.txState.major === TransactionMajorState.Pending,
+ redirectToTx: () => onSuccess(info.transactionId),
error: undefined,
};
}
@@ -303,16 +331,36 @@ export function useComponentStateFromURI({
cancel,
onSuccess,
uri,
- chosenAmount,
- exchangeList,
- defaultExchange,
+ {
+ amount,
+ bankFee,
+ maxAmount,
+ currency: bwi.currency,
+ editableAmount: bwi.editableAmount,
+ editableExchange: bwi.editableExchange,
+ exchange: bwi.defaultExchangeBaseUrl,
+ exchangeList: bwi.possibleExchanges,
+ },
+ setUpdatedExchangeByUser,
);
}, []);
}
+type WithdrawalInfo = {
+ currency: string;
+ amount: AmountJson;
+ bankFee: AmountJson;
+ maxAmount: AmountJson;
+ editableAmount: boolean;
+ exchange: string | undefined;
+ editableExchange: boolean;
+ exchangeList: ExchangeListItem[];
+};
+
type ManualOrManagedWithdrawFunction = (
exchange: string,
ageRestricted: number | undefined,
+ amount: AmountString,
) => Promise<{ transactionId: string; confirmTransferUrl: string | undefined }>;
function exchangeSelectionState(
@@ -320,17 +368,30 @@ function exchangeSelectionState(
cancel: () => Promise<void>,
onSuccess: (txid: string) => Promise<void>,
talerWithdrawUri: string | undefined,
- chosenAmount: AmountJson,
- exchangeList: ExchangeListItem[],
- exchangeSuggestedByTheBank: string | undefined,
+ wInfo: WithdrawalInfo,
+ onExchangeUpdated: (ex: string) => void,
): RecursiveState<State> {
const api = useBackendContext();
const selectedExchange = useSelectedExchange({
- currency: chosenAmount.currency,
- defaultExchange: exchangeSuggestedByTheBank,
- list: exchangeList,
+ currency: wInfo.currency,
+ defaultExchange: wInfo.exchange,
+ list: wInfo.exchangeList,
});
+ const current =
+ selectedExchange.status !== "ready"
+ ? undefined
+ : selectedExchange.selected.exchangeBaseUrl;
+ useEffect(() => {
+ if (current) {
+ onExchangeUpdated(current);
+ }
+ }, [current]);
+
+ const safeAmount = wInfo.amount
+ ? wInfo.amount
+ : Amounts.zeroOfCurrency(wInfo.currency);
+
if (selectedExchange.status !== "ready") {
return selectedExchange;
}
@@ -341,11 +402,12 @@ function exchangeSelectionState(
| State.Loading => {
const { i18n } = useTranslationContext();
const { pushAlertOnError } = useAlertContext();
+
+ const [choosenAmount, setChoosenAmount] = useState(safeAmount);
const [ageRestricted, setAgeRestricted] = useState(0);
- const currentExchange = selectedExchange.selected;
const [selectedCurrency, setSelectedCurrency] = useState<string>(
- chosenAmount.currency,
+ wInfo.currency,
);
/**
* With the exchange and amount, ask the wallet the information
@@ -355,8 +417,8 @@ function exchangeSelectionState(
const info = await api.wallet.call(
WalletApiOperation.GetWithdrawalDetailsForAmount,
{
- exchangeBaseUrl: currentExchange.exchangeBaseUrl,
- amount: Amounts.stringify(chosenAmount),
+ exchangeBaseUrl: selectedExchange.selected.exchangeBaseUrl,
+ amount: Amounts.stringify(choosenAmount),
restrictAge: ageRestricted,
},
);
@@ -368,19 +430,40 @@ function exchangeSelectionState(
return {
amount: withdrawAmount,
+ currentExchange: selectedExchange.selected,
ageRestrictionOptions: info.ageRestrictionOptions,
accounts: info.withdrawalAccountsList,
};
- }, []);
+ }, [choosenAmount, selectedExchange.selected, ageRestricted]);
const [doingWithdraw, setDoingWithdraw] = useState<boolean>(false);
+ if (!amountHook) {
+ return { status: "loading", error: undefined };
+ }
+ if (amountHook.hasError) {
+ return {
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load the withdrawal details`,
+ amountHook,
+ ),
+ };
+ }
+ if (!amountHook.response) {
+ return { status: "loading", error: undefined };
+ }
+
+ const currentExchange = amountHook.response.currentExchange;
+
async function doWithdrawAndCheckError(): Promise<void> {
try {
setDoingWithdraw(true);
const res = await doWithdraw(
currentExchange.exchangeBaseUrl,
!ageRestricted ? undefined : ageRestricted,
+ Amounts.stringify(choosenAmount),
);
if (res.confirmTransferUrl) {
document.location.href = res.confirmTransferUrl;
@@ -395,32 +478,14 @@ function exchangeSelectionState(
setDoingWithdraw(false);
}
- if (!amountHook) {
- return { status: "loading", error: undefined };
- }
- if (amountHook.hasError) {
- return {
- status: "error",
- error: alertFromError(
- i18n,
- i18n.str`Could not load the withdrawal details`,
- amountHook,
- ),
- };
- }
- if (!amountHook.response) {
- return { status: "loading", error: undefined };
- }
-
- const withdrawalFee = Amounts.sub(
- amountHook.response.amount.raw,
- amountHook.response.amount.effective,
- ).amount;
+ const toBeSent = amountHook.response.amount.raw;
const toBeReceived = amountHook.response.amount.effective;
+ const bankFee = wInfo.bankFee;
+
const ageRestrictionOptions =
amountHook.response.ageRestrictionOptions?.reduce(
- (p, c) => ({ ...p, [c]: `under ${c}` }),
+ (p, c) => ({ ...p, [c]: i18n.str`under ${c}` }),
{} as Record<string, string>,
);
@@ -432,12 +497,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,32 +522,54 @@ function exchangeSelectionState(
const conversionInfo = !convAccount
? undefined
: {
- spec: convAccount.currencySpecification!,
- amount: Amounts.parseOrThrow(convAccount.transferAmount!),
- };
+ spec: convAccount.currencySpecification!,
+ amount: Amounts.parseOrThrow(convAccount.transferAmount!),
+ };
+
+ const amountError = Amounts.isZero(choosenAmount)
+ ? i18n.str`should be greater than zero`
+ : Amounts.cmp(choosenAmount, wInfo.maxAmount) === -1
+ ? i18n.str`choose a lower value`
+ : undefined;
return {
status: "success",
error: undefined,
- doSelectExchange: selectedExchange.doSelect,
+ doSelectExchange: {
+ onClick: wInfo.editableExchange
+ ? selectedExchange.doSelect.onClick
+ : undefined,
+ },
+ editableAmount: wInfo.editableAmount,
+ editableExchange: wInfo.editableExchange,
currentExchange,
toBeReceived,
+ toBeSent,
chooseCurrencies,
+ bankFee,
selectedCurrency,
changeCurrency: (s) => {
setSelectedCurrency(s);
},
conversionInfo,
- withdrawalFee,
- chosenAmount,
+ amount: {
+ value: choosenAmount,
+ onInput: wInfo.editableAmount
+ ? pushAlertOnError(async (v) => {
+ setChoosenAmount(v);
+ })
+ : undefined,
+ error: amountError,
+ },
talerWithdrawUri,
ageRestriction,
doWithdrawal: {
- onClick: doingWithdraw
- ? undefined
- : pushAlertOnError(doWithdrawAndCheckError),
+ onClick:
+ doingWithdraw || amountError
+ ? undefined
+ : pushAlertOnError(doWithdrawAndCheckError),
},
cancel,
};
- }, []);
+ }, [selectedExchange.selected]);
}
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx
index 29f39054f..d9b7c380e 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx
@@ -43,17 +43,25 @@ const ageRestrictionSelectField = {
export const TermsOfServiceNotYetLoaded = tests.createExample(SuccessView, {
error: undefined,
status: "success",
- chosenAmount: {
- currency: "USD",
- value: 2,
- fraction: 10000000,
+ amount: {
+ value: {
+ currency: "USD",
+ value: 2,
+ fraction: 10000000,
+ },
},
+ bankFee: {
+ currency: "EUR",
+ fraction: 0,
+ value: 1,
+ },
+
doWithdrawal: { onClick: nullFunction },
currentExchange: {
exchangeBaseUrl: "https://exchange.demo.taler.net",
tos: {},
} as Partial<ExchangeListItem> as any,
- withdrawalFee: {
+ toBeSent: {
currency: "USD",
fraction: 10000000,
value: 1,
@@ -70,34 +78,41 @@ export const TermsOfServiceNotYetLoaded = tests.createExample(SuccessView, {
export const AlreadyAborted = tests.createExample(FinalStateOperation, {
error: undefined,
status: "already-completed",
- operationState: "aborted"
+ operationState: "aborted",
});
export const AlreadySelected = tests.createExample(FinalStateOperation, {
error: undefined,
status: "already-completed",
- operationState: "selected"
+ operationState: "selected",
});
export const AlreadyConfirmed = tests.createExample(FinalStateOperation, {
error: undefined,
status: "already-completed",
- operationState: "confirmed"
+ operationState: "confirmed",
});
-
export const WithSomeFee = tests.createExample(SuccessView, {
error: undefined,
status: "success",
- chosenAmount: {
- currency: "USD",
- value: 2,
- fraction: 10000000,
+ amount: {
+ value: {
+ currency: "USD",
+ value: 2,
+ fraction: 10000000,
+ },
},
+ bankFee: {
+ currency: "EUR",
+ fraction: 0,
+ value: 1,
+ },
+
doWithdrawal: { onClick: nullFunction },
currentExchange: {
exchangeBaseUrl: "https://exchange.demo.taler.net",
tos: {},
} as Partial<ExchangeListItem> as any,
- withdrawalFee: {
+ toBeSent: {
currency: "USD",
fraction: 10000000,
value: 1,
@@ -114,17 +129,25 @@ export const WithSomeFee = tests.createExample(SuccessView, {
export const WithoutFee = tests.createExample(SuccessView, {
error: undefined,
status: "success",
- chosenAmount: {
- currency: "USD",
- value: 2,
+ amount: {
+ value: {
+ currency: "USD",
+ value: 2,
+ fraction: 0,
+ },
+ },
+ bankFee: {
+ currency: "EUR",
fraction: 0,
+ value: 1,
},
+
doWithdrawal: { onClick: nullFunction },
currentExchange: {
exchangeBaseUrl: "https://exchange.demo.taler.net",
tos: {},
} as Partial<ExchangeListItem> as any,
- withdrawalFee: {
+ toBeSent: {
currency: "USD",
fraction: 0,
value: 0,
@@ -141,17 +164,25 @@ export const WithoutFee = tests.createExample(SuccessView, {
export const EditExchangeUntouched = tests.createExample(SuccessView, {
error: undefined,
status: "success",
- chosenAmount: {
- currency: "USD",
- value: 2,
- fraction: 10000000,
+ amount: {
+ value: {
+ currency: "USD",
+ value: 2,
+ fraction: 10000000,
+ },
},
+ bankFee: {
+ currency: "EUR",
+ fraction: 0,
+ value: 1,
+ },
+
doWithdrawal: { onClick: nullFunction },
currentExchange: {
exchangeBaseUrl: "https://exchange.demo.taler.net",
tos: {},
} as Partial<ExchangeListItem> as any,
- withdrawalFee: {
+ toBeSent: {
currency: "USD",
fraction: 0,
value: 0,
@@ -168,17 +199,25 @@ export const EditExchangeUntouched = tests.createExample(SuccessView, {
export const EditExchangeModified = tests.createExample(SuccessView, {
error: undefined,
status: "success",
- chosenAmount: {
- currency: "USD",
- value: 2,
- fraction: 10000000,
+ amount: {
+ value: {
+ currency: "USD",
+ value: 2,
+ fraction: 10000000,
+ },
},
+ bankFee: {
+ currency: "EUR",
+ fraction: 0,
+ value: 1,
+ },
+
doWithdrawal: { onClick: nullFunction },
currentExchange: {
exchangeBaseUrl: "https://exchange.demo.taler.net",
tos: {},
} as Partial<ExchangeListItem> as any,
- withdrawalFee: {
+ toBeSent: {
currency: "USD",
fraction: 0,
value: 0,
@@ -196,18 +235,26 @@ export const WithAgeRestriction = tests.createExample(SuccessView, {
error: undefined,
status: "success",
ageRestriction: ageRestrictionSelectField,
- chosenAmount: {
- currency: "USD",
- value: 2,
- fraction: 10000000,
+ amount: {
+ value: {
+ currency: "USD",
+ value: 2,
+ fraction: 10000000,
+ },
+ },
+ bankFee: {
+ currency: "EUR",
+ fraction: 0,
+ value: 1,
},
+
doSelectExchange: {},
doWithdrawal: { onClick: nullFunction },
currentExchange: {
exchangeBaseUrl: "https://exchange.demo.taler.net",
tos: {},
} as Partial<ExchangeListItem> as any,
- withdrawalFee: {
+ toBeSent: {
currency: "USD",
fraction: 0,
value: 0,
@@ -223,11 +270,19 @@ export const WithAgeRestriction = tests.createExample(SuccessView, {
export const WithAlternateCurrenciesNETZBON = tests.createExample(SuccessView, {
error: undefined,
status: "success",
- chosenAmount: {
- currency: "NETZBON",
- value: 2,
- fraction: 10000000,
+ amount: {
+ value: {
+ currency: "NETZBON",
+ value: 2,
+ fraction: 10000000,
+ },
},
+ bankFee: {
+ currency: "EUR",
+ fraction: 0,
+ value: 1,
+ },
+
chooseCurrencies: ["NETZBON", "EUR"],
selectedCurrency: "NETZBON",
doWithdrawal: { onClick: nullFunction },
@@ -235,7 +290,7 @@ export const WithAlternateCurrenciesNETZBON = tests.createExample(SuccessView, {
exchangeBaseUrl: "https://exchange.netzbon.ch",
tos: {},
} as Partial<ExchangeListItem> as any,
- withdrawalFee: {
+ toBeSent: {
currency: "NETZBON",
fraction: 10000000,
value: 1,
@@ -251,30 +306,38 @@ export const WithAlternateCurrenciesNETZBON = tests.createExample(SuccessView, {
export const WithAlternateCurrenciesEURO = tests.createExample(SuccessView, {
error: undefined,
status: "success",
- chosenAmount: {
- currency: "NETZBON",
- value: 2,
- fraction: 10000000,
+ amount: {
+ value: {
+ currency: "NETZBON",
+ value: 2,
+ fraction: 10000000,
+ },
+ },
+ bankFee: {
+ currency: "EUR",
+ fraction: 0,
+ value: 1,
},
+
chooseCurrencies: ["NETZBON", "EUR"],
selectedCurrency: "EUR",
- changeCurrency: () => { },
+ changeCurrency: () => {},
conversionInfo: {
spec: {
- name: "EUR"
+ name: "EUR",
} as CurrencySpecification,
amount: {
currency: "EUR",
fraction: 10000000,
value: 1,
- }
+ },
},
doWithdrawal: { onClick: nullFunction },
currentExchange: {
exchangeBaseUrl: "https://exchange.netzbon.ch",
tos: {},
} as Partial<ExchangeListItem> as any,
- withdrawalFee: {
+ toBeSent: {
currency: "NETZBON",
fraction: 10000000,
value: 1,
@@ -290,30 +353,37 @@ export const WithAlternateCurrenciesEURO = tests.createExample(SuccessView, {
export const WithAlternateCurrenciesEURO11 = tests.createExample(SuccessView, {
error: undefined,
status: "success",
- chosenAmount: {
- currency: "NETZBON",
- value: 2,
- fraction: 10000000,
+ amount: {
+ value: {
+ currency: "NETZBON",
+ value: 2,
+ fraction: 10000000,
+ },
},
chooseCurrencies: ["NETZBON", "EUR"],
selectedCurrency: "EUR",
- changeCurrency: () => { },
+ changeCurrency: () => {},
+ bankFee: {
+ currency: "EUR",
+ fraction: 0,
+ value: 1,
+ },
conversionInfo: {
spec: {
- name: "EUR"
+ name: "EUR",
} as CurrencySpecification,
amount: {
currency: "EUR",
fraction: 10000000,
value: 2,
- }
+ },
},
doWithdrawal: { onClick: nullFunction },
currentExchange: {
exchangeBaseUrl: "https://exchange.netzbon.ch",
tos: {},
} as Partial<ExchangeListItem> as any,
- withdrawalFee: {
+ toBeSent: {
currency: "NETZBON",
fraction: 10000000,
value: 1,
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts
index f90f7bed7..5a75cb4be 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts
@@ -26,6 +26,7 @@ import {
ExchangeListItem,
ExchangeTosStatus,
ScopeType,
+ TransactionIdStr,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { expect } from "chai";
@@ -99,7 +100,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 +109,23 @@ describe("Withdraw CTA states", () => {
};
handler.addWalletCallResponse(
- WalletApiOperation.GetWithdrawalDetailsForUri,
+ WalletApiOperation.PrepareBankIntegratedWithdrawal,
undefined,
{
- status: "pending",
- operationId: "123",
- amount: "EUR:2" as AmountString,
- possibleExchanges: [],
+ transactionId: "123" as TransactionIdStr,
+ info: {
+ status: "pending",
+ operationId: "123",
+ currency: "ARS",
+ amount: "EUR:2" as AmountString,
+ possibleExchanges: [],
+ editableAmount: false,
+ editableExchange: false,
+ maxAmount: "ARS:1",
+ wireFee: "ARS:0",
+ },
},
);
- handler.addWalletCallResponse(
- WalletApiOperation.GetWithdrawalTransactionByUri,
- undefined,
- {
- transactionId: "123"
- } as any,
- );
const hookBehavior = await tests.hookBehaveLikeThis(
useComponentStateFromURI,
@@ -144,7 +146,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 +155,25 @@ 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" as TransactionIdStr,
+ info: {
+ status: "pending",
+ operationId: "123",
+ currency: "ARS",
+ amount: "ARS:2" as AmountString,
+ possibleExchanges: exchanges,
+ defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl,
+ editableAmount: false,
+ editableExchange: false,
+ maxAmount: "ARS:1",
+ wireFee: "ARS:0",
+ },
},
);
handler.addWalletCallResponse(
- WalletApiOperation.GetWithdrawalTransactionByUri,
- undefined,
- {
- transactionId: "123"
- } as any,
- );
- handler.addWalletCallResponse(
WalletApiOperation.GetWithdrawalDetailsForAmount,
undefined,
{
@@ -181,7 +184,7 @@ describe("Withdraw CTA states", () => {
scopeInfo: {
currency: "ARS",
type: ScopeType.Exchange,
- url: "http://asd"
+ url: "http://asd",
},
withdrawalAccountsList: [],
ageRestrictionOptions: [],
@@ -205,8 +208,8 @@ describe("Withdraw CTA states", () => {
if (state.status !== "success") return;
expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2"));
- expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0"));
- expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"));
+ expect(state.toBeSent).deep.equal(Amounts.parseOrThrow("ARS:2"));
+ expect(state.amount.value).deep.equal(Amounts.parseOrThrow("ARS:2"));
expect(state.doWithdrawal.onClick).not.undefined;
},
@@ -237,9 +240,14 @@ describe("Withdraw CTA states", () => {
{
status: "pending",
operationId: "123",
+ currency: "ARS",
amount: "ARS:2" as AmountString,
possibleExchanges: exchangeWithNewTos,
defaultExchangeBaseUrl: exchangeWithNewTos[0].exchangeBaseUrl,
+ editableAmount: false,
+ editableExchange: false,
+ maxAmount: "ARS:1",
+ wireFee: "ARS:0",
},
);
handler.addWalletCallResponse(
@@ -252,7 +260,7 @@ describe("Withdraw CTA states", () => {
scopeInfo: {
currency: "ARS",
type: ScopeType.Exchange,
- url: "http://asd"
+ url: "http://asd",
},
tosAccepted: false,
withdrawalAccountsList: [],
@@ -267,9 +275,14 @@ describe("Withdraw CTA states", () => {
{
status: "pending",
operationId: "123",
+ currency: "ARS",
amount: "ARS:2" as AmountString,
possibleExchanges: exchanges,
defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl,
+ editableAmount: false,
+ editableExchange: false,
+ maxAmount: "ARS:1",
+ wireFee: "ARS:0",
},
);
@@ -289,8 +302,8 @@ describe("Withdraw CTA states", () => {
if (state.status !== "success") return;
expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2"));
- expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0"));
- expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"));
+ expect(state.toBeSent).deep.equal(Amounts.parseOrThrow("ARS:2"));
+ expect(state.amount.value).deep.equal(Amounts.parseOrThrow("ARS:2"));
expect(state.doWithdrawal.onClick).not.undefined;
},
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx
index aade67835..b6a356de8 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx
@@ -19,11 +19,17 @@ import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { Amount } from "../../components/Amount.js";
import { AmountField } from "../../components/AmountField.js";
+import { EnabledBySettings } from "../../components/EnabledBySettings.js";
import { Part } from "../../components/Part.js";
import { QR } from "../../components/QR.js";
import { SelectList } from "../../components/SelectList.js";
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";
@@ -33,39 +39,111 @@ import {
getAmountWithFee,
} from "../../wallet/Transaction.js";
import { State } from "./index.js";
-import { EnabledBySettings } from "../../components/EnabledBySettings.js";
+import { Amounts } from "@gnu-taler/taler-util";
export function FinalStateOperation(state: State.AlreadyCompleted): VNode {
const { i18n } = useTranslationContext();
+ // 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>
+ &nbsp;
+ <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>&nbsp;<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>&nbsp;
+ <a target="_bank" rel="noreferrer" href={state.confirmTransferUrl}>
+ <i18n.Translate>this page</i18n.Translate>
+ </a>
+ </div>
+ </WarningBox>
+ );
}
}
export function SuccessView(state: State.Success): VNode {
const { i18n } = useTranslationContext();
- // const currentTosVersionIsAccepted =
- // state.currentExchange.tosStatus === ExchangeTosStatus.Accepted;
return (
<Fragment>
<section style={{ textAlign: "left" }}>
@@ -95,21 +173,36 @@ export function SuccessView(state: State.Success): VNode {
kind="neutral"
big
/>
- {state.chooseCurrencies.length > 0 ?
+ {state.editableAmount ? (
+ <Fragment>
+ <AmountField handler={state.amount} label={i18n.str`Amount`} />
+ </Fragment>
+ ) : undefined}
+ {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`}
@@ -118,9 +211,10 @@ export function SuccessView(state: State.Success): VNode {
conversion={state.conversionInfo?.amount}
amount={getAmountWithFee(
state.toBeReceived,
- state.chosenAmount,
+ state.toBeSent,
"credit",
)}
+ bankFee={state.bankFee}
/>
}
/>
@@ -138,7 +232,6 @@ export function SuccessView(state: State.Success): VNode {
</section>
<section>
- {/* <div> */}
<TermsOfService exchangeUrl={state.currentExchange.exchangeBaseUrl}>
<Button
variant="contained"
@@ -151,20 +244,6 @@ export function SuccessView(state: State.Success): VNode {
</i18n.Translate>
</Button>
</TermsOfService>
- {/* </div>
- <div style={{ marginTop: 20 }}>
- <Button
- variant="text"
- color="success"
-
- disabled={!state.doAbort.onClick}
- onClick={state.doAbort.onClick}
- >
- <i18n.Translate>
- Cancel
- </i18n.Translate>
- </Button>
- </div> */}
</section>
{state.talerWithdrawUri ? (
<WithdrawWithMobile talerWithdrawUri={state.talerWithdrawUri} />
@@ -202,7 +281,6 @@ function WithdrawWithMobile({
}
export function SelectAmountView({
- currency,
amount,
exchangeBaseUrl,
confirm,
diff --git a/packages/taler-wallet-webextension/src/hooks/useIsOnline.ts b/packages/taler-wallet-webextension/src/hooks/useIsOnline.ts
index 8d26bf3b6..719aa2f96 100644
--- a/packages/taler-wallet-webextension/src/hooks/useIsOnline.ts
+++ b/packages/taler-wallet-webextension/src/hooks/useIsOnline.ts
@@ -1,7 +1,21 @@
-import { codecForBoolean } from "@gnu-taler/taler-util";
-import { buildStorageKey, useMemoryStorage } from "@gnu-taler/web-util/browser";
-import { platform } from "../platform/foreground.js";
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { useMemoryStorage } from "@gnu-taler/web-util/browser";
import { useEffect } from "preact/hooks";
+import { platform } from "../platform/foreground.js";
export function useIsOnline(): boolean {
const { value, update } = useMemoryStorage("online", true);
diff --git a/packages/taler-wallet-webextension/src/i18n/ru.po b/packages/taler-wallet-webextension/src/i18n/ru.po
new file mode 100644
index 000000000..aa002c984
--- /dev/null
+++ 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 &quot; %1$s&quot;"
+msgstr ""
+"Произошла ошибка при загрузке сведений о поставщике для &quot; %1$s&quot;"
+
+#: src/wallet/ProviderDetailPage.tsx:108
+#, c-format
+msgid "There is not known provider with url &quot;%1$s&quot;."
+msgstr "Нет провайдера с url &quot;%1$s&quot;."
+
+#: 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 &nbsp; %1$s"
+msgstr "Отправить &nbsp; %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 &apos;Add Recipient&apos; 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 &nbsp; %1$s"
+msgstr "Отсканируйте QR код или &nbsp; %1$s"
+
+#: src/cta/Payment/views.tsx:346
+#, c-format
+msgid "Pay &nbsp; %1$s"
+msgstr "Заплатить &nbsp; %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&apos;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 &quot;%1$s&quot; 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 &nbsp; %1$s"
+msgstr "Принять &nbsp; %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 &nbsp; %1$s"
+msgstr "Получить &nbsp; %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&apos;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&apos;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 &nbsp; %1$s"
+msgstr "Вывести &nbsp; %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&apos;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&nbsp;%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&apos;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&apos;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/platform/chrome.ts b/packages/taler-wallet-webextension/src/platform/chrome.ts
index e63040f5c..056351e3f 100644
--- a/packages/taler-wallet-webextension/src/platform/chrome.ts
+++ b/packages/taler-wallet-webextension/src/platform/chrome.ts
@@ -732,15 +732,35 @@ function listenNetworkConnectionState(
function notifyOnline() {
notify("on");
}
- notify(window.navigator.onLine ? "on" : "off");
- window.addEventListener("offline", notifyOffline);
- window.addEventListener("online", notifyOnline);
+ function notifyChange() {
+ if (nav.onLine) {
+ notifyOnline();
+ } else {
+ notifyOnline();
+ }
+ }
+ notify(navigator.onLine ? "on" : "off");
+
+ const nav: any = navigator;
+ if (typeof nav.connection !== "undefined") {
+ nav.connection.addEventListener("change", notifyChange);
+ }
+ if (typeof window !== "undefined") {
+ window.addEventListener("offline", notifyOffline);
+ window.addEventListener("online", notifyOnline);
+ }
return () => {
- window.removeEventListener("offline", notifyOffline);
- window.removeEventListener("online", notifyOnline);
+ if (typeof nav.connection !== "undefined") {
+ nav.connection.removeEventListener("change", notifyChange);
+ }
+ if (typeof window !== "undefined") {
+ window.removeEventListener("offline", notifyOffline);
+ window.removeEventListener("online", notifyOnline);
+ }
};
}
+
function runningOnPrivateMode(): boolean {
return chrome.extension.inIncognitoContext;
}
diff --git a/packages/taler-wallet-webextension/src/platform/dev.ts b/packages/taler-wallet-webextension/src/platform/dev.ts
index d6e743147..b53e8f3c4 100644
--- a/packages/taler-wallet-webextension/src/platform/dev.ts
+++ b/packages/taler-wallet-webextension/src/platform/dev.ts
@@ -35,11 +35,11 @@ const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
keepAlive: (cb: VoidFunction) => cb(),
findTalerUriInActiveTab: async () => undefined,
findTalerUriInClipboard: async () => undefined,
- listenNetworkConnectionState,
+ listenNetworkConnectionState: () => () => undefined,
openNewURLFromPopup: () => undefined,
triggerWalletEvent: () => undefined,
setAlertedIcon: () => undefined,
- setNormalIcon : () => undefined,
+ setNormalIcon: () => undefined,
getPermissionsApi: () => ({
containsClipboardPermissions: async () => true,
removeClipboardPermissions: async () => false,
@@ -200,19 +200,3 @@ interface IframeMessageCommand {
export default api;
-function listenNetworkConnectionState(
- notify: (state: "on" | "off") => void,
-): () => void {
- function notifyOffline() {
- notify("off");
- }
- function notifyOnline() {
- notify("on");
- }
- window.addEventListener("offline", notifyOffline);
- window.addEventListener("online", notifyOnline);
- return () => {
- window.removeEventListener("offline", notifyOffline);
- window.removeEventListener("online", notifyOnline);
- };
-}
diff --git a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
index 93770312e..73bd8e96d 100644
--- a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
+++ b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
@@ -180,7 +180,7 @@ export function BalanceView(state: State.Balances): VNode {
variant="contained"
onClick={state.goToWalletManualWithdraw.onClick}
>
- <i18n.Translate>Add</i18n.Translate>
+ <i18n.Translate>Receive</i18n.Translate>
</Button>
{currencyWithNonZeroAmount.length > 0 && (
<MultiActionButton
diff --git a/packages/taler-wallet-webextension/src/wallet/Application.tsx b/packages/taler-wallet-webextension/src/wallet/Application.tsx
index 884c2eab7..893122c0f 100644
--- 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/DepositPage/index.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts
index 838739ad1..daba6aba4 100644
--- a/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts
+++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts
@@ -94,9 +94,9 @@ export namespace State {
currentAccount: PaytoUri;
totalFee: AmountJson;
- totalToDeposit: AmountJson;
amount: AmountFieldHandler;
+ totalToDeposit: AmountFieldHandler;
account: SelectFieldHandler;
cancelHandler: ButtonHandler;
depositHandler: ButtonHandler;
diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts
index 97b2ab517..b674665cf 100644
--- a/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts
+++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts
@@ -15,19 +15,18 @@
*/
import {
- AmountJson,
Amounts,
- DepositGroupFees,
KnownBankAccountsInfo,
parsePaytoUri,
PaytoUri,
stringifyPaytoUri,
+ TransactionAmountMode
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { useState } from "preact/hooks";
import { alertFromError, useAlertContext } from "../../context/alert.js";
import { useBackendContext } from "../../context/backend.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { RecursiveState } from "../../utils/index.js";
import { Props, State } from "./index.js";
@@ -83,8 +82,11 @@ export function useComponentState({
if (hook.hasError) {
return {
status: "error",
- error: alertFromError(i18n,
- i18n.str`Could not load balance information`, hook),
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load balance information`,
+ hook,
+ ),
};
}
const { accounts, balances } = hook.response;
@@ -141,21 +143,23 @@ export function useComponentState({
}
const firstAccount = accounts[0].uri;
const currentAccount = !selectedAccount ? firstAccount : selectedAccount;
-
- return () => {
- // eslint-disable-next-line react-hooks/rules-of-hooks
- const [amount, setAmount] = useState<AmountJson>(
- initialValue ?? ({} as any),
+ const zero = Amounts.zeroOfCurrency(currency)
+ return (): State => {
+ const [instructed, setInstructed] = useState(
+ {amount: initialValue ?? zero, type: TransactionAmountMode.Raw},
);
- const amountStr = Amounts.stringify(amount);
+ const amountStr = Amounts.stringify(instructed.amount);
const depositPaytoUri = stringifyPaytoUri(currentAccount);
- // eslint-disable-next-line react-hooks/rules-of-hooks
const hook = useAsyncAsHook(async () => {
- const fee = await api.wallet.call(WalletApiOperation.PrepareDeposit, {
- amount: amountStr,
- depositPaytoUri,
- });
+ const fee = await api.wallet.call(
+ WalletApiOperation.ConvertDepositAmount,
+ {
+ amount: amountStr,
+ type: instructed.type,
+ depositPaytoUri,
+ },
+ );
return { fee };
}, [amountStr, depositPaytoUri]);
@@ -183,18 +187,16 @@ export function useComponentState({
const totalFee =
fee !== undefined
- ? Amounts.sum([fee.fees.wire, fee.fees.coin, fee.fees.refresh]).amount
+ ? Amounts.sub(fee.effectiveAmount, fee.rawAmount).amount
: Amounts.zeroOfCurrency(currency);
- const totalToDeposit =
- fee !== undefined
- ? Amounts.sub(amount, totalFee).amount
- : Amounts.zeroOfCurrency(currency);
+ const totalToDeposit = Amounts.parseOrThrow(fee.rawAmount);
+ const totalEffective = Amounts.parseOrThrow(fee.effectiveAmount);
- const isDirty = amount !== initialValue;
+ const isDirty = instructed.amount !== initialValue;
const amountError = !isDirty
? undefined
- : Amounts.cmp(balance, amount) === -1
+ : Amounts.cmp(balance, totalEffective) === -1
? `Too much, your current balance is ${Amounts.stringifyValue(balance)}`
: undefined;
@@ -207,7 +209,7 @@ export function useComponentState({
if (!currency) return;
const depositPaytoUri = stringifyPaytoUri(currentAccount);
- const amountStr = Amounts.stringify(amount);
+ const amountStr = Amounts.stringify(totalEffective);
await api.wallet.call(WalletApiOperation.CreateDepositGroup, {
amount: amountStr,
depositPaytoUri,
@@ -220,8 +222,19 @@ export function useComponentState({
error: undefined,
currency,
amount: {
- value: amount,
- onInput: pushAlertOnError(async (a) => setAmount(a)),
+ value: totalEffective,
+ onInput: pushAlertOnError(async (a) => setInstructed({
+ amount: a,
+ type: TransactionAmountMode.Effective,
+ })),
+ error: amountError,
+ },
+ totalToDeposit: {
+ value: totalToDeposit,
+ onInput: pushAlertOnError(async (a) => setInstructed({
+ amount: a,
+ type: TransactionAmountMode.Raw,
+ })),
error: amountError,
},
onAddAccount: {
@@ -244,7 +257,6 @@ export function useComponentState({
onClick: unableToDeposit ? undefined : pushAlertOnError(doSend),
},
totalFee,
- totalToDeposit,
};
};
}
@@ -269,7 +281,7 @@ export function createLabelsForBankAccount(
): { [value: string]: string } {
const initialList: Record<string, string> = {};
if (!knownBankAccounts.length) return initialList;
- return knownBankAccounts.reduce((prev, cur, i) => {
+ return knownBankAccounts.reduce((prev, cur) => {
prev[stringifyPaytoUri(cur.uri)] = cur.alias;
return prev;
}, initialList);
diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx b/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx
index c23f83fdd..0ed62220b 100644
--- a/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx
@@ -53,7 +53,10 @@ export const WithNoAccountForIBAN = tests.createExample(ReadyView, {
onClick: nullFunction,
},
totalFee: Amounts.zeroOfCurrency("USD"),
- totalToDeposit: Amounts.parseOrThrow("USD:10"),
+ totalToDeposit: {
+ onInput:nullFunction,
+ value: Amounts.parseOrThrow("USD:10"),
+ },
// onCalculateFee: alwaysReturnFeeToOne,
});
@@ -82,7 +85,10 @@ export const WithIBANAccountTypeSelected = tests.createExample(ReadyView, {
onClick: nullFunction,
},
totalFee: Amounts.zeroOfCurrency("USD"),
- totalToDeposit: Amounts.parseOrThrow("USD:10"),
+ totalToDeposit: {
+ onInput:nullFunction,
+ value: Amounts.parseOrThrow("USD:10"),
+ },
// onCalculateFee: alwaysReturnFeeToOne,
});
@@ -111,6 +117,9 @@ export const NewBitcoinAccountTypeSelected = tests.createExample(ReadyView, {
onClick: nullFunction,
},
totalFee: Amounts.zeroOfCurrency("USD"),
- totalToDeposit: Amounts.parseOrThrow("USD:10"),
+ totalToDeposit: {
+ onInput:nullFunction,
+ value: Amounts.parseOrThrow("USD:10"),
+ },
// onCalculateFee: alwaysReturnFeeToOne,
});
diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts
index 157cb868a..1144095e1 100644
--- a/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts
+++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts
@@ -20,17 +20,16 @@
*/
import {
+ AmountResponse,
Amounts,
AmountString,
- DepositGroupFees,
parsePaytoUri,
- PrepareDepositResponse,
ScopeType,
- stringifyPaytoUri,
+ stringifyPaytoUri
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { expect } from "chai";
import * as tests from "@gnu-taler/web-util/testing";
+import { expect } from "chai";
import { nullFunction } from "../../mui/handlers.js";
import { createWalletApiMock } from "../../test-utils.js";
@@ -38,24 +37,14 @@ import { useComponentState } from "./state.js";
const currency = "EUR";
const amount = `${currency}:0`;
-const withoutFee = (): PrepareDepositResponse => ({
- effectiveDepositAmount: `${currency}:5` as AmountString,
- totalDepositCost: `${currency}:5` as AmountString,
- fees: {
- coin: Amounts.stringify(`${currency}:0`),
- wire: Amounts.stringify(`${currency}:0`),
- refresh: Amounts.stringify(`${currency}:0`),
- },
+const withoutFee = (value: number): AmountResponse => ({
+ effectiveAmount: `${currency}:${value}` as AmountString,
+ rawAmount: `${currency}:${value}` as AmountString,
});
-const withSomeFee = (): PrepareDepositResponse => ({
- effectiveDepositAmount: `${currency}:5` as AmountString,
- totalDepositCost: `${currency}:5` as AmountString,
- fees: {
- coin: Amounts.stringify(`${currency}:1`),
- wire: Amounts.stringify(`${currency}:1`),
- refresh: Amounts.stringify(`${currency}:1`),
- },
+const withSomeFee = (value: number, fee: number): AmountResponse => ({
+ effectiveAmount: `${currency}:${value}` as AmountString,
+ rawAmount: `${currency}:${value - fee}` as AmountString,
});
describe("DepositPage states", () => {
@@ -195,9 +184,9 @@ describe("DepositPage states", () => {
},
);
handler.addWalletCallResponse(
- WalletApiOperation.PrepareDeposit,
+ WalletApiOperation.ConvertDepositAmount,
undefined,
- withoutFee(),
+ withoutFee(0),
);
const hookBehavior = await tests.hookBehaveLikeThis(
@@ -255,15 +244,15 @@ describe("DepositPage states", () => {
},
);
handler.addWalletCallResponse(
- WalletApiOperation.PrepareDeposit,
+ WalletApiOperation.ConvertDepositAmount,
undefined,
- withoutFee(),
+ withoutFee(0),
);
handler.addWalletCallResponse(
- WalletApiOperation.PrepareDeposit,
+ WalletApiOperation.ConvertDepositAmount,
undefined,
- withoutFee(),
+ withoutFee(0),
);
const accountSelected = stringifyPaytoUri(ibanPayto.uri);
@@ -345,19 +334,19 @@ describe("DepositPage states", () => {
},
);
handler.addWalletCallResponse(
- WalletApiOperation.PrepareDeposit,
+ WalletApiOperation.ConvertDepositAmount,
undefined,
- withoutFee(),
+ withoutFee(0),
);
handler.addWalletCallResponse(
- WalletApiOperation.PrepareDeposit,
+ WalletApiOperation.ConvertDepositAmount,
undefined,
- withSomeFee(),
+ withSomeFee(10,3),
);
handler.addWalletCallResponse(
- WalletApiOperation.PrepareDeposit,
+ WalletApiOperation.ConvertDepositAmount,
undefined,
- withSomeFee(),
+ withSomeFee(10,3),
);
const accountSelected = stringifyPaytoUri(ibanPayto.uri);
@@ -404,7 +393,7 @@ describe("DepositPage states", () => {
expect(state.account.value).eq(accountSelected);
expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:10"));
expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`));
- expect(state.totalToDeposit).deep.eq(
+ expect(state.totalToDeposit.value).deep.eq(
Amounts.parseOrThrow(`${currency}:7`),
);
expect(state.depositHandler.onClick).not.undefined;
@@ -416,7 +405,7 @@ describe("DepositPage states", () => {
expect(state.account.value).eq(accountSelected);
expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:10"));
expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`));
- expect(state.totalToDeposit).deep.eq(
+ expect(state.totalToDeposit.value).deep.eq(
Amounts.parseOrThrow(`${currency}:7`),
);
expect(state.depositHandler.onClick).not.undefined;
diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx b/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx
index 908becb04..b3607ebba 100644
--- a/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx
@@ -26,7 +26,7 @@ import { Grid } from "../../mui/Grid.js";
import { State } from "./index.js";
export function AmountOrCurrencyErrorView(
- p: State.AmountOrCurrencyError,
+ _p: State.AmountOrCurrencyError,
): VNode {
const { i18n } = useTranslationContext();
@@ -145,7 +145,7 @@ export function ReadyView(state: State.Ready): VNode {
</p>
<Grid container spacing={2} columns={1}>
<Grid item xs={1}>
- <AmountField label={i18n.str`Amount`} handler={state.amount} />
+ <AmountField label={i18n.str`Brut amount`} handler={state.amount} />
</Grid>
<Grid item xs={1}>
<AmountField
@@ -156,12 +156,7 @@ export function ReadyView(state: State.Ready): VNode {
/>
</Grid>
<Grid item xs={1}>
- <AmountField
- label={i18n.str`Total deposit`}
- handler={{
- value: state.totalToDeposit,
- }}
- />
+ <AmountField label={i18n.str`Net amount`} handler={state.totalToDeposit} />
</Grid>
</Grid>
</section>
@@ -180,7 +175,7 @@ export function ReadyView(state: State.Ready): VNode {
) : (
<Button variant="contained" onClick={state.depositHandler.onClick}>
<i18n.Translate>
- Deposit&nbsp;{Amounts.stringifyValue(state.totalToDeposit)}{" "}
+ Deposit&nbsp;{Amounts.stringifyValue(state.totalToDeposit.value)}{" "}
{state.currency}
</i18n.Translate>
</Button>
diff --git a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
index 53380e263..8f23c0685 100644
--- a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
@@ -17,25 +17,31 @@
import {
AbsoluteTime,
Amounts,
- CoinDumpJson,
CoinStatus,
- ExchangeListItem,
ExchangeTosStatus,
LogLevel,
NotificationType,
ScopeType,
- parseWithdrawUri,
- stringifyWithdrawExchange,
+ stringifyWithdrawExchange
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
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,11 +50,7 @@ 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 = {
// ageKeysCount: number | undefined;
denom_value: number;
@@ -64,15 +66,7 @@ type SplitedCoinInfo = {
usable: CalculatedCoinfInfo[];
};
-export interface Props {
- // FIXME: Pending operations don't exist anymore.
-}
-
-function hashObjectId(o: any): string {
- return JSON.stringify(o);
-}
-
-export function DeveloperPage({ }: Props): VNode {
+export function DeveloperPage(): VNode {
const { i18n } = useTranslationContext();
const [downloadedDatabase, setDownloadedDatabase] = useState<
{ time: Date; content: string } | undefined
@@ -110,8 +104,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 +269,6 @@ export function DeveloperPage({ }: Props): VNode {
})}
/>
-
<SubTitle>
<i18n.Translate>Exchange Entries</i18n.Translate>
</SubTitle>
@@ -336,19 +329,32 @@ 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"
+ rel="noreferrer"
+ >
+ {e.exchangeBaseUrl}
+ </a>
</td>
<td>
{e.exchangeEntryStatus} / {e.exchangeUpdateStatus}
@@ -359,10 +365,10 @@ export function DeveloperPage({ }: Props): VNode {
<td>
{e.lastUpdateTimestamp
? AbsoluteTime.toIsoString(
- AbsoluteTime.fromPreciseTimestamp(
- e.lastUpdateTimestamp,
- ),
- )
+ AbsoluteTime.fromPreciseTimestamp(
+ e.lastUpdateTimestamp,
+ ),
+ )
: "never"}
</td>
<td>
@@ -381,31 +387,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 +418,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 +466,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/wallet/ManageAccount/views.tsx b/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx
index 7b80977f3..b995a44d0 100644
--- a/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx
@@ -130,7 +130,6 @@ export function ReadyView({
))}
</div>
<div style={{ border: "1px solid gray", padding: 8, borderRadius: 5 }}>
- --- {uri.value} ---
<p>
<CustomFieldByAccountType
type={accountType.value as AccountType}
diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
index 1f0293352..ca5bc3756 100644
--- a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
@@ -1416,9 +1416,11 @@ export function TransferPickupDetails({
export function WithdrawDetails({
conversion,
amount,
+ bankFee,
}: {
conversion?: AmountJson;
amount: AmountWithFee;
+ bankFee?: AmountJson;
}): VNode {
const { i18n } = useTranslationContext();
@@ -1481,6 +1483,16 @@ export function WithdrawDetails({
</tr>
</Fragment>
)}
+ {!bankFee ? undefined : (
+ <tr>
+ <td>
+ <i18n.Translate>Bank fee</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={bankFee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ )}
</PurchaseDetailsTable>
);
}
diff --git a/packages/taler-wallet-webextension/src/wxApi.ts b/packages/taler-wallet-webextension/src/wxApi.ts
index 4394a982f..47b466fcd 100644
--- a/packages/taler-wallet-webextension/src/wxApi.ts
+++ b/packages/taler-wallet-webextension/src/wxApi.ts
@@ -55,7 +55,7 @@ import { WalletActivityTrack } from "./wxBackend.js";
const logger = new Logger("wxApi");
-export const WALLET_CORE_SUPPORTED_VERSION = "4:0:0"
+export const WALLET_CORE_SUPPORTED_VERSION = "5:0:0"
export interface ExtendedPermissionsResponse {
newValue: boolean;
diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts
index 5fa255f5d..a0b9f2908 100644
--- a/packages/taler-wallet-webextension/src/wxBackend.ts
+++ b/packages/taler-wallet-webextension/src/wxBackend.ts
@@ -39,7 +39,7 @@ import {
makeErrorDetail,
openPromise,
setGlobalLogLevelFromString,
- setLogLevelFromString
+ setLogLevelFromString,
} from "@gnu-taler/taler-util";
import { HttpRequestLibrary } from "@gnu-taler/taler-util/http";
import {
@@ -92,7 +92,7 @@ async function resetDb(): Promise<void> {
export type WalletActivityTrack = {
id: number;
- events: (WalletNotification & {when: AbsoluteTime})[];
+ events: (WalletNotification & { when: AbsoluteTime })[];
start: AbsoluteTime;
type: NotificationType;
end: AbsoluteTime;
@@ -107,130 +107,138 @@ function getUniqueId(): number {
//FIXME: maybe circular buffer
const activity: WalletActivityTrack[] = [];
-function addNewWalletActivityNotification(list: WalletActivityTrack[], n: WalletNotification) {
- const start = AbsoluteTime.now();
- const ev = {...n, when:start};
- switch (n.type) {
+function convertWalletActivityNotification(
+ knownEvents: WalletActivityTrack[],
+ event: WalletNotification & {
+ when: AbsoluteTime;
+ },
+): WalletActivityTrack | undefined {
+ switch (event.type) {
case NotificationType.BalanceChange: {
- const groupId = `${n.type}:${n.hintTransactionId}`;
- const found = list.find((a)=>a.groupId === groupId)
+ const groupId = `${event.type}:${event.hintTransactionId}`;
+ const found = knownEvents.find((a) => a.groupId === groupId);
if (found) {
- found.end = start;
- found.events.unshift(ev)
- return;
+ found.end = event.when;
+ found.events.unshift(event);
+ return found;
}
- list.push({
+ return {
id: getUniqueId(),
- type: n.type,
- start,
+ type: event.type,
+ start: event.when,
end: AbsoluteTime.never(),
- events: [ev],
+ events: [event],
groupId,
- });
- return;
+ };
}
case NotificationType.BackupOperationError: {
const groupId = "";
- list.push({
+ return {
id: getUniqueId(),
- type: n.type,
- start,
+ type: event.type,
+ start: event.when,
end: AbsoluteTime.never(),
- events: [ev],
+ events: [event],
groupId,
- });
- return;
+ };
}
case NotificationType.TransactionStateTransition: {
- const groupId = `${n.type}:${n.transactionId}`;
- const found = list.find((a)=>a.groupId === groupId)
+ const groupId = `${event.type}:${event.transactionId}`;
+ const found = knownEvents.find((a) => a.groupId === groupId);
if (found) {
- found.end = start;
- found.events.unshift(ev)
- return;
+ found.end = event.when;
+ found.events.unshift(event);
+ return found;
}
- list.push({
+ return {
id: getUniqueId(),
- type: n.type,
- start,
+ type: event.type,
+ start: event.when,
end: AbsoluteTime.never(),
- events: [ev],
+ events: [event],
groupId,
- });
- return;
+ };
}
case NotificationType.WithdrawalOperationTransition: {
- return;
+ return undefined;
}
case NotificationType.ExchangeStateTransition: {
- const groupId = `${n.type}:${n.exchangeBaseUrl}`;
- const found = list.find((a)=>a.groupId === groupId)
+ const groupId = `${event.type}:${event.exchangeBaseUrl}`;
+ const found = knownEvents.find((a) => a.groupId === groupId);
if (found) {
- found.end = start;
- found.events.unshift(ev)
- return;
+ found.end = event.when;
+ found.events.unshift(event);
+ return found;
}
- list.push({
+ return {
id: getUniqueId(),
- type: n.type,
- start,
+ type: event.type,
+ start: event.when,
end: AbsoluteTime.never(),
- events: [ev],
+ events: [event],
groupId,
- });
- return;
+ };
}
case NotificationType.Idle: {
const groupId = "";
- list.push({
+ return({
id: getUniqueId(),
- type: n.type,
- start,
+ type: event.type,
+ start: event.when,
end: AbsoluteTime.never(),
- events: [ev],
+ events: [event],
groupId,
});
- return;
}
case NotificationType.TaskObservabilityEvent: {
- const groupId = `${n.type}:${n.taskId}`;
- const found = list.find((a)=>a.groupId === groupId)
+ const groupId = `${event.type}:${event.taskId}`;
+ const found = knownEvents.find((a) => a.groupId === groupId);
if (found) {
- found.end = start;
- found.events.unshift(ev)
- return;
+ found.end = event.when;
+ found.events.unshift(event);
+ return found;
}
- list.push({
+ return({
id: getUniqueId(),
- type: n.type,
- start,
+ type: event.type,
+ start: event.when,
end: AbsoluteTime.never(),
- events: [ev],
+ events: [event],
groupId,
});
- return;
}
case NotificationType.RequestObservabilityEvent: {
- const groupId = `${n.type}:${n.operation}:${n.requestId}`;
- const found = list.find((a)=>a.groupId === groupId)
+ const groupId = `${event.type}:${event.operation}:${event.requestId}`;
+ const found = knownEvents.find((a) => a.groupId === groupId);
if (found) {
- found.end = start;
- found.events.unshift(ev)
- return;
+ found.end = event.when;
+ found.events.unshift(event);
+ return found;
}
- list.push({
+ return({
id: getUniqueId(),
- type: n.type,
- start,
+ type: event.type,
+ start: event.when,
end: AbsoluteTime.never(),
- events: [ev],
+ events: [event],
groupId,
});
- return;
}
}
}
+function addNewWalletActivityNotification(
+ list: WalletActivityTrack[],
+ n: WalletNotification,
+) {
+ const start = AbsoluteTime.now();
+ const ev = { ...n, when: start };
+ const activity = convertWalletActivityNotification(list, ev);
+ if (activity) {
+ list.unshift(activity); // insert at start
+ }
+}
+
async function getNotifications({
filter,
}: {
diff --git a/packages/web-util/package.json b/packages/web-util/package.json
index 369b872b6..c6bf20160 100644
--- a/packages/web-util/package.json
+++ b/packages/web-util/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/web-util",
- "version": "0.10.7",
+ "version": "0.11.4",
"description": "Generic helper functionality for GNU Taler Web Apps",
"type": "module",
"types": "./lib/index.node.d.ts",
diff --git a/packages/web-util/src/components/CopyButton.tsx b/packages/web-util/src/components/CopyButton.tsx
index dbb38b474..4351da018 100644
--- a/packages/web-util/src/components/CopyButton.tsx
+++ b/packages/web-util/src/components/CopyButton.tsx
@@ -21,7 +21,7 @@ export function CopyButton({ class: clazz, children, getContent }: { children?:
const [copied, setCopied] = useState(false);
function copyText(): void {
if (!navigator.clipboard && !window.isSecureContext) {
- alert('clipboard is not available on insecure context (http)')
+ prompt("Clipboard is not available on insecure context (http).", getContent());
}
if (navigator.clipboard) {
navigator.clipboard.writeText(getContent() || "");
diff --git a/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx b/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx
index 0d54c3f69..6b792bfee 100644
--- 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
index e8683468e..647d2c823 100644
--- 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
index 1ac96437c..d90028508 100644
--- 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
index d8361718d..86d3aa926 100644
--- 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
index 3a522bf7e..eee891776 100644
--- 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
index 4bd6b4924..4c5050830 100644
--- 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
index ef9ad96e1..012499d6d 100644
--- 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 {
diff --git a/packages/web-util/src/index.build.ts b/packages/web-util/src/index.build.ts
index c0c5fc179..2260ecb9a 100644
--- a/packages/web-util/src/index.build.ts
+++ b/packages/web-util/src/index.build.ts
@@ -305,8 +305,10 @@ export function computeConfig(params: BuildParams): esbuild.BuildOptions {
/**
* Build sources for prod environment
*/
-export function build(config: BuildParams) {
- return esbuild.build(computeConfig(config));
+export async function build(config: BuildParams) {
+ const res = await esbuild.build(computeConfig(config));
+ fs.writeFileSync(`${config.destination}/version.txt`, `${_package.version}`);
+ return res;
}
const LIVE_RELOAD_SCRIPT =