/* This file is part of GNU Taler (C) 2023 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 */ import { codecForAny } from "./codec.js"; import { createPlatformHttpLib, expectSuccessResponseOrThrow, readSuccessResponseJsonOrThrow, } from "./http.js"; import { FacadeCredentials } from "./libeufin-api-types.js"; import { Logger } from "./logging.js"; import { MerchantReserveCreateConfirmation, codecForMerchantReserveCreateConfirmation, TippingReserveStatus, MerchantInstancesResponse, MerchantPostOrderRequest, MerchantPostOrderResponse, codecForMerchantPostOrderResponse, MerchantOrderPrivateStatusResponse, codecForMerchantOrderPrivateStatusResponse, RewardCreateRequest, RewardCreateConfirmation, MerchantTemplateAddDetails, } from "./merchant-api-types.js"; import { AmountString } from "./taler-types.js"; import { TalerProtocolDuration } from "./time.js"; const logger = new Logger("MerchantApiClient.ts"); export interface MerchantAuthConfiguration { method: "external" | "token"; token?: string; } // FIXME: Why do we need this? Describe / fix! export interface PartialMerchantInstanceConfig { auth?: MerchantAuthConfiguration; id: string; name: string; paytoUris: string[]; address?: unknown; jurisdiction?: unknown; defaultWireTransferDelay?: TalerProtocolDuration; defaultPayDelay?: TalerProtocolDuration; } export interface CreateMerchantTippingReserveRequest { // Amount that the merchant promises to put into the reserve initial_balance: AmountString; // Exchange the merchant intends to use for tipping exchange_url: string; // Desired wire method, for example "iban" or "x-taler-bank" wire_method: string; } export interface DeleteTippingReserveArgs { reservePub: string; purge?: boolean; } export interface MerchantInstanceConfig { accounts: MerchantBankAccount[]; auth: MerchantAuthConfiguration; id: string; name: string; address: unknown; jurisdiction: unknown; use_stefan: boolean; default_wire_transfer_delay: TalerProtocolDuration; default_pay_delay: TalerProtocolDuration; } interface MerchantBankAccount { // The payto:// URI where the wallet will send coins. payto_uri: string; // Optional base URL for a facade where the // merchant backend can see incoming wire // transfers to reconcile its accounting // with that of the exchange. Used by // taler-merchant-wirewatch. credit_facade_url?: string; // Credentials for accessing the credit facade. credit_facade_credentials?: FacadeCredentials; } export interface MerchantInstanceConfig { accounts: MerchantBankAccount[]; auth: MerchantAuthConfiguration; id: string; name: string; address: unknown; jurisdiction: unknown; use_stefan: boolean; default_wire_transfer_delay: TalerProtocolDuration; default_pay_delay: TalerProtocolDuration; } export interface PrivateOrderStatusQuery { instance?: string; orderId: string; sessionId?: string; } /** * Client for the GNU Taler merchant backend. */ export class MerchantApiClient { /** * Base URL for the particular instance that this merchant API client * is for. */ private baseUrl: string; readonly auth: MerchantAuthConfiguration; constructor(baseUrl: string, auth?: MerchantAuthConfiguration) { this.baseUrl = baseUrl; this.auth = auth ?? { method: "external", }; } httpClient = createPlatformHttpLib(); async changeAuth(auth: MerchantAuthConfiguration): Promise { const url = new URL("private/auth", this.baseUrl); const res = await this.httpClient.fetch(url.href, { method: "POST", body: auth, headers: this.makeAuthHeader(), }); await expectSuccessResponseOrThrow(res); } async deleteTippingReserve(req: DeleteTippingReserveArgs): Promise { const url = new URL(`private/reserves/${req.reservePub}`, this.baseUrl); if (req.purge) { url.searchParams.set("purge", "YES"); } const resp = await this.httpClient.fetch(url.href, { method: "DELETE", headers: this.makeAuthHeader(), }); logger.info(`delete status: ${resp.status}`); return; } async createTippingReserve( req: CreateMerchantTippingReserveRequest, ): Promise { const url = new URL("private/reserves", this.baseUrl); const resp = await this.httpClient.fetch(url.href, { method: "POST", body: req, headers: this.makeAuthHeader(), }); const respData = readSuccessResponseJsonOrThrow( resp, codecForMerchantReserveCreateConfirmation(), ); return respData; } async getPrivateInstanceInfo(): Promise { const url = new URL("private", this.baseUrl); const resp = await this.httpClient.fetch(url.href, { method: "GET", headers: this.makeAuthHeader(), }); return await resp.json(); } async getPrivateTipReserves(): Promise { const url = new URL("private/reserves", this.baseUrl); const resp = await this.httpClient.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); const resp = await this.httpClient.fetch(url.href, { method: "DELETE", headers: this.makeAuthHeader(), }); await expectSuccessResponseOrThrow(resp); } async createInstance(req: MerchantInstanceConfig): Promise { const url = new URL("management/instances", this.baseUrl); await this.httpClient.fetch(url.href, { method: "POST", body: req, headers: this.makeAuthHeader(), }); } async getInstances(): Promise { const url = new URL("management/instances", this.baseUrl); const resp = await this.httpClient.fetch(url.href, { headers: this.makeAuthHeader(), }); return readSuccessResponseJsonOrThrow(resp, codecForAny()); } async getInstanceFullDetails(instanceId: string): Promise { const url = new URL(`management/instances/${instanceId}`, this.baseUrl); try { const resp = await this.httpClient.fetch(url.href, { headers: this.makeAuthHeader(), }); return resp.json(); } catch (e) { throw e; } } async createOrder( req: MerchantPostOrderRequest, ): Promise { let url = new URL("private/orders", this.baseUrl); const resp = await this.httpClient.fetch(url.href, { method: "POST", body: req, headers: this.makeAuthHeader(), }); return readSuccessResponseJsonOrThrow( resp, codecForMerchantPostOrderResponse(), ); } async queryPrivateOrderStatus( query: PrivateOrderStatusQuery, ): Promise { const reqUrl = new URL(`private/orders/${query.orderId}`, this.baseUrl); if (query.sessionId) { reqUrl.searchParams.set("session_id", query.sessionId); } const resp = await this.httpClient.fetch(reqUrl.href, { headers: this.makeAuthHeader(), }); return readSuccessResponseJsonOrThrow( resp, codecForMerchantOrderPrivateStatusResponse(), ); } async giveTip(req: RewardCreateRequest): Promise { const reqUrl = new URL(`private/rewards`, this.baseUrl); const resp = await this.httpClient.fetch(reqUrl.href, { method: "POST", body: req, }); // FIXME: validate return resp.json(); } async queryTippingReserves(): Promise { const reqUrl = new URL(`private/reserves`, this.baseUrl); const resp = await this.httpClient.fetch(reqUrl.href, { headers: this.makeAuthHeader(), }); // FIXME: validate return resp.json(); } async giveRefund(r: { instance: string; orderId: string; amount: string; justification: string; }): Promise<{ talerRefundUri: string }> { const reqUrl = new URL(`private/orders/${r.orderId}/refund`, this.baseUrl); const resp = await this.httpClient.fetch(reqUrl.href, { method: "POST", body: { refund: r.amount, reason: r.justification, }, }); const respBody = await resp.json(); return { talerRefundUri: respBody.taler_refund_uri, }; } async createTemplate(req: MerchantTemplateAddDetails) { let url = new URL("private/templates", this.baseUrl); const resp = await this.httpClient.fetch(url.href, { method: "POST", body: req, headers: this.makeAuthHeader(), }); if (resp.status !== 204) { throw Error("failed to create template"); } } private makeAuthHeader(): Record { switch (this.auth.method) { case "external": return {}; case "token": return { Authorization: `Bearer ${this.auth.token}`, }; } } }