implement fakebank withdrawal
This commit is contained in:
parent
6f4c0a6fb2
commit
c532648694
@ -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");
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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"];
|
@ -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,
|
||||
});
|
||||
|
@ -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 = {
|
||||
|
@ -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<
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user