aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2023-02-23 00:52:10 +0100
committerFlorian Dold <florian@dold.me>2023-02-23 00:52:17 +0100
commit7985b0a33ffc3e258da5d73f4056384c38e626fe (patch)
tree68908cb8ac2d49551f22bb4745bdf541156b8be5
parent7879efcff70ea73935e139f4522aedadfe755c04 (diff)
taler-harness: deployment tooling for tipping
-rw-r--r--packages/taler-harness/src/harness/harness.ts54
-rw-r--r--packages/taler-harness/src/harness/libeufin-apis.ts7
-rw-r--r--packages/taler-harness/src/index.ts125
-rw-r--r--packages/taler-util/src/http-common.ts18
-rw-r--r--packages/taler-util/src/http-impl.node.ts7
-rw-r--r--packages/taler-util/src/http-impl.qtart.ts17
-rw-r--r--packages/taler-wallet-core/src/bank-api-client.ts60
7 files changed, 266 insertions, 22 deletions
diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts
index 6168ea0b7..5733e776b 100644
--- a/packages/taler-harness/src/harness/harness.ts
+++ b/packages/taler-harness/src/harness/harness.ts
@@ -1436,12 +1436,20 @@ export interface MerchantServiceInterface {
readonly name: string;
}
+export interface DeleteTippingReserveArgs {
+ reservePub: string;
+ purge?: boolean;
+}
+
export class MerchantApiClient {
constructor(
private baseUrl: string,
public readonly auth: MerchantAuthConfiguration,
) {}
+ // FIXME: Migrate everything to this in favor of axios
+ http = createPlatformHttpLib();
+
async changeAuth(auth: MerchantAuthConfiguration): Promise<void> {
const url = new URL("private/auth", this.baseUrl);
await axios.post(url.href, auth, {
@@ -1449,6 +1457,51 @@ export class MerchantApiClient {
});
}
+ async deleteTippingReserve(req: DeleteTippingReserveArgs): Promise<void> {
+ const url = new URL(`private/reserves/${req.reservePub}`, this.baseUrl);
+ if (req.purge) {
+ url.searchParams.set("purge", "YES");
+ }
+ const resp = await axios.delete(url.href, {
+ headers: this.makeAuthHeader(),
+ });
+ logger.info(`delete status: ${resp.status}`);
+ return;
+ }
+
+ async createTippingReserve(
+ req: CreateMerchantTippingReserveRequest,
+ ): Promise<CreateMerchantTippingReserveConfirmation> {
+ const url = new URL("private/reserves", this.baseUrl);
+ const resp = await axios.post(url.href, req, {
+ headers: this.makeAuthHeader(),
+ });
+ // FIXME: validate
+ return resp.data;
+ }
+
+ async getPrivateInstanceInfo(): Promise<any> {
+ console.log(this.makeAuthHeader());
+ const url = new URL("private", this.baseUrl);
+ logger.info(`request url ${url.href}`);
+ const resp = await this.http.fetch(url.href, {
+ method: "GET",
+ headers: this.makeAuthHeader(),
+ });
+ return await resp.json();
+ }
+
+ async getPrivateTipReserves(): Promise<TippingReserveStatus> {
+ console.log(this.makeAuthHeader());
+ const url = new URL("private/reserves", this.baseUrl);
+ const resp = await this.http.fetch(url.href, {
+ method: "GET",
+ headers: this.makeAuthHeader(),
+ });
+ // FIXME: Validate!
+ return await resp.json();
+ }
+
async deleteInstance(instanceId: string) {
const url = new URL(`management/instances/${instanceId}`, this.baseUrl);
await axios.delete(url.href, {
@@ -1578,6 +1631,7 @@ export namespace MerchantPrivateApi {
`private/reserves`,
merchantService.makeInstanceBaseUrl(instance),
);
+ // FIXME: Don't use axios!
const resp = await axios.post(reqUrl.href, req);
// FIXME: validate
return resp.data;
diff --git a/packages/taler-harness/src/harness/libeufin-apis.ts b/packages/taler-harness/src/harness/libeufin-apis.ts
index a6abe3466..4ef588fb5 100644
--- a/packages/taler-harness/src/harness/libeufin-apis.ts
+++ b/packages/taler-harness/src/harness/libeufin-apis.ts
@@ -7,7 +7,8 @@
import axiosImp from "axios";
const axios = axiosImp.default;
-import { Logger, URL } from "@gnu-taler/taler-util";
+import { AmountString, Logger, URL } from "@gnu-taler/taler-util";
+import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
export interface LibeufinSandboxServiceInterface {
baseUrl: string;
@@ -163,10 +164,6 @@ export interface LibeufinSandboxAddIncomingRequest {
direction: string;
}
-function getRandomString(): string {
- return Math.random().toString(36).substring(2);
-}
-
/**
* APIs spread across Legacy and Access, it is therefore
* the "base URL" relative to which API every call addresses.
diff --git a/packages/taler-harness/src/index.ts b/packages/taler-harness/src/index.ts
index 14b8a4302..370550420 100644
--- a/packages/taler-harness/src/index.ts
+++ b/packages/taler-harness/src/index.ts
@@ -22,10 +22,13 @@ import fs from "fs";
import os from "os";
import path from "path";
import {
+ addPaytoQueryParams,
Amounts,
Configuration,
decodeCrock,
+ j2s,
Logger,
+ parsePaytoUri,
rsaBlind,
setGlobalLogLevelFromString,
} from "@gnu-taler/taler-util";
@@ -33,11 +36,18 @@ import { runBench1 } from "./bench1.js";
import { runBench2 } from "./bench2.js";
import { runBench3 } from "./bench3.js";
import { runEnv1 } from "./env1.js";
-import { GlobalTestState, runTestWithState } from "./harness/harness.js";
+import {
+ GlobalTestState,
+ MerchantApiClient,
+ MerchantPrivateApi,
+ runTestWithState,
+} from "./harness/harness.js";
import { getTestInfo, runTests } from "./integrationtests/testrunner.js";
import { lintExchangeDeployment } from "./lint.js";
import { runEnvFull } from "./env-full.js";
import { clk } from "@gnu-taler/taler-util/clk";
+import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
+import { BankAccessApiClient } from "@gnu-taler/taler-wallet-core";
const logger = new Logger("taler-harness:index.ts");
@@ -152,11 +162,124 @@ advancedCli
await runTestWithState(testState, runEnv1, "env1", true);
});
+const sandcastleCli = testingCli.subcommand("sandcastleArgs", "sandcastle", {
+ help: "Subcommands for handling GNU Taler sandcastle deployments.",
+});
+
const deploymentCli = testingCli.subcommand("deploymentArgs", "deployment", {
help: "Subcommands for handling GNU Taler deployments.",
});
deploymentCli
+ .subcommand("tipTopup", "tip-topup")
+ .requiredOption("merchantBaseUrl", ["--merchant-url"], clk.STRING)
+ .requiredOption("exchangeBaseUrl", ["--exchange-url"], clk.STRING)
+ .requiredOption("merchantApikey", ["--merchant-apikey"], clk.STRING)
+ .requiredOption("bankAccessUrl", ["--bank-access-url"], clk.STRING)
+ .requiredOption("bankAccount", ["--bank-account"], clk.STRING)
+ .requiredOption("bankPassword", ["--bank-password"], clk.STRING)
+ .requiredOption("wireMethod", ["--wire-method"], clk.STRING)
+ .requiredOption("amount", ["--amount"], clk.STRING)
+ .action(async (args) => {
+ const amount = args.tipTopup.amount;
+
+ const merchantClient = new MerchantApiClient(
+ args.tipTopup.merchantBaseUrl,
+ {
+ method: "token",
+ token: args.tipTopup.merchantApikey,
+ },
+ );
+
+ const res = await merchantClient.getPrivateInstanceInfo();
+ console.log(res);
+
+ const tipReserveResp = await merchantClient.createTippingReserve({
+ exchange_url: args.tipTopup.exchangeBaseUrl,
+ initial_balance: amount,
+ wire_method: args.tipTopup.wireMethod,
+ });
+
+ console.log(tipReserveResp);
+
+ const bankAccessApiClient = new BankAccessApiClient({
+ baseUrl: args.tipTopup.bankAccessUrl,
+ username: args.tipTopup.bankAccount,
+ password: args.tipTopup.bankPassword,
+ });
+
+ const paytoUri = addPaytoQueryParams(tipReserveResp.payto_uri, {
+ message: `tip-reserve ${tipReserveResp.reserve_pub}`,
+ });
+
+ console.log("payto URI:", paytoUri);
+
+ const transactions = await bankAccessApiClient.getTransactions();
+ console.log("transactions:", j2s(transactions));
+
+ await bankAccessApiClient.createTransaction({
+ amount,
+ paytoUri,
+ });
+ });
+
+deploymentCli
+ .subcommand("tipCleanup", "tip-cleanup")
+ .requiredOption("merchantBaseUrl", ["--merchant-url"], clk.STRING)
+ .requiredOption("merchantApikey", ["--merchant-apikey"], clk.STRING)
+ .flag("dryRun", ["--dry-run"])
+ .action(async (args) => {
+ const merchantClient = new MerchantApiClient(
+ args.tipCleanup.merchantBaseUrl,
+ {
+ method: "token",
+ token: args.tipCleanup.merchantApikey,
+ },
+ );
+
+ const res = await merchantClient.getPrivateInstanceInfo();
+ console.log(res);
+
+ const tipRes = await merchantClient.getPrivateTipReserves();
+ console.log(tipRes);
+
+ for (const reserve of tipRes.reserves) {
+ if (Amounts.isZero(reserve.exchange_initial_amount)) {
+ if (args.tipCleanup.dryRun) {
+ logger.info(`dry run, would purge reserve ${reserve}`);
+ } else {
+ await merchantClient.deleteTippingReserve({
+ reservePub: reserve.reserve_pub,
+ purge: true,
+ });
+ }
+ }
+ }
+
+ // FIXME: Now delete reserves that are not filled yet
+ });
+
+deploymentCli
+ .subcommand("tipStatus", "tip-status")
+ .requiredOption("merchantBaseUrl", ["--merchant-url"], clk.STRING)
+ .requiredOption("merchantApikey", ["--merchant-apikey"], clk.STRING)
+ .action(async (args) => {
+ const merchantClient = new MerchantApiClient(
+ args.tipStatus.merchantBaseUrl,
+ {
+ method: "token",
+ token: args.tipStatus.merchantApikey,
+ },
+ );
+
+ const res = await merchantClient.getPrivateInstanceInfo();
+ console.log(res);
+
+ const tipRes = await merchantClient.getPrivateTipReserves();
+ console.log(j2s(tipRes));
+ });
+
+deploymentCli
.subcommand("lintExchange", "lint-exchange", {
help: "Run checks on the exchange deployment.",
})
diff --git a/packages/taler-util/src/http-common.ts b/packages/taler-util/src/http-common.ts
index 54f26e615..e5dd92567 100644
--- a/packages/taler-util/src/http-common.ts
+++ b/packages/taler-util/src/http-common.ts
@@ -59,7 +59,7 @@ export interface HttpRequestOptions {
*/
cancellationToken?: CancellationToken;
- body?: string | ArrayBuffer | Record<string, unknown>;
+ body?: string | ArrayBuffer | object;
}
/**
@@ -344,9 +344,8 @@ export function getExpiry(
return t;
}
-
export interface HttpLibArgs {
- enableThrottling?: boolean,
+ enableThrottling?: boolean;
}
export function encodeBody(body: any): ArrayBuffer {
@@ -364,3 +363,16 @@ export function encodeBody(body: any): ArrayBuffer {
}
throw new TypeError("unsupported request body type");
}
+
+export function getDefaultHeaders(method: string): Record<string, string> {
+ const headers: Record<string, string> = {};
+
+ if (method === "POST" || method === "PUT" || method === "PATCH") {
+ // Default to JSON if we have a body
+ headers["Content-Type"] = "application/json";
+ }
+
+ headers["Accept"] = "application/json";
+
+ return headers;
+}
diff --git a/packages/taler-util/src/http-impl.node.ts b/packages/taler-util/src/http-impl.node.ts
index 798b81e2d..6dfce934f 100644
--- a/packages/taler-util/src/http-impl.node.ts
+++ b/packages/taler-util/src/http-impl.node.ts
@@ -23,7 +23,7 @@ import * as http from "node:http";
import * as https from "node:https";
import { RequestOptions } from "node:http";
import { TalerError } from "./errors.js";
-import { encodeBody, HttpLibArgs } from "./http-common.js";
+import { encodeBody, getDefaultHeaders, HttpLibArgs } from "./http-common.js";
import {
DEFAULT_REQUEST_TIMEOUT_MS,
Headers,
@@ -85,8 +85,7 @@ export class HttpLibImpl implements HttpRequestLibrary {
timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS;
}
- const headers = { ...opt?.headers };
- headers["Content-Type"] = "application/json";
+ const requestHeadersMap = { ...getDefaultHeaders(method), ...opt?.headers };
let reqBody: ArrayBuffer | undefined;
@@ -114,7 +113,7 @@ export class HttpLibImpl implements HttpRequestLibrary {
host: parsedUrl.hostname,
method: method,
path,
- headers: opt?.headers,
+ headers: requestHeadersMap,
};
const chunks: Uint8Array[] = [];
diff --git a/packages/taler-util/src/http-impl.qtart.ts b/packages/taler-util/src/http-impl.qtart.ts
index 954b41802..ee3d1f725 100644
--- a/packages/taler-util/src/http-impl.qtart.ts
+++ b/packages/taler-util/src/http-impl.qtart.ts
@@ -21,7 +21,7 @@
*/
import { Logger } from "@gnu-taler/taler-util";
import { TalerError } from "./errors.js";
-import { encodeBody, HttpLibArgs } from "./http-common.js";
+import { encodeBody, getDefaultHeaders, HttpLibArgs } from "./http-common.js";
import {
Headers,
HttpRequestLibrary,
@@ -54,7 +54,7 @@ export class HttpLibImpl implements HttpRequestLibrary {
}
async fetch(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
- const method = opt?.method ?? "GET";
+ const method = (opt?.method ?? "GET").toUpperCase();
logger.trace(`Requesting ${method} ${url}`);
@@ -72,19 +72,18 @@ export class HttpLibImpl implements HttpRequestLibrary {
}
let data: ArrayBuffer | undefined = undefined;
- let headers: string[] = [];
- if (opt?.headers) {
- for (let headerName of Object.keys(opt.headers)) {
- headers.push(`${headerName}: ${opt.headers[headerName]}`);
- }
+ const requestHeadersMap = { ...getDefaultHeaders(method), ...opt?.headers };
+ let headersList: string[] = [];
+ for (let headerName of Object.keys(requestHeadersMap)) {
+ headersList.push(`${headerName}: ${requestHeadersMap[headerName]}`);
}
- if (method.toUpperCase() === "POST") {
+ if (method === "POST") {
data = encodeBody(opt?.body);
}
const res = await qjsOs.fetchHttp(url, {
method,
data,
- headers,
+ headers: headersList,
});
return {
requestMethod: method,
diff --git a/packages/taler-wallet-core/src/bank-api-client.ts b/packages/taler-wallet-core/src/bank-api-client.ts
index f807d2daa..de0d4b852 100644
--- a/packages/taler-wallet-core/src/bank-api-client.ts
+++ b/packages/taler-wallet-core/src/bank-api-client.ts
@@ -37,6 +37,7 @@ import {
TalerErrorCode,
} from "@gnu-taler/taler-util";
import {
+ createPlatformHttpLib,
HttpRequestLibrary,
readSuccessResponseJsonOrThrow,
} from "@gnu-taler/taler-util/http";
@@ -277,3 +278,62 @@ export namespace BankAccessApi {
);
}
}
+
+export interface BankAccessApiClientArgs {
+ baseUrl: string;
+ username: string;
+ password: string;
+}
+
+export interface BankAccessApiCreateTransactionRequest {
+ amount: AmountString;
+ paytoUri: string;
+}
+
+export class BankAccessApiClient {
+ httpLib = createPlatformHttpLib();
+
+ constructor(private args: BankAccessApiClientArgs) {}
+
+ async getTransactions(): Promise<void> {
+ const reqUrl = new URL(
+ `accounts/${this.args.username}/transactions`,
+ this.args.baseUrl,
+ );
+ const authHeaderValue = makeBasicAuthHeader(
+ this.args.username,
+ this.args.password,
+ );
+ const resp = await this.httpLib.fetch(reqUrl.href, {
+ method: "GET",
+ headers: {
+ Authorization: authHeaderValue,
+ },
+ });
+
+ const res = await readSuccessResponseJsonOrThrow(resp, codecForAny());
+ logger.info(`result: ${j2s(res)}`);
+ }
+
+ async createTransaction(
+ req: BankAccessApiCreateTransactionRequest,
+ ): Promise<any> {
+ const reqUrl = new URL(
+ `accounts/${this.args.username}/transactions`,
+ this.args.baseUrl,
+ );
+ const authHeaderValue = makeBasicAuthHeader(
+ this.args.username,
+ this.args.password,
+ );
+ const resp = await this.httpLib.fetch(reqUrl.href, {
+ method: "POST",
+ body: req,
+ headers: {
+ Authorization: authHeaderValue,
+ },
+ });
+
+ return await readSuccessResponseJsonOrThrow(resp, codecForAny());
+ }
+}