diff options
-rw-r--r-- | packages/taler-harness/src/harness/harness.ts | 54 | ||||
-rw-r--r-- | packages/taler-harness/src/harness/libeufin-apis.ts | 7 | ||||
-rw-r--r-- | packages/taler-harness/src/index.ts | 125 | ||||
-rw-r--r-- | packages/taler-util/src/http-common.ts | 18 | ||||
-rw-r--r-- | packages/taler-util/src/http-impl.node.ts | 7 | ||||
-rw-r--r-- | packages/taler-util/src/http-impl.qtart.ts | 17 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/bank-api-client.ts | 60 |
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()); + } +} |