/* 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 BankServiceHandle { readonly bankAccessApiBaseUrl: string; readonly http: HttpRequestLibrary; } export interface BankUser { username: string; password: string; accountPaytoUri: string; } export interface WithdrawalOperationInfo { withdrawal_id: string; taler_withdraw_uri: string; } /** * FIXME: Rename, this is not part of the integration test harness anymore. */ export interface HarnessExchangeBankAccount { accountName: string; accountPassword: string; accountPaytoUri: string; wireGatewayApiBaseUrl: 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"); /** * @deprecated Use BankAccessApiClient or WireGatewayApi */ export namespace BankApi { // FIXME: Move to BankAccessApi?! export async function registerAccount( bank: BankServiceHandle, username: string, password: string, options: { iban?: string; }, ): Promise { const url = new URL("testing/register", bank.bankAccessApiBaseUrl); const resp = await bank.http.postJson(url.href, { 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, }; } // FIXME: Move to BankAccessApi?! export async function createRandomBankUser( bank: BankServiceHandle, ): 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 registerAccount(bank, username, password, { iban, }); } export async function confirmWithdrawalOperation( bank: BankServiceHandle, bankUser: BankUser, wopi: WithdrawalOperationInfo, ): Promise { const url = new URL( `accounts/${bankUser.username}/withdrawals/${wopi.withdrawal_id}/confirm`, bank.bankAccessApiBaseUrl, ); logger.info(`confirming withdrawal operation via ${url.href}`); const resp = await bank.http.postJson( url.href, {}, { headers: { Authorization: makeBasicAuthHeader( bankUser.username, bankUser.password, ), }, }, ); logger.info(`response status ${resp.status}`); const respJson = await readSuccessResponseJsonOrThrow(resp, codecForAny()); // FIXME: We don't check the status here! } export async function abortWithdrawalOperation( bank: BankServiceHandle, bankUser: BankUser, wopi: WithdrawalOperationInfo, ): Promise { const url = new URL( `accounts/${bankUser.username}/withdrawals/${wopi.withdrawal_id}/abort`, bank.bankAccessApiBaseUrl, ); const resp = await bank.http.postJson( url.href, {}, { headers: { Authorization: makeBasicAuthHeader( bankUser.username, bankUser.password, ), }, }, ); await readSuccessResponseJsonOrThrow(resp, codecForAny()); } } /** * @deprecated use BankAccessApiClient */ export namespace BankAccessApi { export async function getAccountBalance( bank: BankServiceHandle, bankUser: BankUser, ): Promise { const url = new URL( `accounts/${bankUser.username}`, bank.bankAccessApiBaseUrl, ); const resp = await bank.http.get(url.href, { headers: { Authorization: makeBasicAuthHeader( bankUser.username, bankUser.password, ), }, }); return await resp.json(); } export async function createWithdrawalOperation( bank: BankServiceHandle, bankUser: BankUser, amount: string, ): Promise { const url = new URL( `accounts/${bankUser.username}/withdrawals`, bank.bankAccessApiBaseUrl, ); const resp = await bank.http.postJson( url.href, { amount, }, { headers: { Authorization: makeBasicAuthHeader( bankUser.username, bankUser.password, ), }, }, ); return readSuccessResponseJsonOrThrow( resp, codecForWithdrawalOperationInfo(), ); } } export interface BankAccessApiClientArgs { baseUrl: string; username: string; password: string; enableThrottling?: boolean; allowHttp?: boolean; } export interface BankAccessApiCreateTransactionRequest { amount: AmountString; paytoUri: string; } export class WireGatewayApiClientArgs { accountName: string; accountPassword: string; wireGatewayApiBaseUrl: string; enableThrottling?: boolean; allowHttp?: boolean; } /** * 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 args: WireGatewayApiClientArgs) { this.httpLib = createPlatformHttpLib({ enableThrottling: !!args.enableThrottling, allowHttp: !!args.allowHttp, }); } async adminAddIncoming(params: { amount: string; reservePub: string; debitAccountPayto: string; }): Promise { let url = new URL(`admin/add-incoming`, this.args.wireGatewayApiBaseUrl); const resp = await this.httpLib.fetch(url.href, { method: "POST", body: { amount: params.amount, reserve_pub: params.reservePub, debit_account: params.debitAccountPayto, }, headers: { Authorization: makeBasicAuthHeader( this.args.accountName, this.args.accountPassword, ), }, }); 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; constructor(private args: BankAccessApiClientArgs) { this.httpLib = createPlatformHttpLib({ enableThrottling: !!args.enableThrottling, allowHttp: !!args.allowHttp, }); } async getTransactions(): Promise { 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 { 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()); } }