summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2024-01-15 17:34:19 -0300
committerSebastian <sebasjm@gmail.com>2024-01-15 17:36:48 -0300
commit2e2cf4049a771c82fcc520686de3ace7603baa05 (patch)
tree620ab22d4fc0f621d0a574c8f98d1c49f1d67804
parentef0bb60f23c0c755814f648b8d71a29a843e066c (diff)
downloadwallet-core-2e2cf4049a771c82fcc520686de3ace7603baa05.tar.gz
wallet-core-2e2cf4049a771c82fcc520686de3ace7603baa05.tar.bz2
wallet-core-2e2cf4049a771c82fcc520686de3ace7603baa05.zip
fixes #8083
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-notify-before-tx.ts80
-rw-r--r--packages/taler-harness/src/integrationtests/testrunner.ts2
-rw-r--r--packages/taler-util/src/http-client/bank-integration.ts8
-rw-r--r--packages/taler-util/src/http-impl.node.ts2
-rw-r--r--packages/taler-util/src/http-impl.qtart.ts2
-rw-r--r--packages/taler-util/src/notifications.ts8
-rw-r--r--packages/taler-util/src/transactions-types.ts10
-rw-r--r--packages/taler-util/src/wallet-types.ts32
-rw-r--r--packages/taler-wallet-core/src/operations/transactions.ts62
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw.ts75
-rw-r--r--packages/taler-wallet-core/src/wallet-api-types.ts22
-rw-r--r--packages/taler-wallet-core/src/wallet.ts16
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/index.ts10
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/state.ts49
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx19
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/test.ts22
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx29
-rw-r--r--packages/taler-wallet-webextension/src/utils/index.ts9
-rw-r--r--packages/taler-wallet-webextension/src/wxBackend.ts12
-rw-r--r--packages/web-util/src/utils/http-impl.browser.ts19
-rw-r--r--packages/web-util/src/utils/http-impl.sw.ts19
21 files changed, 435 insertions, 72 deletions
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-notify-before-tx.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-notify-before-tx.ts
new file mode 100644
index 000000000..fc36b8fc3
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-notify-before-tx.ts
@@ -0,0 +1,80 @@
+/*
+ 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 { NotificationType, TalerCorebankApiClient } 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";
+
+/**
+ * Run test for basic, bank-integrated withdrawal.
+ */
+export async function runWithdrawalNotifyBeforeTxTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bank, exchange } =
+ await createSimpleTestkudosEnvironmentV2(t);
+
+ // Create a withdrawal operation
+
+ const bankAccessApiClient = new TalerCorebankApiClient(
+ bank.corebankApiBaseUrl,
+ );
+ const user = await bankAccessApiClient.createRandomBankUser();
+ bankAccessApiClient.setAuth(user);
+ const wop = await bankAccessApiClient.createWithdrawalOperation(
+ user.username,
+ "TESTKUDOS:10",
+ );
+
+ // Hand it to the wallet
+ const r1 = await walletClient.call(WalletApiOperation.GetWithdrawalDetailsForUri, {
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ notifyChangeFromPendingTimeoutMs: 10000
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ // Withdraw
+
+ // Abort it
+ // const api = new TalerCoreBankHttpClient(bank.corebankApiBaseUrl);
+ // const token = await api.getAuthenticationAPI(user.username).createAccessToken(user.password, {
+ // scope: "readwrite",
+ // })
+ // t.assertTrue(token.type !== "fail")
+
+ // const confirm = await api.confirmWithdrawalById({
+ // username: user.username,
+ // token: token.body.access_token,
+ // }, wop.withdrawal_id)
+ // t.assertTrue(confirm.type !== "fail")
+
+ await walletClient.waitForNotificationCond((x) => {
+ return (
+ x.type === NotificationType.WithdrawalOperationTransition &&
+ x.operationId === r1.operationId &&
+ x.state === "confirmed"
+ );
+ });
+
+ await t.shutdown();
+}
+
+runWithdrawalNotifyBeforeTxTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts
index 1d8353acf..6a8eb9504 100644
--- a/packages/taler-harness/src/integrationtests/testrunner.ts
+++ b/packages/taler-harness/src/integrationtests/testrunner.ts
@@ -97,6 +97,7 @@ import { runMultiExchangeTest } from "./test-multiexchange.js";
import { runAgeRestrictionsDepositTest } from "./test-age-restrictions-deposit.js";
import { runWithdrawalConversionTest } from "./test-withdrawal-conversion.js";
import { runPaymentDeletedTest } from "./test-payment-deleted.js";
+import { runWithdrawalNotifyBeforeTxTest } from "./test-withdrawal-notify-before-tx.js";
/**
* Test runner.
@@ -172,6 +173,7 @@ const allTests: TestMainFunction[] = [
runWalletDblessTest,
runWallettestingTest,
runWithdrawalAbortBankTest,
+ // runWithdrawalNotifyBeforeTxTest,
runWithdrawalBankIntegratedTest,
runWithdrawalFakebankTest,
runWithdrawalFeesTest,
diff --git a/packages/taler-util/src/http-client/bank-integration.ts b/packages/taler-util/src/http-client/bank-integration.ts
index 8131b36b6..757f1f897 100644
--- a/packages/taler-util/src/http-client/bank-integration.ts
+++ b/packages/taler-util/src/http-client/bank-integration.ts
@@ -1,6 +1,7 @@
import { HttpRequestLibrary, readSuccessResponseJsonOrThrow } from "../http-common.js";
import { HttpStatusCode } from "../http-status-codes.js";
import { createPlatformHttpLib } from "../http.js";
+import { LibtoolVersion } from "../libtool-version.js";
import { FailCasesByMethod, ResultByMethod, opKnownFailure, opSuccess, opUnknownFailure } from "../operation.js";
import { TalerErrorCode } from "../taler-error-codes.js";
import { codecForTalerErrorDetail } from "../wallet-types.js";
@@ -19,6 +20,8 @@ export type TalerBankIntegrationErrorsByMethod<prop extends keyof TalerBankInteg
* The API is used by the wallets.
*/
export class TalerBankIntegrationHttpClient {
+ public readonly PROTOCOL_VERSION = "3:0:3";
+
httpLib: HttpRequestLibrary;
constructor(
@@ -28,6 +31,11 @@ export class TalerBankIntegrationHttpClient {
this.httpLib = httpClient ?? createPlatformHttpLib();
}
+ isCompatible(version: string): boolean {
+ const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version)
+ return compare?.compatible ?? false
+ }
+
/**
* https://docs.taler.net/core/api-bank-integration.html#get--config
*
diff --git a/packages/taler-util/src/http-impl.node.ts b/packages/taler-util/src/http-impl.node.ts
index 5aca6e99d..8ec823eca 100644
--- a/packages/taler-util/src/http-impl.node.ts
+++ b/packages/taler-util/src/http-impl.node.ts
@@ -70,7 +70,7 @@ export class HttpLibImpl implements HttpRequestLibrary {
private requireTls = false;
constructor(args?: HttpLibArgs) {
- this.throttlingEnabled = args?.enableThrottling ?? false;
+ this.throttlingEnabled = args?.enableThrottling ?? true;
this.requireTls = args?.requireTls ?? false;
}
diff --git a/packages/taler-util/src/http-impl.qtart.ts b/packages/taler-util/src/http-impl.qtart.ts
index d4ec26bd0..73ca417f7 100644
--- a/packages/taler-util/src/http-impl.qtart.ts
+++ b/packages/taler-util/src/http-impl.qtart.ts
@@ -44,7 +44,7 @@ export class HttpLibImpl implements HttpRequestLibrary {
private requireTls = false;
constructor(args?: HttpLibArgs) {
- this.throttlingEnabled = args?.enableThrottling ?? false;
+ this.throttlingEnabled = args?.enableThrottling ?? true;
this.requireTls = args?.requireTls ?? false;
}
diff --git a/packages/taler-util/src/notifications.ts b/packages/taler-util/src/notifications.ts
index a5c971bdd..d84d3706d 100644
--- a/packages/taler-util/src/notifications.ts
+++ b/packages/taler-util/src/notifications.ts
@@ -22,6 +22,7 @@
/**
* Imports.
*/
+import { WithdrawalOperationStatus } from "./index.node.js";
import { TransactionState } from "./transactions-types.js";
import { ExchangeEntryState, TalerErrorDetail } from "./wallet-types.js";
@@ -31,6 +32,7 @@ export enum NotificationType {
PendingOperationProcessed = "pending-operation-processed",
TransactionStateTransition = "transaction-state-transition",
ExchangeStateTransition = "exchange-state-transition",
+ WithdrawalOperationTransition = "withdrawal-state-transition",
}
export interface ErrorInfoSummary {
@@ -105,10 +107,16 @@ export interface PendingOperationProcessedNotification {
id: string;
taskResultType: string;
}
+export interface WithdrawalOperationTransitionNotification {
+ type: NotificationType.WithdrawalOperationTransition;
+ operationId: string;
+ state: WithdrawalOperationStatus;
+}
export type WalletNotification =
| BalanceChangeNotification
| BackupOperationErrorNotification
| ExchangeStateTransitionNotification
| PendingOperationProcessedNotification
+ | WithdrawalOperationTransitionNotification
| TransactionStateTransitionNotification;
diff --git a/packages/taler-util/src/transactions-types.ts b/packages/taler-util/src/transactions-types.ts
index 17b56d13b..00802577a 100644
--- a/packages/taler-util/src/transactions-types.ts
+++ b/packages/taler-util/src/transactions-types.ts
@@ -708,6 +708,16 @@ export const codecForTransactionByIdRequest =
.property("transactionId", codecForString())
.build("TransactionByIdRequest");
+export interface WithdrawalTransactionByURIRequest {
+ talerWithdrawUri: string;
+}
+
+export const codecForWithdrawalTransactionByURIRequest =
+ (): Codec<WithdrawalTransactionByURIRequest> =>
+ buildCodecForObject<WithdrawalTransactionByURIRequest>()
+ .property("talerWithdrawUri", codecForString())
+ .build("WithdrawalTransactionByURIRequest");
+
export const codecForTransactionsRequest = (): Codec<TransactionsRequest> =>
buildCodecForObject<TransactionsRequest>()
.property("currency", codecOptional(codecForString()))
diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts
index 8ccc93c38..583d5dff5 100644
--- a/packages/taler-util/src/wallet-types.ts
+++ b/packages/taler-util/src/wallet-types.ts
@@ -46,7 +46,7 @@ import {
codecOptional,
renderContext,
} from "./codec.js";
-import { CurrencySpecification } from "./index.js";
+import { CurrencySpecification, WithdrawalOperationStatus } from "./index.js";
import { VersionMatchResult } from "./libtool-version.js";
import { PaytoUri } from "./payto.js";
import { AgeCommitmentProof } from "./taler-crypto.js";
@@ -71,10 +71,12 @@ import {
} from "./taler-types.js";
import {
AbsoluteTime,
+ Duration,
TalerPreciseTimestamp,
TalerProtocolDuration,
TalerProtocolTimestamp,
codecForAbsoluteTime,
+ codecForDuration,
codecForTimestamp,
} from "./time.js";
import {
@@ -575,11 +577,11 @@ export interface CoinDumpJson {
withdrawal_reserve_pub: string | undefined;
coin_status: CoinStatus;
spend_allocation:
- | {
- id: string;
- amount: AmountString;
- }
- | undefined;
+ | {
+ id: string;
+ amount: AmountString;
+ }
+ | undefined;
/**
* Information about the age restriction
*/
@@ -942,13 +944,14 @@ export interface PreparePayResultAlreadyConfirmed {
}
export interface BankWithdrawDetails {
- selectionDone: boolean;
- transferDone: boolean;
+ status: WithdrawalOperationStatus,
amount: AmountJson;
senderWire?: string;
suggestedExchange?: string;
confirmTransferUrl?: string;
wireTypes: string[];
+ operationId: string,
+ apiBaseUrl: string,
}
export interface AcceptWithdrawalResponse {
@@ -1799,6 +1802,7 @@ export const codecForApplyRefundFromPurchaseIdRequest =
export interface GetWithdrawalDetailsForUriRequest {
talerWithdrawUri: string;
restrictAge?: number;
+ notifyChangeFromPendingTimeoutMs?: number;
}
export const codecForGetWithdrawalDetailsForUri =
@@ -1806,6 +1810,7 @@ export const codecForGetWithdrawalDetailsForUri =
buildCodecForObject<GetWithdrawalDetailsForUriRequest>()
.property("talerWithdrawUri", codecForString())
.property("restrictAge", codecOptional(codecForNumber()))
+ .property("notifyChangeFromPendingTimeoutMs", codecOptional(codecForNumber()))
.build("GetWithdrawalDetailsForUriRequest");
export interface ListKnownBankAccountsRequest {
@@ -2185,6 +2190,9 @@ export interface TxIdResponse {
}
export interface WithdrawUriInfoResponse {
+ operationId: string;
+ status: WithdrawalOperationStatus,
+ confirmTransferUrl?: string;
amount: AmountString;
defaultExchangeBaseUrl?: string;
possibleExchanges: ExchangeListItem[];
@@ -2193,6 +2201,14 @@ export interface WithdrawUriInfoResponse {
export const codecForWithdrawUriInfoResponse =
(): Codec<WithdrawUriInfoResponse> =>
buildCodecForObject<WithdrawUriInfoResponse>()
+ .property("operationId", codecForString())
+ .property("confirmTransferUrl", codecOptional(codecForString()))
+ .property("status", codecForEither(
+ codecForConstString("pending"),
+ codecForConstString("selected"),
+ codecForConstString("aborted"),
+ codecForConstString("confirmed"),
+ ))
.property("amount", codecForAmountString())
.property("defaultExchangeBaseUrl", codecOptional(codecForString()))
.property("possibleExchanges", codecForList(codecForExchangeListItem()))
diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts
index 908aa540a..d93396ca5 100644
--- a/packages/taler-wallet-core/src/operations/transactions.ts
+++ b/packages/taler-wallet-core/src/operations/transactions.ts
@@ -41,7 +41,9 @@ import {
TransactionsResponse,
TransactionState,
TransactionType,
+ TransactionWithdrawal,
WalletContractData,
+ WithdrawalTransactionByURIRequest,
WithdrawalType,
} from "@gnu-taler/taler-util";
import {
@@ -520,7 +522,7 @@ function buildTransactionForPeerPullCredit(
const silentWithdrawalErrorForInvoice =
wsrOrt?.lastError &&
wsrOrt.lastError.code ===
- TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE &&
+ TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE &&
Object.values(wsrOrt.lastError.errorsPerCoin ?? {}).every((e) => {
return (
e.code === TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR &&
@@ -550,10 +552,10 @@ function buildTransactionForPeerPullCredit(
kycUrl: pullCredit.kycUrl,
...(wsrOrt?.lastError
? {
- error: silentWithdrawalErrorForInvoice
- ? undefined
- : wsrOrt.lastError,
- }
+ error: silentWithdrawalErrorForInvoice
+ ? undefined
+ : wsrOrt.lastError,
+ }
: {}),
};
}
@@ -641,7 +643,7 @@ function buildTransactionForPeerPushCredit(
function buildTransactionForBankIntegratedWithdraw(
wgRecord: WithdrawalGroupRecord,
ort?: OperationRetryRecord,
-): Transaction {
+): TransactionWithdrawal {
if (wgRecord.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated)
throw Error("");
@@ -676,7 +678,7 @@ function buildTransactionForManualWithdraw(
withdrawalGroup: WithdrawalGroupRecord,
exchangeDetails: ExchangeWireDetails,
ort?: OperationRetryRecord,
-): Transaction {
+): TransactionWithdrawal {
if (withdrawalGroup.wgInfo.withdrawalType !== WithdrawalRecordType.BankManual)
throw Error("");
@@ -948,6 +950,52 @@ async function buildTransactionForPurchase(
};
}
+export async function getWithdrawalTransactionByUri(
+ ws: InternalWalletState,
+ request: WithdrawalTransactionByURIRequest,
+): Promise<TransactionWithdrawal | undefined> {
+ return await ws.db
+ .mktx((x) => [
+ x.withdrawalGroups,
+ x.exchangeDetails,
+ x.exchanges,
+ x.operationRetries,
+ ])
+ .runReadWrite(async (tx) => {
+ const withdrawalGroupRecord = await tx.withdrawalGroups.indexes.byTalerWithdrawUri.get(
+ request.talerWithdrawUri,
+ );
+
+ if (!withdrawalGroupRecord) {
+ return undefined;
+ }
+
+ const opId = TaskIdentifiers.forWithdrawal(withdrawalGroupRecord);
+ const ort = await tx.operationRetries.get(opId);
+
+ if (
+ withdrawalGroupRecord.wgInfo.withdrawalType ===
+ WithdrawalRecordType.BankIntegrated
+ ) {
+ return buildTransactionForBankIntegratedWithdraw(
+ withdrawalGroupRecord,
+ ort,
+ );
+ }
+ const exchangeDetails = await getExchangeWireDetailsInTx(
+ tx,
+ withdrawalGroupRecord.exchangeBaseUrl,
+ );
+ if (!exchangeDetails) throw Error("not exchange details");
+
+ return buildTransactionForManualWithdraw(
+ withdrawalGroupRecord,
+ exchangeDetails,
+ ort,
+ );
+ });
+}
+
/**
* Retrieve the full event history for this wallet.
*/
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts
index 58df75964..6c7e8c37a 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -45,6 +45,7 @@ import {
LibtoolVersion,
Logger,
NotificationType,
+ TalerBankIntegrationHttpClient,
TalerError,
TalerErrorCode,
TalerErrorDetail,
@@ -556,19 +557,11 @@ export async function getBankWithdrawalInfo(
throw Error(`can't parse URL ${talerWithdrawUri}`);
}
- const configReqUrl = new URL("config", uriResult.bankIntegrationApiBaseUrl);
+ const bankApi = new TalerBankIntegrationHttpClient(uriResult.bankIntegrationApiBaseUrl, http);
- const configResp = await http.fetch(configReqUrl.href);
- const config = await readSuccessResponseJsonOrThrow(
- configResp,
- codecForIntegrationBankConfig(),
- );
+ const { body: config } = await bankApi.getConfig()
- const versionRes = LibtoolVersion.compare(
- WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
- config.version,
- );
- if (versionRes?.compatible != true) {
+ if (!bankApi.isCompatible(config.version)) {
throw TalerError.fromDetail(
TalerErrorCode.WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE,
{
@@ -579,29 +572,24 @@ export async function getBankWithdrawalInfo(
);
}
- const reqUrl = new URL(
- `withdrawal-operation/${uriResult.withdrawalOperationId}`,
- uriResult.bankIntegrationApiBaseUrl,
- );
-
- logger.info(`bank withdrawal status URL: ${reqUrl.href}}`);
+ const resp = await bankApi.getWithdrawalOperationById(uriResult.withdrawalOperationId)
- const resp = await http.fetch(reqUrl.href);
- const status = await readSuccessResponseJsonOrThrow(
- resp,
- codecForWithdrawOperationStatusResponse(),
- );
+ if (resp.type === "fail") {
+ throw TalerError.fromUncheckedDetail(resp.detail);
+ }
+ const { body: status } = resp
logger.info(`bank withdrawal operation status: ${j2s(status)}`);
return {
+ operationId: uriResult.withdrawalOperationId,
+ apiBaseUrl: uriResult.bankIntegrationApiBaseUrl,
amount: Amounts.parseOrThrow(status.amount),
confirmTransferUrl: status.confirm_transfer_url,
- selectionDone: status.selection_done,
senderWire: status.sender_wire,
suggestedExchange: status.suggested_exchange,
- transferDone: status.transfer_done,
wireTypes: status.wire_types,
+ status: status.status,
};
}
@@ -1226,8 +1214,7 @@ export async function updateWithdrawalDenoms(
denom.verificationStatus === DenominationVerificationStatus.Unverified
) {
logger.trace(
- `Validating denomination (${current + 1}/${
- denominations.length
+ `Validating denomination (${current + 1}/${denominations.length
}) signature of ${denom.denomPubHash}`,
);
let valid = false;
@@ -1872,7 +1859,7 @@ export async function getExchangeWithdrawalInfo(
) {
logger.warn(
`wallet's support for exchange protocol version ${WALLET_EXCHANGE_PROTOCOL_VERSION} might be outdated ` +
- `(exchange has ${exchange.protocolVersionRange}), checking for updates`,
+ `(exchange has ${exchange.protocolVersionRange}), checking for updates`,
);
}
}
@@ -1915,6 +1902,7 @@ export async function getExchangeWithdrawalInfo(
export interface GetWithdrawalDetailsForUriOpts {
restrictAge?: number;
+ notifyChangeFromPendingTimeoutMs?: number;
}
/**
@@ -1957,7 +1945,40 @@ export async function getWithdrawalDetailsForUri(
);
});
+ if (info.status === "pending" && opts.notifyChangeFromPendingTimeoutMs !== undefined) {
+ const bankApi = new TalerBankIntegrationHttpClient(info.apiBaseUrl, ws.http);
+ console.log(
+ `waiting operation (${info.operationId}) to change from pending`,
+ );
+ bankApi.getWithdrawalOperationById(info.operationId, {
+ old_state: "pending",
+ timeoutMs: opts.notifyChangeFromPendingTimeoutMs
+ }).then(resp => {
+ console.log(
+ `operation (${info.operationId}) to change to ${JSON.stringify(resp, undefined, 2)}`,
+ );
+ if (resp.type === "fail") {
+ //not found, this is rare since the previous request succeed
+ ws.notify({
+ type: NotificationType.WithdrawalOperationTransition,
+ operationId: info.operationId,
+ state: info.status,
+ })
+ return;
+ }
+
+ ws.notify({
+ type: NotificationType.WithdrawalOperationTransition,
+ operationId: info.operationId,
+ state: resp.body.status,
+ });
+ })
+ }
+
return {
+ operationId: info.operationId,
+ confirmTransferUrl: info.confirmTransferUrl,
+ status: info.status,
amount: Amounts.stringify(info.amount),
defaultExchangeBaseUrl: info.suggestedExchange,
possibleExchanges,
diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts
index 7ac347b6d..7d3dc86a3 100644
--- a/packages/taler-wallet-core/src/wallet-api-types.ts
+++ b/packages/taler-wallet-core/src/wallet-api-types.ts
@@ -128,6 +128,8 @@ import {
WalletCurrencyInfo,
WithdrawTestBalanceRequest,
WithdrawUriInfoResponse,
+ WithdrawalTransactionByURIRequest,
+ TransactionWithdrawal,
} from "@gnu-taler/taler-util";
import {
AddBackupProviderRequest,
@@ -154,6 +156,7 @@ export enum WalletApiOperation {
AddExchange = "addExchange",
GetTransactions = "getTransactions",
GetTransactionById = "getTransactionById",
+ GetWithdrawalTransactionByUri = "getWithdrawalTransactionByUri",
TestingGetSampleTransactions = "testingGetSampleTransactions",
ListExchanges = "listExchanges",
GetExchangeEntryByUrl = "getExchangeEntryByUrl",
@@ -377,6 +380,12 @@ export type GetTransactionByIdOp = {
response: Transaction;
};
+export type GetWithdrawalTransactionByUriOp = {
+ op: WalletApiOperation.GetWithdrawalTransactionByUri;
+ request: WithdrawalTransactionByURIRequest;
+ response: TransactionWithdrawal | undefined;
+};
+
export type RetryPendingNowOp = {
op: WalletApiOperation.RetryPendingNow;
request: EmptyObject;
@@ -1124,6 +1133,7 @@ export type WalletOperations = {
[WalletApiOperation.GetTransactions]: GetTransactionsOp;
[WalletApiOperation.TestingGetSampleTransactions]: TestingGetSampleTransactionsOp;
[WalletApiOperation.GetTransactionById]: GetTransactionByIdOp;
+ [WalletApiOperation.GetWithdrawalTransactionByUri]: GetWithdrawalTransactionByUriOp;
[WalletApiOperation.RetryPendingNow]: RetryPendingNowOp;
[WalletApiOperation.GetPendingOperations]: GetPendingTasksOp;
[WalletApiOperation.GetUserAttentionRequests]: GetUserAttentionRequests;
@@ -1219,10 +1229,10 @@ type Primitives = string | number | boolean;
type RecursivePartial<T extends object> = {
[P in keyof T]?: T[P] extends Array<infer U extends object>
- ? Array<RecursivePartial<U>>
- : T[P] extends Array<infer J extends Primitives>
- ? Array<J>
- : T[P] extends object
- ? RecursivePartial<T[P]>
- : T[P];
+ ? Array<RecursivePartial<U>>
+ : T[P] extends Array<infer J extends Primitives>
+ ? Array<J>
+ : T[P] extends object
+ ? RecursivePartial<T[P]>
+ : T[P];
} & object;
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index d6da2250a..1a876b2c8 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -264,6 +264,7 @@ import {
failTransaction,
getTransactionById,
getTransactions,
+ getWithdrawalTransactionByUri,
parseTransactionIdentifier,
resumeTransaction,
retryTransaction,
@@ -725,9 +726,9 @@ async function dumpCoins(ws: InternalWalletState): Promise<CoinDumpJson> {
ageCommitmentProof: c.ageCommitmentProof,
spend_allocation: c.spendAllocation
? {
- amount: c.spendAllocation.amount,
- id: c.spendAllocation.id,
- }
+ amount: c.spendAllocation.amount,
+ id: c.spendAllocation.id,
+ }
: undefined,
});
}
@@ -938,6 +939,10 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
const req = codecForTransactionByIdRequest().decode(payload);
return await getTransactionById(ws, req);
}
+ case WalletApiOperation.GetWithdrawalTransactionByUri: {
+ const req = codecForGetWithdrawalDetailsForUri().decode(payload);
+ return await getWithdrawalTransactionByUri(ws, req);
+ }
case WalletApiOperation.AddExchange: {
const req = codecForAddExchangeRequest().decode(payload);
await fetchFreshExchange(ws, req.exchangeBaseUrl, {
@@ -997,7 +1002,10 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
}
case WalletApiOperation.GetWithdrawalDetailsForUri: {
const req = codecForGetWithdrawalDetailsForUri().decode(payload);
- return await getWithdrawalDetailsForUri(ws, req.talerWithdrawUri);
+ return await getWithdrawalDetailsForUri(ws, req.talerWithdrawUri, {
+ notifyChangeFromPendingTimeoutMs: req.notifyChangeFromPendingTimeoutMs,
+ restrictAge: req.restrictAge,
+ });
}
case WalletApiOperation.AcceptManualWithdrawal: {
const req = codecForAcceptManualWithdrawalRequet().decode(payload);
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts
index 04713f3c4..1f8745a5d 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts
@@ -38,7 +38,7 @@ import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { ErrorAlert } from "../../context/alert.js";
import { ExchangeSelectionPage } from "../../wallet/ExchangeSelection/index.js";
import { NoExchangesView } from "../../wallet/ExchangeSelection/views.js";
-import { SelectAmountView, SuccessView } from "./views.js";
+import { FinalStateOperation, SelectAmountView, SuccessView } from "./views.js";
export interface PropsFromURI {
talerWithdrawUri: string | undefined;
@@ -60,6 +60,7 @@ export type State =
| SelectExchangeState.NoExchangeFound
| SelectExchangeState.Selecting
| State.SelectAmount
+ | State.AlreadyCompleted
| State.Success;
export namespace State {
@@ -80,6 +81,12 @@ export namespace State {
amount: AmountFieldHandler;
currency: string;
}
+ export interface AlreadyCompleted {
+ status: "already-completed";
+ operationState: "confirmed" | "aborted" | "selected";
+ confirmTransferUrl?: string,
+ error: undefined;
+ }
export type Success = {
status: "success";
@@ -116,6 +123,7 @@ const viewMapping: StateViewMap<State> = {
"no-exchange-found": NoExchangesView,
"selecting-exchange": ExchangeSelectionPage,
success: SuccessView,
+ "already-completed": FinalStateOperation,
};
export const WithdrawPageFromURI = compose(
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts
index 7bff13e51..bf460834d 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts
@@ -21,11 +21,12 @@ import {
ExchangeFullDetails,
ExchangeListItem,
ExchangeTosStatus,
+ NotificationType,
TalerError,
parseWithdrawExchangeUri,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { useEffect, useState } from "preact/hooks";
+import { useEffect, useState, useMemo, useCallback } from "preact/hooks";
import { alertFromError, useAlertContext } from "../../context/alert.js";
import { useBackendContext } from "../../context/backend.js";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
@@ -208,17 +209,40 @@ export function useComponentStateFromURI({
WalletApiOperation.GetWithdrawalDetailsForUri,
{
talerWithdrawUri,
+ notifyChangeFromPendingTimeoutMs: 30 * 1000
},
);
- const { amount, defaultExchangeBaseUrl, possibleExchanges } = uriInfo;
+ 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,
};
});
+ const readyToListen = uriInfoHook && !uriInfoHook.hasError
+
+ useEffect(() => {
+ if (!uriInfoHook) {
+ return;
+ }
+ return api.listener.onUpdateNotification(
+ [NotificationType.WithdrawalOperationTransition],
+ () => {
+ uriInfoHook.retry()
+ },
+ );
+ }, [readyToListen]);
+
if (!uriInfoHook) return { status: "loading", error: undefined };
if (uriInfoHook.hasError) {
@@ -257,8 +281,20 @@ export function useComponentStateFromURI({
};
}
- return () =>
- exchangeSelectionState(
+ if (uriInfoHook.response.status !== "pending") {
+ if (uriInfoHook.response.transaction) {
+ onSuccess(uriInfoHook.response.transaction.transactionId)
+ }
+ return {
+ status: "already-completed",
+ operationState: uriInfoHook.response.status,
+ confirmTransferUrl: uriInfoHook.response.confirmTransferUrl,
+ error: undefined,
+ }
+ }
+
+ return useCallback(() => {
+ return exchangeSelectionState(
doManagedWithdraw,
cancel,
onSuccess,
@@ -267,6 +303,7 @@ export function useComponentStateFromURI({
exchangeList,
defaultExchange,
);
+ }, [])
}
type ManualOrManagedWithdrawFunction = (
@@ -294,7 +331,7 @@ function exchangeSelectionState(
return selectedExchange;
}
- return (): State.Success | State.LoadingUriError | State.Loading => {
+ return useCallback((): State.Success | State.LoadingUriError | State.Loading => {
const { i18n } = useTranslationContext();
const { pushAlertOnError } = useAlertContext();
const [ageRestricted, setAgeRestricted] = useState(0);
@@ -428,5 +465,5 @@ function exchangeSelectionState(
},
cancel,
};
- };
+ }, []);
}
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx
index a3127fafc..29f39054f 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx
@@ -23,7 +23,7 @@ import { CurrencySpecification, ExchangeListItem } from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import { nullFunction } from "../../mui/handlers.js";
// import { TermsState } from "../../utils/index.js";
-import { SuccessView } from "./views.js";
+import { SuccessView, FinalStateOperation } from "./views.js";
export default {
title: "withdraw",
@@ -67,6 +67,23 @@ export const TermsOfServiceNotYetLoaded = tests.createExample(SuccessView, {
chooseCurrencies: [],
});
+export const AlreadyAborted = tests.createExample(FinalStateOperation, {
+ error: undefined,
+ status: "already-completed",
+ operationState: "aborted"
+});
+export const AlreadySelected = tests.createExample(FinalStateOperation, {
+ error: undefined,
+ status: "already-completed",
+ operationState: "selected"
+});
+export const AlreadyConfirmed = tests.createExample(FinalStateOperation, {
+ error: undefined,
+ status: "already-completed",
+ operationState: "confirmed"
+});
+
+
export const WithSomeFee = tests.createExample(SuccessView, {
error: undefined,
status: "success",
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts
index 3493415d9..f90f7bed7 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts
@@ -111,10 +111,19 @@ describe("Withdraw CTA states", () => {
WalletApiOperation.GetWithdrawalDetailsForUri,
undefined,
{
+ status: "pending",
+ operationId: "123",
amount: "EUR:2" as AmountString,
possibleExchanges: [],
},
);
+ handler.addWalletCallResponse(
+ WalletApiOperation.GetWithdrawalTransactionByUri,
+ undefined,
+ {
+ transactionId: "123"
+ } as any,
+ );
const hookBehavior = await tests.hookBehaveLikeThis(
useComponentStateFromURI,
@@ -147,12 +156,21 @@ describe("Withdraw CTA states", () => {
WalletApiOperation.GetWithdrawalDetailsForUri,
undefined,
{
+ status: "pending",
+ operationId: "123",
amount: "ARS:2" as AmountString,
possibleExchanges: exchanges,
defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl,
},
);
handler.addWalletCallResponse(
+ WalletApiOperation.GetWithdrawalTransactionByUri,
+ undefined,
+ {
+ transactionId: "123"
+ } as any,
+ );
+ handler.addWalletCallResponse(
WalletApiOperation.GetWithdrawalDetailsForAmount,
undefined,
{
@@ -217,6 +235,8 @@ describe("Withdraw CTA states", () => {
WalletApiOperation.GetWithdrawalDetailsForUri,
undefined,
{
+ status: "pending",
+ operationId: "123",
amount: "ARS:2" as AmountString,
possibleExchanges: exchangeWithNewTos,
defaultExchangeBaseUrl: exchangeWithNewTos[0].exchangeBaseUrl,
@@ -245,6 +265,8 @@ describe("Withdraw CTA states", () => {
WalletApiOperation.GetWithdrawalDetailsForUri,
undefined,
{
+ status: "pending",
+ operationId: "123",
amount: "ARS:2" as AmountString,
possibleExchanges: exchanges,
defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl,
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx
index 748b65817..bd9f75696 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx
@@ -21,7 +21,7 @@ import { Amount } from "../../components/Amount.js";
import { Part } from "../../components/Part.js";
import { QR } from "../../components/QR.js";
import { SelectList } from "../../components/SelectList.js";
-import { Input, LinkSuccess, SvgIcon } from "../../components/styled/index.js";
+import { Input, LinkSuccess, SvgIcon, WarningBox } from "../../components/styled/index.js";
import { TermsOfService } from "../../components/TermsOfService/index.js";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Button } from "../../mui/Button.js";
@@ -35,6 +35,33 @@ import { State } from "./index.js";
import { Grid } from "../../mui/Grid.js";
import { AmountField } from "../../components/AmountField.js";
+export function FinalStateOperation(state: State.AlreadyCompleted): VNode {
+ const { i18n } = useTranslationContext();
+
+ 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>
+ }
+}
+
export function SuccessView(state: State.Success): VNode {
const { i18n } = useTranslationContext();
// const currentTosVersionIsAccepted =
diff --git a/packages/taler-wallet-webextension/src/utils/index.ts b/packages/taler-wallet-webextension/src/utils/index.ts
index ad4eabf15..d83e6f472 100644
--- a/packages/taler-wallet-webextension/src/utils/index.ts
+++ b/packages/taler-wallet-webextension/src/utils/index.ts
@@ -15,6 +15,7 @@
*/
import { createElement, VNode } from "preact";
+import { useCallback, useMemo } from "preact/hooks";
function getJsonIfOk(r: Response): Promise<any> {
if (r.ok) {
@@ -26,8 +27,7 @@ function getJsonIfOk(r: Response): Promise<any> {
}
throw new Error(
- `Try another server: (${r.status}) ${
- r.statusText || "internal server error"
+ `Try another server: (${r.status}) ${r.statusText || "internal server error"
}`,
);
}
@@ -89,6 +89,7 @@ export function compose<SType extends { status: string }, PType>(
): (p: PType) => VNode {
function withHook(stateHook: () => RecursiveState<SType>): () => VNode {
function TheComponent(): VNode {
+ //if the function is the same, do not compute
const state = stateHook();
if (typeof state === "function") {
@@ -102,7 +103,9 @@ export function compose<SType extends { status: string }, PType>(
}
// TheComponent.name = `${name}`;
- return TheComponent;
+ return useMemo(() => {
+ return TheComponent
+ }, [stateHook]);
}
return (p: PType) => {
diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts
index a194de0ff..1ecd66f05 100644
--- a/packages/taler-wallet-webextension/src/wxBackend.ts
+++ b/packages/taler-wallet-webextension/src/wxBackend.ts
@@ -272,11 +272,15 @@ async function reinitWallet(): Promise<void> {
let timer;
if (platform.useServiceWorkerAsBackgroundProcess()) {
- httpLib = new ServiceWorkerHttpLib();
+ httpLib = new ServiceWorkerHttpLib({
+ // enableThrottling: false,
+ });
cryptoWorker = new SynchronousCryptoWorkerFactoryPlain();
timer = new SetTimeoutTimerAPI();
} else {
- httpLib = new BrowserHttpLib();
+ httpLib = new BrowserHttpLib({
+ // enableThrottling: false,
+ });
// We could (should?) use the BrowserCryptoWorkerFactory here,
// but right now we don't, to have less platform differences.
// cryptoWorker = new BrowserCryptoWorkerFactory();
@@ -409,9 +413,9 @@ async function toggleHeaderListener(
platform.registerTalerHeaderListener();
return { newValue: true };
} catch (e) {
- logger.error("FAIL to toggle",e)
+ logger.error("FAIL to toggle", e)
}
- return { newValue: false }
+ return { newValue: false }
}
const rem = await platform.getPermissionsApi().removeHostPermissions();
diff --git a/packages/web-util/src/utils/http-impl.browser.ts b/packages/web-util/src/utils/http-impl.browser.ts
index 18140ef13..5d65c3903 100644
--- a/packages/web-util/src/utils/http-impl.browser.ts
+++ b/packages/web-util/src/utils/http-impl.browser.ts
@@ -33,6 +33,7 @@ import {
getDefaultHeaders,
encodeBody,
DEFAULT_REQUEST_TIMEOUT_MS,
+ HttpLibArgs,
} from "@gnu-taler/taler-util/http";
const logger = new Logger("browserHttpLib");
@@ -44,6 +45,12 @@ const logger = new Logger("browserHttpLib");
export class BrowserHttpLib implements HttpRequestLibrary {
private throttle = new RequestThrottler();
private throttlingEnabled = true;
+ private requireTls = false;
+
+ constructor(args?: HttpLibArgs) {
+ this.throttlingEnabled = args?.enableThrottling ?? true;
+ this.requireTls = args?.requireTls ?? false;
+ }
fetch(
requestUrl: string,
@@ -55,8 +62,8 @@ export class BrowserHttpLib implements HttpRequestLibrary {
const requestTimeout =
options?.timeout ?? Duration.fromMilliseconds(DEFAULT_REQUEST_TIMEOUT_MS);
+ const parsedUrl = new URL(requestUrl);
if (this.throttlingEnabled && this.throttle.applyThrottle(requestUrl)) {
- const parsedUrl = new URL(requestUrl);
throw TalerError.fromDetail(
TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
{
@@ -67,6 +74,16 @@ export class BrowserHttpLib implements HttpRequestLibrary {
`request to origin ${parsedUrl.origin} was throttled`,
);
}
+ if (this.requireTls && parsedUrl.protocol !== "https:") {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_NETWORK_ERROR,
+ {
+ requestMethod: requestMethod,
+ requestUrl: requestUrl,
+ },
+ `request to ${parsedUrl.origin} is not possible with protocol ${parsedUrl.protocol}`,
+ );
+ }
let myBody: ArrayBuffer | undefined =
requestMethod === "POST" || requestMethod === "PUT" || requestMethod === "PATCH"
diff --git a/packages/web-util/src/utils/http-impl.sw.ts b/packages/web-util/src/utils/http-impl.sw.ts
index 3c269e695..2ae4ccd86 100644
--- a/packages/web-util/src/utils/http-impl.sw.ts
+++ b/packages/web-util/src/utils/http-impl.sw.ts
@@ -27,6 +27,7 @@ import {
import {
DEFAULT_REQUEST_TIMEOUT_MS,
Headers,
+ HttpLibArgs,
HttpRequestLibrary,
HttpRequestOptions,
HttpResponse,
@@ -41,6 +42,12 @@ import {
export class ServiceWorkerHttpLib implements HttpRequestLibrary {
private throttle = new RequestThrottler();
private throttlingEnabled = true;
+ private requireTls = false;
+
+ public constructor(args?: HttpLibArgs) {
+ this.throttlingEnabled = args?.enableThrottling ?? true;
+ this.requireTls = args?.requireTls ?? false;
+ }
async fetch(
requestUrl: string,
@@ -52,8 +59,8 @@ export class ServiceWorkerHttpLib implements HttpRequestLibrary {
const requestTimeout =
options?.timeout ?? Duration.fromMilliseconds(DEFAULT_REQUEST_TIMEOUT_MS);
+ const parsedUrl = new URL(requestUrl);
if (this.throttlingEnabled && this.throttle.applyThrottle(requestUrl)) {
- const parsedUrl = new URL(requestUrl);
throw TalerError.fromDetail(
TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
{
@@ -64,6 +71,16 @@ export class ServiceWorkerHttpLib implements HttpRequestLibrary {
`request to origin ${parsedUrl.origin} was throttled`,
);
}
+ if (this.requireTls && parsedUrl.protocol !== "https:") {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_NETWORK_ERROR,
+ {
+ requestMethod: requestMethod,
+ requestUrl: requestUrl,
+ },
+ `request to ${parsedUrl.origin} is not possible with protocol ${parsedUrl.protocol}`,
+ );
+ }
let myBody: ArrayBuffer | undefined =
requestMethod === "POST" ? encodeBody(requestBody) : undefined;