diff options
author | Florian Dold <florian@dold.me> | 2021-10-14 11:36:43 +0200 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2021-10-14 11:36:43 +0200 |
commit | c53264869451ccbfbaf1976e01df8c7636163068 (patch) | |
tree | a6f4359d4fcd558ee443991111404bc095642e5b /packages | |
parent | 6f4c0a6fb244b8e42b6d91edd3c5901ae39f2202 (diff) |
implement fakebank withdrawal
Diffstat (limited to 'packages')
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, |