/* This file is part of GNU Taler (C) 2022 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 */ /** * Client for the Taler (demo-)bank. */ /** * Imports. */ import { AmountString, base64FromArrayBuffer, buildCodecForObject, Codec, codecForAny, codecForString, encodeCrock, generateIban, getRandomBytes, j2s, Logger, stringToBytes, TalerError, TalerErrorCode, } from "@gnu-taler/taler-util"; import { checkSuccessResponseOrThrow, createPlatformHttpLib, HttpRequestLibrary, readSuccessResponseJsonOrThrow, } from "@gnu-taler/taler-util/http"; const logger = new Logger("bank-api-client.ts"); export enum CreditDebitIndicator { Credit = "credit", Debit = "debit", } export interface BankAccountBalanceResponse { balance: { amount: AmountString; credit_debit_indicator: CreditDebitIndicator; }; } export interface BankUser { username: string; password: string; accountPaytoUri: string; } export interface WithdrawalOperationInfo { withdrawal_id: string; taler_withdraw_uri: string; } /** * Helper function to generate the "Authorization" HTTP header. */ function makeBasicAuthHeader(username: string, password: string): string { const auth = `${username}:${password}`; const authEncoded: string = base64FromArrayBuffer(stringToBytes(auth)); return `Basic ${authEncoded}`; } const codecForWithdrawalOperationInfo = (): Codec => buildCodecForObject() .property("withdrawal_id", codecForString()) .property("taler_withdraw_uri", codecForString()) .build("WithdrawalOperationInfo"); export interface BankAccessApiClientArgs { auth?: { username: string; password: string }; httpClient?: HttpRequestLibrary; } export interface BankAccessApiCreateTransactionRequest { amount: AmountString; paytoUri: string; } export class WireGatewayApiClientArgs { auth?: { username: string; password: string; }; httpClient?: HttpRequestLibrary; } /** * This API look like it belongs to harness * but it will be nice to have in utils to be used by others */ export class WireGatewayApiClient { httpLib; constructor( private baseUrl: string, private args: WireGatewayApiClientArgs = {}, ) { this.httpLib = args.httpClient ?? createPlatformHttpLib(); } private makeAuthHeader(): Record { const auth = this.args.auth; if (auth) { return { Authorization: makeBasicAuthHeader(auth.username, auth.password), }; } return {}; } async adminAddIncoming(params: { amount: string; reservePub: string; debitAccountPayto: string; }): Promise { let url = new URL(`admin/add-incoming`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "POST", body: { amount: params.amount, reserve_pub: params.reservePub, debit_account: params.debitAccountPayto, }, headers: this.makeAuthHeader(), }); logger.info(`add-incoming response status: ${resp.status}`); await checkSuccessResponseOrThrow(resp); } } /** * This API look like it belongs to harness * but it will be nice to have in utils to be used by others */ export class BankAccessApiClient { httpLib: HttpRequestLibrary; constructor( private baseUrl: string, private args: BankAccessApiClientArgs = {}, ) { this.httpLib = args.httpClient ?? createPlatformHttpLib(); } setAuth(auth: { username: string; password: string }) { this.args.auth = auth; } private makeAuthHeader(): Record { if (!this.args.auth) { return {}; } const authHeaderValue = makeBasicAuthHeader( this.args.auth.username, this.args.auth.password, ); return { Authorization: authHeaderValue, }; } async getAccountBalance( username: string, ): Promise { const url = new URL(`accounts/${username}`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { headers: this.makeAuthHeader(), }); return await resp.json(); } async getTransactions(username: string): Promise { const reqUrl = new URL(`accounts/${username}/transactions`, this.baseUrl); const resp = await this.httpLib.fetch(reqUrl.href, { method: "GET", headers: { ...this.makeAuthHeader(), }, }); const res = await readSuccessResponseJsonOrThrow(resp, codecForAny()); logger.info(`result: ${j2s(res)}`); } async createTransaction( username: string, req: BankAccessApiCreateTransactionRequest, ): Promise { const reqUrl = new URL(`accounts/${username}/transactions`, this.baseUrl); const resp = await this.httpLib.fetch(reqUrl.href, { method: "POST", body: req, headers: this.makeAuthHeader(), }); return await readSuccessResponseJsonOrThrow(resp, codecForAny()); } async registerAccount( username: string, password: string, options: { iban?: string; } = {}, ): Promise { const url = new URL("testing/register", this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "POST", body: { username, password, iban: options?.iban, }, }); let paytoUri = `payto://x-taler-bank/localhost/${username}`; if (resp.status !== 200 && resp.status !== 202 && resp.status !== 204) { logger.error(`${j2s(await resp.json())}`); throw TalerError.fromDetail( TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR, { httpStatusCode: resp.status, }, ); } try { // Pybank has no body, thus this might throw. const respJson = await resp.json(); // LibEuFin demobank returns payto URI in response if (respJson.paytoUri) { paytoUri = respJson.paytoUri; } } catch (e) { // Do nothing } return { password, username, accountPaytoUri: paytoUri, }; } async createRandomBankUser(): Promise { const username = "user-" + encodeCrock(getRandomBytes(10)).toLowerCase(); const password = "pw-" + encodeCrock(getRandomBytes(10)).toLowerCase(); // FIXME: This is just a temporary workaround, because demobank is running out of short IBANs const iban = generateIban("DE", 15); return await this.registerAccount(username, password, { iban, }); } async createWithdrawalOperation( user: string, amount: string, ): Promise { const url = new URL(`accounts/${user}/withdrawals`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "POST", body: { amount, }, headers: this.makeAuthHeader(), }); return readSuccessResponseJsonOrThrow( resp, codecForWithdrawalOperationInfo(), ); } async confirmWithdrawalOperation( username: string, wopi: WithdrawalOperationInfo, ): Promise { const url = new URL( `accounts/${username}/withdrawals/${wopi.withdrawal_id}/confirm`, this.baseUrl, ); logger.info(`confirming withdrawal operation via ${url.href}`); const resp = await this.httpLib.fetch(url.href, { method: "POST", body: {}, headers: this.makeAuthHeader(), }); logger.info(`response status ${resp.status}`); const respJson = await readSuccessResponseJsonOrThrow(resp, codecForAny()); // FIXME: We don't check the status here! } async abortWithdrawalOperation( accountName: string, wopi: WithdrawalOperationInfo, ): Promise { const url = new URL( `accounts/${accountName}/withdrawals/${wopi.withdrawal_id}/abort`, this.baseUrl, ); const resp = await this.httpLib.fetch(url.href, { method: "POST", body: {}, headers: this.makeAuthHeader(), }); await readSuccessResponseJsonOrThrow(resp, codecForAny()); } }