implement fakebank withdrawal

This commit is contained in:
Florian Dold 2021-10-14 11:36:43 +02:00
parent 6f4c0a6fb2
commit c532648694
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
9 changed files with 304 additions and 32 deletions

View File

@ -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");

View File

@ -634,6 +634,29 @@ const advancedCli = walletCli.subcommand("advancedArgs", "advanced", {
"Subcommands for advanced operations (only use if you know what you're doing!).",
});
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();
}

View File

@ -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;

View File

@ -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");
}

View File

@ -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"];

View File

@ -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,
});

View File

@ -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 = {

View File

@ -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<

View File

@ -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,