summaryrefslogtreecommitdiff
path: root/packages/taler-util/src/bank-api-client.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-util/src/bank-api-client.ts')
-rw-r--r--packages/taler-util/src/bank-api-client.ts440
1 files changed, 440 insertions, 0 deletions
diff --git a/packages/taler-util/src/bank-api-client.ts b/packages/taler-util/src/bank-api-client.ts
new file mode 100644
index 000000000..51359129d
--- /dev/null
+++ b/packages/taler-util/src/bank-api-client.ts
@@ -0,0 +1,440 @@
+/*
+ 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 <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Client for the Taler (demo-)bank.
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AmountString,
+ base64FromArrayBuffer,
+ buildCodecForObject,
+ Codec,
+ codecForAny,
+ codecForString,
+ encodeCrock,
+ getRandomBytes,
+ HttpStatusCode,
+ j2s,
+ Logger,
+ opEmptySuccess,
+ opKnownHttpFailure,
+ opUnknownFailure,
+ stringToBytes,
+ TalerError,
+ TalerErrorCode,
+} from "@gnu-taler/taler-util";
+import {
+ checkSuccessResponseOrThrow,
+ createPlatformHttpLib,
+ HttpRequestLibrary,
+ readSuccessResponseJsonOrThrow,
+ readTalerErrorResponse,
+} 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<WithdrawalOperationInfo> =>
+ buildCodecForObject<WithdrawalOperationInfo>()
+ .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<string, string> {
+ 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<void> {
+ 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);
+ }
+}
+
+export interface ChallengeContactData {
+ // E-Mail address
+ email?: string;
+
+ // Phone number.
+ phone?: string;
+}
+
+export interface AccountBalance {
+ amount: AmountString;
+ credit_debit_indicator: "credit" | "debit";
+}
+
+export interface RegisterAccountRequest {
+ // Username
+ username: string;
+
+ // Password.
+ password: string;
+
+ // Legal name of the account owner
+ name: string;
+
+ // Defaults to false.
+ is_public?: boolean;
+
+ // Is this a taler exchange account?
+ // If true:
+ // - incoming transactions to the account that do not
+ // have a valid reserve public key are automatically
+ // - the account provides the taler-wire-gateway-api endpoints
+ // Defaults to false.
+ is_taler_exchange?: boolean;
+
+ // Addresses where to send the TAN for transactions.
+ // Currently only used for cashouts.
+ // If missing, cashouts will fail.
+ // In the future, might be used for other transactions
+ // as well.
+ challenge_contact_data?: ChallengeContactData;
+
+ // 'payto' address pointing a bank account
+ // external to the libeufin-bank.
+ // Payments will be sent to this bank account
+ // when the user wants to convert the local currency
+ // back to fiat currency outside libeufin-bank.
+ cashout_payto_uri?: string;
+
+ // Internal payto URI of this bank account.
+ // Used mostly for testing.
+ payto_uri?: string;
+}
+
+export interface AccountData {
+ // Legal name of the account owner.
+ name: string;
+
+ // Available balance on the account.
+ balance: AccountBalance;
+
+ // payto://-URI of the account.
+ payto_uri: string;
+
+ // Number indicating the max debit allowed for the requesting user.
+ debit_threshold: AmountString;
+
+ contact_data?: ChallengeContactData;
+
+ // 'payto' address pointing the bank account
+ // where to send cashouts. This field is optional
+ // because not all the accounts are required to participate
+ // in the merchants' circuit. One example is the exchange:
+ // that never cashouts. Registering these accounts can
+ // be done via the access API.
+ cashout_payto_uri?: string;
+}
+
+export interface ConfirmWithdrawalArgs {
+ withdrawalOperationId: string;
+}
+
+/**
+ * Client for the Taler corebank API.
+ */
+export class TalerCorebankApiClient {
+ 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<string, string> {
+ if (!this.args.auth) {
+ return {};
+ }
+ const authHeaderValue = makeBasicAuthHeader(
+ this.args.auth.username,
+ this.args.auth.password,
+ );
+ return {
+ Authorization: authHeaderValue,
+ };
+ }
+
+ async getAccountBalance(
+ username: string,
+ ): Promise<BankAccountBalanceResponse> {
+ const url = new URL(`accounts/${username}`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ headers: this.makeAuthHeader(),
+ });
+ return readSuccessResponseJsonOrThrow(resp, codecForAny());
+ }
+
+ async getTransactions(username: string): Promise<void> {
+ 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<any> {
+ 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 registerAccountExtended(req: RegisterAccountRequest): Promise<void> {
+ const url = new URL("accounts", this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body: req,
+ headers: this.makeAuthHeader(),
+ });
+
+ if (
+ resp.status !== 200 &&
+ resp.status !== 201 &&
+ resp.status !== 202 &&
+ resp.status !== 204
+ ) {
+ logger.error(`unexpected status ${resp.status} from POST ${url.href}`);
+ logger.error(`${j2s(await resp.json())}`);
+ throw TalerError.fromDetail(
+ TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR,
+ {
+ httpStatusCode: resp.status,
+ },
+ );
+ }
+ }
+
+ /**
+ * Register a new account and return information about it.
+ *
+ * This is a helper, as it does both the registration and the
+ * account info query.
+ */
+ async registerAccount(username: string, password: string): Promise<BankUser> {
+ const url = new URL("accounts", this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body: {
+ username,
+ password,
+ name: username,
+ },
+ headers: this.makeAuthHeader(),
+ });
+ if (
+ resp.status !== 200 &&
+ resp.status !== 201 &&
+ resp.status !== 202 &&
+ resp.status !== 204
+ ) {
+ logger.error(`unexpected status ${resp.status} from POST ${url.href}`);
+ logger.error(`${j2s(await resp.json())}`);
+ throw TalerError.fromDetail(
+ TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR,
+ {
+ httpStatusCode: resp.status,
+ },
+ );
+ }
+ // FIXME: Corebank should directly return this info!
+ const infoUrl = new URL(`accounts/${username}`, this.baseUrl);
+ const infoResp = await this.httpLib.fetch(infoUrl.href, {
+ headers: {
+ Authorization: makeBasicAuthHeader(username, password),
+ },
+ });
+ // FIXME: Validate!
+ const acctInfo: AccountData = await readSuccessResponseJsonOrThrow(
+ infoResp,
+ codecForAny(),
+ );
+ return {
+ password,
+ username,
+ accountPaytoUri: acctInfo.payto_uri,
+ };
+ }
+
+ async createRandomBankUser(): Promise<BankUser> {
+ const username = "user-" + encodeCrock(getRandomBytes(10)).toLowerCase();
+ const password = "pw-" + encodeCrock(getRandomBytes(10)).toLowerCase();
+ return await this.registerAccount(username, password);
+ }
+
+ async createWithdrawalOperation(
+ user: string,
+ amount: string,
+ ): Promise<WithdrawalOperationInfo> {
+ 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: ConfirmWithdrawalArgs,
+ ) {
+ const url = new URL(
+ `accounts/${username}/withdrawals/${wopi.withdrawalOperationId}/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(),
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ async abortWithdrawalOperation(wopi: WithdrawalOperationInfo): Promise<void> {
+ const url = new URL(
+ `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());
+ }
+}