summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/taler-util/src/walletTypes.ts28
-rw-r--r--packages/taler-wallet-cli/src/index.ts24
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/harness.ts61
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/helpers.ts33
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-withdrawal-fakebank.ts96
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-withdrawal-manual.ts4
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/testrunner.ts4
-rw-r--r--packages/taler-wallet-core/src/wallet-api-types.ts13
-rw-r--r--packages/taler-wallet-core/src/wallet.ts73
9 files changed, 304 insertions, 32 deletions
diff --git a/packages/taler-util/src/walletTypes.ts b/packages/taler-util/src/walletTypes.ts
index 63ece1e60..6e68ee080 100644
--- a/packages/taler-util/src/walletTypes.ts
+++ b/packages/taler-util/src/walletTypes.ts
@@ -590,11 +590,11 @@ export interface GetExchangeTosResult {
* if any.
*/
acceptedEtag: string | undefined;
-
+
/**
* Accepted content type
*/
- contentType: string;
+ contentType: string;
}
export interface TestPayArgs {
@@ -658,9 +658,9 @@ export interface GetExchangeTosRequest {
export const codecForGetExchangeTosRequest = (): Codec<GetExchangeTosRequest> =>
buildCodecForObject<GetExchangeTosRequest>()
- .property("exchangeBaseUrl", codecForString())
- .property("acceptedFormat", codecOptional(codecForList(codecForString())))
- .build("GetExchangeTosRequest");
+ .property("exchangeBaseUrl", codecForString())
+ .property("acceptedFormat", codecOptional(codecForList(codecForString())))
+ .build("GetExchangeTosRequest");
export interface AcceptManualWithdrawalRequest {
exchangeBaseUrl: string;
@@ -734,7 +734,10 @@ export const codecForGetExchangeWithdrawalInfo = (): Codec<GetExchangeWithdrawal
buildCodecForObject<GetExchangeWithdrawalInfo>()
.property("exchangeBaseUrl", codecForString())
.property("amount", codecForAmountJson())
- .property("tosAcceptedFormat", codecOptional(codecForList(codecForString())))
+ .property(
+ "tosAcceptedFormat",
+ codecOptional(codecForList(codecForString())),
+ )
.build("GetExchangeWithdrawalInfo");
export interface AbortProposalRequest {
@@ -1029,3 +1032,16 @@ export const codecForSetWalletDeviceIdRequest = (): Codec<SetWalletDeviceIdReque
buildCodecForObject<SetWalletDeviceIdRequest>()
.property("walletDeviceId", codecForString())
.build("SetWalletDeviceIdRequest");
+
+export interface WithdrawFakebankRequest {
+ amount: AmountString;
+ exchange: string;
+ bank: string;
+}
+
+export const codecForWithdrawFakebankRequest = (): Codec<WithdrawFakebankRequest> =>
+ buildCodecForObject<WithdrawFakebankRequest>()
+ .property("amount", codecForAmountString())
+ .property("bank", codecForString())
+ .property("exchange", codecForString())
+ .build("WithdrawFakebankRequest");
diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts
index 0985ba884..a5e129d92 100644
--- a/packages/taler-wallet-cli/src/index.ts
+++ b/packages/taler-wallet-cli/src/index.ts
@@ -635,6 +635,29 @@ const advancedCli = walletCli.subcommand("advancedArgs", "advanced", {
});
advancedCli
+ .subcommand("withdrawFakebank", "withdraw-fakebank", {
+ help: "Withdraw via a fakebank.",
+ })
+ .requiredOption("exchange", ["--exchange"], clk.STRING, {
+ help: "Base URL of the exchange to use",
+ })
+ .requiredOption("amount", ["--amount"], clk.STRING, {
+ help: "Amount to withdraw (before fees)."
+ })
+ .requiredOption("bank", ["--bank"], clk.STRING, {
+ help: "Base URL of the Taler fakebank service.",
+ })
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ await wallet.client.call(WalletApiOperation.WithdrawFakebank, {
+ amount: args.withdrawFakebank.amount,
+ bank: args.withdrawFakebank.bank,
+ exchange: args.withdrawFakebank.exchange,
+ });
+ });
+ });
+
+advancedCli
.subcommand("manualWithdrawalDetails", "manual-withdrawal-details", {
help: "Query withdrawal fees.",
})
@@ -1064,6 +1087,5 @@ export function main() {
logger.warn("Allowing withdrawal of late denominations for debugging");
walletCoreDebugFlags.denomselAllowLate = true;
}
- logger.trace(`running wallet-cli with`, process.argv);
walletCli.run();
}
diff --git a/packages/taler-wallet-cli/src/integrationtests/harness.ts b/packages/taler-wallet-cli/src/integrationtests/harness.ts
index a3a6e9e1c..6644e567f 100644
--- a/packages/taler-wallet-cli/src/integrationtests/harness.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/harness.ts
@@ -395,6 +395,11 @@ export interface BankConfig {
maxDebt?: string;
}
+export interface FakeBankConfig {
+ currency: string;
+ httpPort: number;
+}
+
function setTalerPaths(config: Configuration, home: string) {
config.setString("paths", "taler_home", home);
// We need to make sure that the path of taler_runtime_dir isn't too long,
@@ -714,6 +719,62 @@ export class BankService implements BankServiceInterface {
}
}
+export class FakeBankService {
+ proc: ProcessWrapper | undefined;
+
+ static fromExistingConfig(gc: GlobalTestState): FakeBankService {
+ const cfgFilename = gc.testDir + "/bank.conf";
+ console.log("reading fakebank config from", cfgFilename);
+ const config = Configuration.load(cfgFilename);
+ const bc: FakeBankConfig = {
+ currency: config.getString("taler", "currency").required(),
+ httpPort: config.getNumber("bank", "http_port").required(),
+ };
+ return new FakeBankService(gc, bc, cfgFilename);
+ }
+
+ static async create(
+ gc: GlobalTestState,
+ bc: FakeBankConfig,
+ ): Promise<FakeBankService> {
+ const config = new Configuration();
+ setTalerPaths(config, gc.testDir + "/talerhome");
+ config.setString("taler", "currency", bc.currency);
+ config.setString("bank", "http_port", `${bc.httpPort}`);
+ const cfgFilename = gc.testDir + "/bank.conf";
+ config.write(cfgFilename);
+ return new FakeBankService(gc, bc, cfgFilename);
+ }
+
+ get baseUrl(): string {
+ return `http://localhost:${this.bankConfig.httpPort}/`;
+ }
+
+ get port() {
+ return this.bankConfig.httpPort;
+ }
+
+ private constructor(
+ private globalTestState: GlobalTestState,
+ private bankConfig: FakeBankConfig,
+ private configFile: string,
+ ) {}
+
+ async start(): Promise<void> {
+ this.proc = this.globalTestState.spawnService(
+ "taler-fakebank-run",
+ ["-c", this.configFile],
+ "fakebank",
+ );
+ }
+
+ async pingUntilAvailable(): Promise<void> {
+ // Fakebank doesn't have "/config", so we ping just "/".
+ const url = `http://localhost:${this.bankConfig.httpPort}/`;
+ await pingProc(this.proc, url, "bank");
+ }
+}
+
export interface BankUser {
username: string;
password: string;
diff --git a/packages/taler-wallet-cli/src/integrationtests/helpers.ts b/packages/taler-wallet-cli/src/integrationtests/helpers.ts
index 1fdc36788..3b4e1643f 100644
--- a/packages/taler-wallet-cli/src/integrationtests/helpers.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/helpers.ts
@@ -353,13 +353,22 @@ export async function makeTestPayment(
const { wallet, merchant } = args;
const instance = args.instance ?? "default";
- const orderResp = await MerchantPrivateApi.createOrder(merchant, instance, {
- order: args.order,
- }, auth);
+ const orderResp = await MerchantPrivateApi.createOrder(
+ merchant,
+ instance,
+ {
+ order: args.order,
+ },
+ auth,
+ );
- let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
- orderId: orderResp.order_id,
- }, auth);
+ let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(
+ merchant,
+ {
+ orderId: orderResp.order_id,
+ },
+ auth,
+ );
t.assertTrue(orderStatus.order_status === "unpaid");
@@ -384,10 +393,14 @@ export async function makeTestPayment(
// Check if payment was successful.
- orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
- orderId: orderResp.order_id,
- instance,
- }, auth);
+ orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(
+ merchant,
+ {
+ orderId: orderResp.order_id,
+ instance,
+ },
+ auth,
+ );
t.assertTrue(orderStatus.order_status === "paid");
}
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-fakebank.ts b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-fakebank.ts
new file mode 100644
index 000000000..bfe29b322
--- /dev/null
+++ b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-fakebank.ts
@@ -0,0 +1,96 @@
+/*
+ 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 {
+ GlobalTestState,
+ BankApi,
+ WalletCli,
+ setupDb,
+ ExchangeService,
+ FakeBankService,
+} from "./harness";
+import { createSimpleTestkudosEnvironment } from "./helpers";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { CoinConfig, defaultCoinConfig } from "./denomStructures.js";
+import { URL } from "@gnu-taler/taler-util";
+
+/**
+ * Run test for basic, bank-integrated withdrawal.
+ */
+export async function runTestWithdrawalFakebankTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const db = await setupDb(t);
+
+ const bank = await FakeBankService.create(t, {
+ currency: "TESTKUDOS",
+ httpPort: 8082,
+ });
+
+ const exchange = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db.connStr,
+ });
+
+ exchange.addBankAccount("1", {
+ accountName: "exchange",
+ accountPassword: "x",
+ wireGatewayApiBaseUrl: new URL("/exchange/", bank.baseUrl).href,
+ accountPaytoUri: "payto://x-taler-bank/localhost/exchange",
+ });
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS"));
+ exchange.addCoinConfigList(coinConfig);
+
+ await exchange.start();
+ await exchange.pingUntilAvailable();
+
+ console.log("setup done!");
+
+ const wallet = new WalletCli(t);
+
+ await wallet.client.call(WalletApiOperation.AddExchange, {
+ exchangeBaseUrl: exchange.baseUrl,
+ });
+
+ await wallet.client.call(WalletApiOperation.WithdrawFakebank, {
+ exchange: exchange.baseUrl,
+ amount: "TESTKUDOS:10",
+ bank: bank.baseUrl,
+ });
+
+ await exchange.runWirewatchOnce();
+
+ await wallet.runUntilDone();
+
+ // Check balance
+
+ const balResp = await wallet.client.call(WalletApiOperation.GetBalances, {});
+ t.assertAmountEquals("TESTKUDOS:9.72", balResp.balances[0].available);
+
+ await t.shutdown();
+}
+
+runTestWithdrawalFakebankTest.suites = ["wallet"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-manual.ts b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-manual.ts
index 613618071..fe8fd3c56 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-manual.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-manual.ts
@@ -19,8 +19,6 @@
*/
import { GlobalTestState, BankApi } from "./harness";
import { createSimpleTestkudosEnvironment } from "./helpers";
-import { CoreApiResponse } from "@gnu-taler/taler-util";
-import { codecForBalancesResponse } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
/**
@@ -40,8 +38,6 @@ export async function runTestWithdrawalManualTest(t: GlobalTestState) {
const user = await BankApi.createRandomBankUser(bank);
- let wresp: CoreApiResponse;
-
await wallet.client.call(WalletApiOperation.AddExchange, {
exchangeBaseUrl: exchange.baseUrl,
});
diff --git a/packages/taler-wallet-cli/src/integrationtests/testrunner.ts b/packages/taler-wallet-cli/src/integrationtests/testrunner.ts
index 720dd8b80..bcb0dd271 100644
--- a/packages/taler-wallet-cli/src/integrationtests/testrunner.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/testrunner.ts
@@ -87,6 +87,7 @@ import { runPaymentZeroTest } from "./test-payment-zero.js";
import { runMerchantSpecPublicOrdersTest } from "./test-merchant-spec-public-orders.js";
import { runExchangeTimetravelTest } from "./test-exchange-timetravel.js";
import { runDenomUnofferedTest } from "./test-denom-unoffered.js";
+import { runTestWithdrawalFakebankTest } from "./test-withdrawal-fakebank.js";
/**
* Test runner.
@@ -154,6 +155,7 @@ const allTests: TestMainFunction[] = [
runRefundTest,
runRevocationTest,
runTestWithdrawalManualTest,
+ runTestWithdrawalFakebankTest,
runTimetravelAutorefreshTest,
runTimetravelWithdrawTest,
runTippingTest,
@@ -340,7 +342,7 @@ export async function runTests(spec: TestRunSpec) {
try {
result = await token.racePromise(resultPromise);
- } catch (e) {
+ } catch (e: any) {
console.error(`test ${testName} timed out`);
if (token.isCancelled) {
result = {
diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts
index 75121ed38..c5bf2c8c0 100644
--- a/packages/taler-wallet-core/src/wallet-api-types.ts
+++ b/packages/taler-wallet-core/src/wallet-api-types.ts
@@ -63,10 +63,14 @@ import {
TransactionsResponse,
WalletBackupContentV1,
WalletCurrencyInfo,
+ WithdrawFakebankRequest,
WithdrawTestBalanceRequest,
WithdrawUriInfoResponse,
} from "@gnu-taler/taler-util";
-import { AddBackupProviderRequest, BackupInfo } from "./operations/backup/index.js";
+import {
+ AddBackupProviderRequest,
+ BackupInfo,
+} from "./operations/backup/index.js";
import { PendingOperationsResponse } from "./pending-types.js";
export enum WalletApiOperation {
@@ -110,9 +114,14 @@ export enum WalletApiOperation {
CreateDepositGroup = "createDepositGroup",
SetWalletDeviceId = "setWalletDeviceId",
ExportBackupPlain = "exportBackupPlain",
+ WithdrawFakebank = "withdrawFakebank",
}
export type WalletOperations = {
+ [WalletApiOperation.WithdrawFakebank]: {
+ request: WithdrawFakebankRequest;
+ response: {};
+ };
[WalletApiOperation.PreparePayForUri]: {
request: PreparePayRequest;
response: PreparePayResult;
@@ -256,7 +265,7 @@ export type WalletOperations = {
[WalletApiOperation.TestPay]: {
request: TestPayArgs;
response: {};
- }
+ };
};
export type RequestType<
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index 253a69df3..32e3945e8 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -38,6 +38,9 @@ import {
Timestamp,
timestampMin,
WalletNotification,
+ codecForWithdrawFakebankRequest,
+ URL,
+ parsePaytoUri,
} from "@gnu-taler/taler-util";
import {
addBackupProvider,
@@ -173,7 +176,10 @@ import {
openPromise,
} from "./util/promiseUtils.js";
import { DbAccess } from "./util/query.js";
-import { HttpRequestLibrary } from "./util/http.js";
+import {
+ HttpRequestLibrary,
+ readSuccessResponseJsonOrThrow,
+} from "./util/http.js";
const builtinAuditors: AuditorTrustRecord[] = [
{
@@ -217,7 +223,12 @@ async function processOnePendingOperation(
logger.trace(`running pending ${JSON.stringify(pending, undefined, 2)}`);
switch (pending.type) {
case PendingTaskType.ExchangeUpdate:
- await updateExchangeFromUrl(ws, pending.exchangeBaseUrl, undefined, forceNow);
+ await updateExchangeFromUrl(
+ ws,
+ pending.exchangeBaseUrl,
+ undefined,
+ forceNow,
+ );
break;
case PendingTaskType.Refresh:
await processRefreshGroup(ws, pending.refreshGroupId, forceNow);
@@ -418,7 +429,7 @@ async function fillDefaults(ws: InternalWalletState): Promise<void> {
}
/**
- * Create a reserve, but do not flag it as confirmed yet.
+ * Create a reserve for a manual withdrawal.
*
* Adds the corresponding exchange as a trusted exchange if it is neither
* audited nor trusted already.
@@ -462,7 +473,11 @@ async function getExchangeTos(
const content = exchangeDetails.termsOfServiceText;
const currentEtag = exchangeDetails.termsOfServiceLastEtag;
const contentType = exchangeDetails.termsOfServiceContentType;
- if (content === undefined || currentEtag === undefined || contentType === undefined) {
+ if (
+ content === undefined ||
+ currentEtag === undefined ||
+ contentType === undefined
+ ) {
throw Error("exchange is in invalid state");
}
return {
@@ -688,7 +703,12 @@ async function dispatchRequestInternal(
}
case "addExchange": {
const req = codecForAddExchangeRequest().decode(payload);
- await updateExchangeFromUrl(ws, req.exchangeBaseUrl, undefined, req.forceUpdate);
+ await updateExchangeFromUrl(
+ ws,
+ req.exchangeBaseUrl,
+ undefined,
+ req.forceUpdate,
+ );
return {};
}
case "listExchanges": {
@@ -700,7 +720,11 @@ async function dispatchRequestInternal(
}
case "getExchangeWithdrawalInfo": {
const req = codecForGetExchangeWithdrawalInfo().decode(payload);
- return await getExchangeWithdrawalInfo(ws, req.exchangeBaseUrl, req.amount);
+ return await getExchangeWithdrawalInfo(
+ ws,
+ req.exchangeBaseUrl,
+ req.amount,
+ );
}
case "acceptManualWithdrawal": {
const req = codecForAcceptManualWithdrawalRequet().decode(payload);
@@ -748,7 +772,7 @@ async function dispatchRequestInternal(
}
case "getExchangeTos": {
const req = codecForGetExchangeTosRequest().decode(payload);
- return getExchangeTos(ws, req.exchangeBaseUrl , req.acceptedFormat);
+ return getExchangeTos(ws, req.exchangeBaseUrl, req.acceptedFormat);
}
case "retryPendingNow": {
await runPending(ws, true);
@@ -889,6 +913,35 @@ async function dispatchRequestInternal(
};
});
}
+ case "withdrawFakebank": {
+ const req = codecForWithdrawFakebankRequest().decode(payload);
+ const amount = Amounts.parseOrThrow(req.amount);
+ const details = await getWithdrawalDetailsForAmount(
+ ws,
+ req.exchange,
+ amount,
+ );
+ const wres = await acceptManualWithdrawal(ws, req.exchange, amount);
+ const paytoUri = details.paytoUris[0];
+ const pt = parsePaytoUri(paytoUri);
+ if (!pt) {
+ throw Error("failed to parse payto URI");
+ }
+ const components = pt.targetPath.split("/");
+ const creditorAcct = components[components.length - 1];
+ logger.info(`making testbank transfer to '${creditorAcct}''`)
+ const fbReq = await ws.http.postJson(
+ new URL(`${creditorAcct}/admin/add-incoming`, req.bank).href,
+ {
+ amount: Amounts.stringify(amount),
+ reserve_pub: wres.reservePub,
+ debit_account: "payto://x-taler-bank/localhost/testdebtor",
+ },
+ );
+ const fbResp = await readSuccessResponseJsonOrThrow(fbReq, codecForAny());
+ logger.info(`started fakebank withdrawal: ${j2s(fbResp)}`);
+ return {};
+ }
}
throw OperationFailedError.fromCode(
TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN,
@@ -916,7 +969,7 @@ export async function handleCoreApiRequest(
id,
result,
};
- } catch (e) {
+ } catch (e: any) {
if (
e instanceof OperationFailedError ||
e instanceof OperationFailedAndReportedError
@@ -928,6 +981,10 @@ export async function handleCoreApiRequest(
error: e.operationError,
};
} else {
+ try {
+ logger.error("Caught unexpected exception:");
+ logger.error(e.stack);
+ } catch (e) {}
return {
type: "error",
operation,