diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx index 91c5da718..35fb94a6c 100644 --- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx @@ -52,6 +52,41 @@ export function WithdrawalQRCode({ if (result.loading) { return ; } + if (result.type === ErrorType.CLIENT && result.status === HttpStatusCode.NotFound) { + return
+
+
+ +
+ +
+ +
+

+ + This operation is not known by the server. The operation id is wrong or the + server deleted the operation information before reaching here. + +

+
+
+
+
+ +
+
+ } return handleNotOkResult(i18n)(result); } const { data } = result; diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts index 65a19959a..c16200933 100644 --- a/packages/taler-harness/src/harness/harness.ts +++ b/packages/taler-harness/src/harness/harness.ts @@ -654,6 +654,9 @@ export class FakebankService return this.baseUrl; } + // FIXME: Why do we have this function at all? + // We now have a unified corebank API, we should just use that + // to create bank accounts, also for the exchange. async createExchangeAccount( accountName: string, password: string, diff --git a/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts b/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts index 449142809..def2462e0 100644 --- a/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts +++ b/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts @@ -136,20 +136,22 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) { }, ); - await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); - let p: PendingOperationsResponse; p = await walletClient.call(WalletApiOperation.GetPendingOperations, {}); console.log("pending operations after first time travel"); console.log(JSON.stringify(p, undefined, 2)); - await withdrawViaBankV2(t, { + await walletClient.call(WalletApiOperation.TestingWaitTasksProcessed, {}); + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); + + const wres2 = await withdrawViaBankV2(t, { walletClient, bank, exchange, amount: "TESTKUDOS:20", }); + await wres2.withdrawalFinishedCond; await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); @@ -165,12 +167,13 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) { }, ); + await walletClient.call(WalletApiOperation.TestingWaitTasksProcessed, {}); + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); + // At this point, the original coins should've been refreshed. // It would be too late to refresh them now, as we're past // the two year deposit expiration. - await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); - const orderResp = await merchantClient.createOrder({ order: { fulfillment_url: "http://example.com", @@ -195,7 +198,7 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) { t.assertTrue(r.status === PreparePayResultType.PaymentPossible); const cpr = await walletClient.call(WalletApiOperation.ConfirmPay, { - proposalId: r.proposalId, + transactionId: r.transactionId, }); t.assertTrue(cpr.type === ConfirmPayResultType.Done); diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-fakebank.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-fakebank.ts index e3057451e..e26d9f964 100644 --- a/packages/taler-harness/src/integrationtests/test-withdrawal-fakebank.ts +++ b/packages/taler-harness/src/integrationtests/test-withdrawal-fakebank.ts @@ -55,12 +55,14 @@ export async function runWithdrawalFakebankTest(t: GlobalTestState) { accountName: "exchange", accountPassword: "x", wireGatewayApiBaseUrl: new URL( - "/accounts/exchange/taler-wire-gateway", + "/accounts/exchange/taler-wire-gateway/", bank.baseUrl, ).href, accountPaytoUri: "payto://x-taler-bank/localhost/exchange", }); + await bank.createExchangeAccount("exchange", "x"); + await bank.start(); await bank.pingUntilAvailable(); @@ -93,8 +95,6 @@ export async function runWithdrawalFakebankTest(t: GlobalTestState) { const balResp = await wallet.client.call(WalletApiOperation.GetBalances, {}); t.assertAmountEquals("TESTKUDOS:9.72", balResp.balances[0].available); - - await t.shutdown(); } runWithdrawalFakebankTest.suites = ["wallet"]; diff --git a/packages/taler-util/src/http-client/bank-core.ts b/packages/taler-util/src/http-client/bank-core.ts new file mode 100644 index 000000000..c77f9ddda --- /dev/null +++ b/packages/taler-util/src/http-client/bank-core.ts @@ -0,0 +1,489 @@ +/* + 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 + */ + +import { + AmountJson, + Amounts, + Logger +} from "@gnu-taler/taler-util"; +import { + createPlatformHttpLib, + expectSuccessResponseOrThrow, + HttpRequestLibrary, + readSuccessResponseJsonOrThrow +} from "@gnu-taler/taler-util/http"; +import { AccessToken, codecForAccountData, codecForBankAccountCreateWithdrawalResponse, codecForBankAccountGetWithdrawalResponse, codecForBankAccountTransactionInfo, codecForBankAccountTransactionsResponse, codecForCashoutConversionResponse, codecForCashoutPending, codecForCashouts, codecForCashoutStatusResponse, codecForConversionRatesResponse, codecForCoreBankConfig, codecForGlobalCashouts, codecForListBankAccountsResponse, codecForMonitorResponse, codecForPublicAccountsResponse, codecForTokenSuccessResponse, TalerAuthentication, TalerCorebankApi } from "./types.js"; +import { addPaginationParams, makeBasicAuthHeader, makeBearerTokenAuthHeader, PaginationParams, UserAndPassword, UserAndToken } from "./utils.js"; +import { TalerRevenueHttpClient } from "./bank-revenue.js"; +import { TalerWireGatewayHttpClient } from "./bank-wire.js"; +import { TalerBankIntegrationHttpClient } from "./bank-integration.js"; + +const logger = new Logger("http-client/core-bank.ts"); + +export class TalerCoreBankHttpClient { + httpLib: HttpRequestLibrary; + + constructor( + private baseUrl: string, + httpClient?: HttpRequestLibrary, + ) { + this.httpLib = httpClient ?? createPlatformHttpLib(); + } + + /** + * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-token + * + * @returns + */ + async createAccessToken( + auth: UserAndPassword, + body: TalerAuthentication.TokenRequest, + ): Promise { + const url = new URL(`accounts/${auth.username}/token`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + headers: { + Authorization: makeBasicAuthHeader(auth.username, auth.password), + }, + body + }); + return readSuccessResponseJsonOrThrow(resp, codecForTokenSuccessResponse()); + } + + async deleteAccessToken( + auth: UserAndToken, + ): Promise { + const url = new URL(`accounts/${auth.username}/token`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "DELETE", + headers: { + Authorization: makeBearerTokenAuthHeader(auth.token), + } + }); + return expectSuccessResponseOrThrow(resp); + } + + /** + * https://docs.taler.net/core/api-corebank.html#get--accounts-$USERNAME + * + */ + async getConfig(): Promise { + const url = new URL(`config`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "GET" + }); + return readSuccessResponseJsonOrThrow(resp, codecForCoreBankConfig()); + } + + // + // ACCOUNTS + // + + /** + * https://docs.taler.net/core/api-corebank.html#post--accounts + * + */ + async createAccount(auth: AccessToken, body: TalerCorebankApi.RegisterAccountRequest): Promise { + const url = new URL(`accounts`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body, + headers: { + Authorization: makeBearerTokenAuthHeader(auth) + }, + }); + return expectSuccessResponseOrThrow(resp); + } + + /** + * https://docs.taler.net/core/api-corebank.html#delete--accounts-$USERNAME + * + */ + async deleteAccount(auth: UserAndToken): Promise { + const url = new URL(`accounts/${auth.username}`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "DELETE", + headers: { + Authorization: makeBearerTokenAuthHeader(auth.token) + }, + }); + return expectSuccessResponseOrThrow(resp); + } + + /** + * https://docs.taler.net/core/api-corebank.html#patch--accounts-$USERNAME + * + */ + async updateAccount(auth: UserAndToken, body: TalerCorebankApi.AccountReconfiguration): Promise { + const url = new URL(`accounts/${auth.username}`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "PATCH", + body, + headers: { + Authorization: makeBearerTokenAuthHeader(auth.token) + }, + }); + return expectSuccessResponseOrThrow(resp); + } + + /** + * https://docs.taler.net/core/api-corebank.html#patch--accounts-$USERNAME-auth + * + */ + async updatePassword(auth: UserAndToken, body: TalerCorebankApi.AccountPasswordChange): Promise { + const url = new URL(`accounts/${auth.username}`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "PATCH", + body, + headers: { + Authorization: makeBearerTokenAuthHeader(auth.token) + }, + }); + return expectSuccessResponseOrThrow(resp); + } + + /** + * https://docs.taler.net/core/get-$BANK_API_BASE_URL-public-accounts + * + */ + async getPublicAccounts(): Promise { + const url = new URL(`public-accounts`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers: { + }, + }); + return readSuccessResponseJsonOrThrow(resp, codecForPublicAccountsResponse()); + } + + /** + * https://docs.taler.net/core/api-corebank.html#get--accounts + * + */ + async getAccounts(auth: AccessToken): Promise { + const url = new URL(`accounts`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers: { + Authorization: makeBearerTokenAuthHeader(auth) + }, + }); + return readSuccessResponseJsonOrThrow(resp, codecForListBankAccountsResponse()); + } + + /** + * https://docs.taler.net/core/api-corebank.html#get--accounts-$USERNAME + * + */ + async getAccount(auth: UserAndToken): Promise { + const url = new URL(`accounts/${auth.username}`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers: { + Authorization: makeBearerTokenAuthHeader(auth.token) + }, + }); + return readSuccessResponseJsonOrThrow(resp, codecForAccountData()); + } + + // + // TRANSACTIONS + // + + /** + * https://docs.taler.net/core/api-corebank.html#get-$BANK_API_BASE_URL-accounts-$account_name-transactions + * + */ + async getTransactions(auth: UserAndToken, pagination?: PaginationParams): Promise { + const url = new URL(`accounts/${auth.username}/transactions`, this.baseUrl); + addPaginationParams(url, pagination) + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers: { + Authorization: makeBearerTokenAuthHeader(auth.token) + }, + }); + return readSuccessResponseJsonOrThrow(resp, codecForBankAccountTransactionsResponse()); + } + + /** + * https://docs.taler.net/core/api-corebank.html#get-$BANK_API_BASE_URL-accounts-$account_name-transactions-$transaction_id + * + */ + async getTransactionById(auth: UserAndToken, txid: number): Promise { + const url = new URL(`accounts/${auth.username}/transactions/${String(txid)}`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers: { + Authorization: makeBearerTokenAuthHeader(auth.token) + }, + }); + return readSuccessResponseJsonOrThrow(resp, codecForBankAccountTransactionInfo()); + } + + /** + * https://docs.taler.net/core/api-corebank.html#post-$BANK_API_BASE_URL-accounts-$account_name-transactions + * + */ + async createTransaction(auth: UserAndToken, body: TalerCorebankApi.CreateBankAccountTransactionCreate): Promise { + const url = new URL(`accounts/${auth.username}/transactions`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + headers: { + Authorization: makeBearerTokenAuthHeader(auth.token) + }, + body, + }); + return expectSuccessResponseOrThrow(resp); + } + + // + // WITHDRAWALS + // + + /** + * https://docs.taler.net/core/api-corebank.html#post-$BANK_API_BASE_URL-accounts-$account_name-withdrawals + * + */ + async createWithdrawal(auth: UserAndToken, body: TalerCorebankApi.BankAccountCreateWithdrawalRequest): Promise { + const url = new URL(`accounts/${auth.username}/withdrawals`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + headers: { + Authorization: makeBearerTokenAuthHeader(auth.token) + }, + body, + }); + return readSuccessResponseJsonOrThrow(resp, codecForBankAccountCreateWithdrawalResponse()); + } + + /** + * https://docs.taler.net/core/api-corebank.html#post-$BANK_API_BASE_URL-accounts-$account_name-withdrawals + * + */ + async getWithdrawalById(wid: string): Promise { + const url = new URL(`withdrawals/${wid}`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + }); + return readSuccessResponseJsonOrThrow(resp, codecForBankAccountGetWithdrawalResponse()); + } + + /** + * https://docs.taler.net/core/api-corebank.html#post-$BANK_API_BASE_URL-withdrawals-$withdrawal_id-abort + * + */ + async abortWithdrawalById(wid: string): Promise { + const url = new URL(`withdrawals/${wid}/abort`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + }); + return expectSuccessResponseOrThrow(resp); + } + + /** + * https://docs.taler.net/core/api-corebank.html#post-$BANK_API_BASE_URL-withdrawals-$withdrawal_id-confirm + * + */ + async confirmWithdrawalById(wid: string): Promise { + const url = new URL(`withdrawals/${wid}/confirm`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + }); + return expectSuccessResponseOrThrow(resp); + } + + // + // CASHOUTS + // + + /** + * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-cashouts + * + */ + async createCashout(auth: UserAndToken, body: TalerCorebankApi.CashoutRequest): Promise { + const url = new URL(`accounts/${auth.username}/cashouts`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + headers: { + Authorization: makeBearerTokenAuthHeader(auth.token) + }, + body, + }); + return readSuccessResponseJsonOrThrow(resp, codecForCashoutPending()); + } + + /** + * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-cashouts-$CASHOUT_ID-abort + * + */ + async abortCashoutById(auth: UserAndToken, cid: string): Promise { + const url = new URL(`accounts/${auth.username}/cashouts/${cid}/abort`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + headers: { + Authorization: makeBearerTokenAuthHeader(auth.token) + }, + }); + return expectSuccessResponseOrThrow(resp); + } + + /** + * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-cashouts-$CASHOUT_ID-confirm + * + */ + async confirmCashoutById(auth: UserAndToken, cid: string, body: TalerCorebankApi.CashoutConfirmRequest): Promise { + const url = new URL(`accounts/${auth.username}/cashouts/${cid}/confirm`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + headers: { + Authorization: makeBearerTokenAuthHeader(auth.token) + }, + body, + }); + return expectSuccessResponseOrThrow(resp); + } + + /** + * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-cashouts-$CASHOUT_ID-confirm + * + */ + async getCashoutRate(conversion: { debit?: AmountJson, credit?: AmountJson }): Promise { + const url = new URL(`cashout-rate`, this.baseUrl); + if (conversion.debit) { + url.searchParams.set("amount_debit", Amounts.stringify(conversion.debit)) + } + if (conversion.credit) { + url.searchParams.set("amount_debit", Amounts.stringify(conversion.credit)) + } + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + }); + return readSuccessResponseJsonOrThrow(resp, codecForCashoutConversionResponse()); + } + + /** + * https://docs.taler.net/core/api-corebank.html#get--accounts-$USERNAME-cashouts + * + */ + async getAccountCashouts(auth: UserAndToken): Promise { + const url = new URL(`accounts/${auth.username}/cashouts`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers: { + Authorization: makeBearerTokenAuthHeader(auth.token) + }, + }); + return readSuccessResponseJsonOrThrow(resp, codecForCashouts()); + } + + /** + * https://docs.taler.net/core/api-corebank.html#get--cashouts + * + */ + async getGlobalCashouts(auth: AccessToken): Promise { + const url = new URL(`cashouts`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers: { + Authorization: makeBearerTokenAuthHeader(auth) + }, + }); + return readSuccessResponseJsonOrThrow(resp, codecForGlobalCashouts()); + } + + /** + * https://docs.taler.net/core/api-corebank.html#get--accounts-$USERNAME-cashouts-$CASHOUT_ID + * + */ + async getCashoutById(auth: UserAndToken, cid: string): Promise { + const url = new URL(`accounts/${auth.username}/cashouts/${cid}`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers: { + Authorization: makeBearerTokenAuthHeader(auth.token) + }, + }); + return readSuccessResponseJsonOrThrow(resp, codecForCashoutStatusResponse()); + } + + // + // CONVERSION RATE + // + + /** + * https://docs.taler.net/core/api-corebank.html#get--conversion-rates + * + */ + async getConversionRates(): Promise { + const url = new URL(`conversion-rates`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + }); + return readSuccessResponseJsonOrThrow(resp, codecForConversionRatesResponse()); + } + + // + // MONITOR + // + + /** + * https://docs.taler.net/core/api-corebank.html#get--monitor + * + */ + async getMonitor(params: { timeframe: TalerCorebankApi.MonitorTimeframeParam, which: number }): Promise { + const url = new URL(`monitor`, this.baseUrl); + url.searchParams.set("timeframe", params.timeframe.toString()) + url.searchParams.set("which", String(params.which)) + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + }); + return readSuccessResponseJsonOrThrow(resp, codecForMonitorResponse()); + } + + // + // Others API + // + + /** + * https://docs.taler.net/core/api-corebank.html#taler-bank-integration-api + * + */ + getIntegrationAPI(): TalerBankIntegrationHttpClient { + const url = new URL(`taler-integration`, this.baseUrl); + return new TalerBankIntegrationHttpClient(url.href, this.httpLib) + } + + /** + * https://docs.taler.net/core/api-corebank.html#taler-bank-integration-api + * + */ + getWireGatewayAPI(username: string): TalerWireGatewayHttpClient { + const url = new URL(`accounts/${username}/taler-wire-gateway`, this.baseUrl); + return new TalerWireGatewayHttpClient(url.href, username, this.httpLib) + } + + /** + * https://docs.taler.net/core/api-corebank.html#taler-bank-integration-api + * + */ + getRevenueAPI(username: string): TalerRevenueHttpClient { + const url = new URL(`accounts/${username}/taler-revenue`, this.baseUrl); + return new TalerRevenueHttpClient(url.href, username, this.httpLib,) + } + +} + diff --git a/packages/taler-util/src/http-client/bank-integration.ts b/packages/taler-util/src/http-client/bank-integration.ts new file mode 100644 index 000000000..cd6462417 --- /dev/null +++ b/packages/taler-util/src/http-client/bank-integration.ts @@ -0,0 +1,47 @@ +import { HttpRequestLibrary, readSuccessResponseJsonOrThrow } from "../http-common.js"; +import { createPlatformHttpLib } from "../http.js"; +import { + TalerBankIntegrationApi, + codecForBankWithdrawalOperationPostResponse, + codecForBankWithdrawalOperationStatus +} from "./types.js"; + +export class TalerBankIntegrationHttpClient { + httpLib: HttpRequestLibrary; + + constructor( + private baseUrl: string, + httpClient?: HttpRequestLibrary, + ) { + this.httpLib = httpClient ?? createPlatformHttpLib(); + } + + /** + * https://docs.taler.net/core/api-bank-integration.html#get-$BANK_API_BASE_URL-withdrawal-operation-$wopid + * + */ + async getWithdrawalOperationById(woid: string, timeoutMs?: number): Promise { + const url = new URL(`withdrawal-operation/${woid}`, this.baseUrl); + if (timeoutMs) { + url.searchParams.set("long_poll_ms", String(timeoutMs)) + } + const resp = await this.httpLib.fetch(url.href, { + method: "GET" + }); + return readSuccessResponseJsonOrThrow(resp, codecForBankWithdrawalOperationStatus()); + } + + /** + * https://docs.taler.net/core/api-bank-integration.html#post-$BANK_API_BASE_URL-withdrawal-operation-$wopid + * + */ + async completeWithdrawalOperationById(woid: string): Promise { + const url = new URL(`withdrawal-operation/${woid}`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + }); + return readSuccessResponseJsonOrThrow(resp, codecForBankWithdrawalOperationPostResponse()); + } + +} + diff --git a/packages/taler-util/src/http-client/bank-revenue.ts b/packages/taler-util/src/http-client/bank-revenue.ts new file mode 100644 index 000000000..99ff71457 --- /dev/null +++ b/packages/taler-util/src/http-client/bank-revenue.ts @@ -0,0 +1,32 @@ +import { HttpRequestLibrary, makeBasicAuthHeader, readSuccessResponseJsonOrThrow } from "../http-common.js"; +import { createPlatformHttpLib } from "../http.js"; +import { TalerRevenueApi, codecForMerchantIncomingHistory } from "./types.js"; +import { UserAndPassword } from "./utils.js"; + +export class TalerRevenueHttpClient { + httpLib: HttpRequestLibrary; + + constructor( + private baseUrl: string, + private username: string, + httpClient?: HttpRequestLibrary, + ) { + this.httpLib = httpClient ?? createPlatformHttpLib(); + } + + /** + * https://docs.taler.net/core/api-bank-revenue.html#get-$BASE_URL-history + * + * @returns + */ + async getHistory(auth: string): Promise { + const url = new URL(`history`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers: { + Authorization: makeBasicAuthHeader(this.username, auth), + } + }); + return readSuccessResponseJsonOrThrow(resp, codecForMerchantIncomingHistory()); + } +} \ No newline at end of file diff --git a/packages/taler-util/src/http-client/bank-wire.ts b/packages/taler-util/src/http-client/bank-wire.ts new file mode 100644 index 000000000..9f2b859ed --- /dev/null +++ b/packages/taler-util/src/http-client/bank-wire.ts @@ -0,0 +1,98 @@ +import { HttpRequestLibrary, makeBasicAuthHeader, readSuccessResponseJsonOrThrow } from "../http-common.js"; +import { createPlatformHttpLib } from "../http.js"; +import { TalerWireGatewayApi, codecForAddIncomingResponse, codecForIncomingHistory, codecForOutgoingHistory, codecForTransferResponse } from "./types.js"; +import { PaginationParams, UserAndPassword, addPaginationParams } from "./utils.js"; + +/** + * The API is used by the exchange to trigger transactions and query + * incoming transactions, as well as by the auditor to query incoming + * and outgoing transactions. + * + * https://docs.taler.net/core/api-bank-wire.html + */ +export class TalerWireGatewayHttpClient { + httpLib: HttpRequestLibrary; + + constructor( + private baseUrl: string, + private username: string, + httpClient?: HttpRequestLibrary, + ) { + this.httpLib = httpClient ?? createPlatformHttpLib(); + } + + /** + * https://docs.taler.net/core/api-bank-wire.html#post-$BASE_URL-transfer + * + */ + async transfer( + auth: string, + body: TalerWireGatewayApi.TransferRequest, + ): Promise { + const url = new URL(`transfer`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + headers: { + Authorization: makeBasicAuthHeader(this.username, auth), + }, + body + }); + return readSuccessResponseJsonOrThrow(resp, codecForTransferResponse()); + } + + /** + * https://docs.taler.net/core/api-bank-wire.html#get-$BASE_URL-history-incoming + * + */ + async getHistoryIncoming( + auth: string, + pagination?: PaginationParams + ): Promise { + const url = new URL(`history/incoming`, this.baseUrl); + addPaginationParams(url, pagination) + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers: { + Authorization: makeBasicAuthHeader(this.username, auth), + } + }); + return readSuccessResponseJsonOrThrow(resp, codecForIncomingHistory()); + } + /** + * https://docs.taler.net/core/api-bank-wire.html#get-$BASE_URL-history-outgoing + * + */ + async getHistoryOutgoing( + auth: string, + pagination?: PaginationParams + ): Promise { + const url = new URL(`history/outgoing`, this.baseUrl); + addPaginationParams(url, pagination) + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers: { + Authorization: makeBasicAuthHeader(this.username, auth), + } + }); + return readSuccessResponseJsonOrThrow(resp, codecForOutgoingHistory()); + } + /** + * https://docs.taler.net/core/api-bank-wire.html#post-$BASE_URL-admin-add-incoming + * + */ + async addIncoming( + auth: string, + body: TalerWireGatewayApi.AddIncomingRequest, + ): Promise { + const url = new URL(`admin/add-incoming`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + headers: { + Authorization: makeBasicAuthHeader(this.username, auth), + }, + body + }); + return readSuccessResponseJsonOrThrow(resp, codecForAddIncomingResponse()); + } +} + diff --git a/packages/taler-util/src/http-client/index.ts b/packages/taler-util/src/http-client/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/taler-util/src/http-client/types.ts b/packages/taler-util/src/http-client/types.ts new file mode 100644 index 000000000..66ac39f59 --- /dev/null +++ b/packages/taler-util/src/http-client/types.ts @@ -0,0 +1,1183 @@ +import { codecForAmountString } from "../amounts.js"; +import { Codec, buildCodecForObject, buildCodecForUnion, codecForBoolean, codecForConstString, codecForEither, codecForList, codecForMap, codecForNumber, codecForString, codecOptional } from "../codec.js"; +import { codecForTimestamp } from "../time.js"; + + +/// +/// HASH +/// + +// 64-byte hash code. +type HashCode = string; + +// 32-byte hash code. +type ShortHashCode = string; + +// 16-byte salt. +type WireSalt = string; + +type SHA256HashCode = ShortHashCode; + +type SHA512HashCode = HashCode; + +// 32-byte nonce value, must only be used once. +type CSNonce = string; + +// 32-byte nonce value, must only be used once. +type RefreshMasterSeed = string; + +// 32-byte value representing a point on Curve25519. +type Cs25519Point = string; + +// 32-byte value representing a scalar multiplier +// for scalar operations on points on Curve25519. +type Cs25519Scalar = string; + +/// +/// KEYS +/// + +// 16-byte access token used to authorize access. +type ClaimToken = string; + +// EdDSA and ECDHE public keys always point on Curve25519 +// and represented using the standard 256 bits Ed25519 compact format, +// converted to Crockford Base32. +type EddsaPublicKey = string; + +// EdDSA and ECDHE public keys always point on Curve25519 +// and represented using the standard 256 bits Ed25519 compact format, +// converted to Crockford Base32. +type EddsaPrivateKey = string; + +// Edx25519 public keys are points on Curve25519 and represented using the +// standard 256 bits Ed25519 compact format converted to Crockford +// Base32. +type Edx25519PublicKey = string; + +// Edx25519 private keys are always points on Curve25519 +// and represented using the standard 256 bits Ed25519 compact format, +// converted to Crockford Base32. +type Edx25519PrivateKey = string; + +// EdDSA and ECDHE public keys always point on Curve25519 +// and represented using the standard 256 bits Ed25519 compact format, +// converted to Crockford Base32. +type EcdhePublicKey = string; + +// Point on Curve25519 represented using the standard 256 bits Ed25519 compact format, +// converted to Crockford Base32. +type CsRPublic = string; + +// EdDSA and ECDHE public keys always point on Curve25519 +// and represented using the standard 256 bits Ed25519 compact format, +// converted to Crockford Base32. +type EcdhePrivateKey = string; + +type CoinPublicKey = EddsaPublicKey; + +// RSA public key converted to Crockford Base32. +type RsaPublicKey = string; + +type Integer = number; + +type WireTransferIdentifierRawP = string; +// Subset of numbers: Integers in the +// inclusive range 0 .. (2^53 - 1). +type SafeUint64 = number; + +// The string must be a data URL according to RFC 2397 +// with explicit mediatype and base64 parameters. +// +// data:;base64, +// +// Supported mediatypes are image/jpeg and image/png. +// Invalid strings will be rejected by the wallet. +type ImageDataUrl = string; + + +// :. +type Amount = string; + +type WadId = string; + +interface Timestamp { + // Seconds since epoch, or the special + // value "never" to represent an event that will + // never happen. + t_s: number | "never"; +} + +interface RelativeTime { + // Duration in microseconds or "forever" + // to represent an infinite duration. Numeric + // values are capped at 2^53 - 1 inclusive. + d_us: number | "forever"; +} + +export interface LoginToken { + token: AccessToken, + expiration: Timestamp, +} +// token used to get loginToken +// must forget after used +declare const __ac_token: unique symbol; +export type AccessToken = string & { + [__ac_token]: true; +}; + +export namespace TalerAuthentication { + + export interface TokenRequest { + // Service-defined scope for the token. + // Typical scopes would be "readonly" or "readwrite". + scope: string; + + // Server may impose its own upper bound + // on the token validity duration + duration?: RelativeTime; + + // Is the token refreshable into a new token during its + // validity? + // Refreshable tokens effectively provide indefinite + // access if they are refreshed in time. + refreshable?: boolean; + } + + export interface TokenSuccessResponse { + // Expiration determined by the server. + // Can be based on the token_duration + // from the request, but ultimately the + // server decides the expiration. + expiration: Timestamp; + + // Opque access token. + access_token: AccessToken; + } +} + +export interface CurrencySpecification { + + // Name of the currency. + name: string; + + // Decimal separator for fractional digits. + decimal_separator: string; + + // how many digits the user may enter after the decimal_separator + num_fractional_input_digits: Integer; + + // Number of fractional digits to render in normal font and size. + num_fractional_normal_digits: Integer; + + // Number of fractional digits to render always, if needed by + // padding with zeros. + num_fractional_trailing_zero_digits: Integer; + + // Whether the currency name should be rendered before (true) or + // after (false) the numeric value + is_currency_name_leading: boolean; + + // map of powers of 10 to alternative currency names / symbols, must + // always have an entry under "0" that defines the base name, + // e.g. "0 => €" or "3 => k€". For BTC, would be "0 => BTC, -3 => mBTC". + // Communicates the currency symbol to be used. + alt_unit_names: { [log10: string]: string }; +} + +export const codecForAccessToken = codecForString as () => Codec; +export const codecForTokenSuccessResponse = + (): Codec => + buildCodecForObject() + .property("access_token", codecForAccessToken()) + .property("expiration", codecForTimestamp) + .build("TalerAuthentication.TokenSuccessResponse") + +export const codecForCurrencySpecificiation = + (): Codec => + buildCodecForObject() + .property("name", codecForString()) + .property("decimal_separator", codecForString()) + .property("num_fractional_input_digits", codecForNumber()) + .property("num_fractional_normal_digits", codecForNumber()) + .property("num_fractional_trailing_zero_digits", codecForNumber()) + .property("is_currency_name_leading", codecForBoolean()) + .property("alt_unit_names", codecForMap(codecForString())) + .build("CurrencySpecification") + +export const codecForCoreBankConfig = + (): Codec => + buildCodecForObject() + .property("name", codecForString()) + .property("version", codecForString()) + .property("have_cashout", codecOptional(codecForBoolean())) + .property("currency", codecForCurrencySpecificiation()) + .property("fiat_currency", codecOptional(codecForCurrencySpecificiation())) + .build("TalerCorebankApi.Config") + +const codecForBalance = (): Codec => + buildCodecForObject() + .property("amount", codecForAmountString()) + .property("credit_debit_indicator", codecForEither(codecForConstString("credit"), codecForConstString("debit"))) + .build("TalerCorebankApi.Balance") + +const codecForPublicAccount = (): Codec => + buildCodecForObject() + .property("account_name", codecForString()) + .property("balance", codecForBalance()) + .property("payto_uri", codecForPaytoURI()) + .build("TalerCorebankApi.PublicAccount") + +export const codecForPublicAccountsResponse = + (): Codec => + buildCodecForObject() + .property("public_accounts", codecForList(codecForPublicAccount())) + .build("TalerCorebankApi.PublicAccountsResponse") + + +export const codecForAccountMinimalData = + (): Codec => + buildCodecForObject() + .property("balance", codecForBalance()) + .property("debit_threshold", codecForAmountString()) + .property("name", codecForString()) + .property("username", codecForString()) + .build("TalerCorebankApi.AccountMinimalData") + +export const codecForListBankAccountsResponse = + (): Codec => + buildCodecForObject() + .property("accounts", codecForList(codecForAccountMinimalData())) + .build("TalerCorebankApi.ListBankAccountsResponse") + +export const codecForAccountData = + (): Codec => + buildCodecForObject() + .property("name", codecForString()) + .property("balance", codecForBalance()) + .property("payto_uri", codecForPaytoURI()) + .property("debit_threshold", codecForAmountString()) + .property("contact_data", codecOptional(codecForChallengeContactData())) + .property("cashout_payto_uri", codecOptional(codecForPaytoURI())) + .build("TalerCorebankApi.AccountData") + + +export const codecForChallengeContactData = + (): Codec => + buildCodecForObject() + .property("email", codecOptional(codecForString())) + .property("phone", codecOptional(codecForString())) + .build("TalerCorebankApi.ChallengeContactData") + +export const codecForBankAccountTransactionsResponse = + (): Codec => + buildCodecForObject() + .property("transactions", codecForList(codecForBankAccountTransactionInfo())) + .build("TalerCorebankApi.BankAccountTransactionsResponse"); + +export const codecForBankAccountTransactionInfo = + (): Codec => + buildCodecForObject() + .property("amount", codecForAmountString()) + .property("creditor_payto_uri", codecForPaytoURI()) + .property("date", codecForTimestamp) + .property("debtor_payto_uri", codecForPaytoURI()) + .property("direction", codecForEither(codecForConstString("debit"), codecForConstString("credit"))) + .property("row_id", codecForNumber()) + .property("subject", codecForString()) + .build("TalerCorebankApi.BankAccountTransactionInfo"); + +export const codecForBankAccountCreateWithdrawalResponse = + (): Codec => + buildCodecForObject() + .property("taler_withdraw_uri", codecForTalerWithdrawalURI()) + .property("withdrawal_id", codecForString()) + .build("TalerCorebankApi.BankAccountCreateWithdrawalResponse"); + +export const codecForBankAccountGetWithdrawalResponse = + (): Codec => + buildCodecForObject() + .property("aborted", codecForBoolean()) + .property("amount", codecForAmountString()) + .property("confirmation_done", codecForBoolean()) + .property("selected_exchange_account", codecOptional(codecForString())) + .property("selected_reserve_pub", codecOptional(codecForString())) + .property("selection_done", (codecForBoolean())) + .build("TalerCorebankApi.BankAccountGetWithdrawalResponse"); + +export const codecForCashoutPending = + (): Codec => + buildCodecForObject() + .property("cashout_id", codecForString()) + .build("TalerCorebankApi.CashoutPending"); + +export const codecForCashoutConversionResponse = + (): Codec => + buildCodecForObject() + .property("amount_credit", codecForAmountString()) + .property("amount_debit", codecForAmountString()) + .build("TalerCorebankApi.CashoutConversionResponse"); + +export const codecForCashouts = + (): Codec => + buildCodecForObject() + .property("cashouts", codecForList(codecForCashoutInfo())) + .build("TalerCorebankApi.Cashouts"); + +export const codecForCashoutInfo = + (): Codec => + buildCodecForObject() + .property("cashout_id", codecForString()) + .property("status", codecForEither(codecForConstString("pending"), codecForConstString("confirmed"),)) + .build("TalerCorebankApi.CashoutInfo"); + +export const codecForGlobalCashouts = + (): Codec => + buildCodecForObject() + .property("cashouts", codecForList(codecForGlobalCashoutInfo())) + .build("TalerCorebankApi.GlobalCashouts"); + +export const codecForGlobalCashoutInfo = + (): Codec => + buildCodecForObject() + .property("cashout_id", codecForString()) + .property("username", codecForString()) + .property("status", codecForEither(codecForConstString("pending"), codecForConstString("confirmed"),)) + .build("TalerCorebankApi.GlobalCashoutInfo"); + +export const codecForCashoutStatusResponse = + (): Codec => + buildCodecForObject() + .property("amount_credit", codecForAmountString()) + .property("amount_debit", codecForAmountString()) + .property("confirmation_time", codecForTimestamp) + .property("creation_time", codecForTimestamp) + .property("credit_payto_uri", codecForPaytoURI()) + .property("status", codecForEither(codecForConstString("pending"), codecForConstString("confirmed"))) + .property("subject", codecForString()) + .build("TalerCorebankApi.CashoutStatusResponse"); + +export const codecForConversionRatesResponse = + (): Codec => + buildCodecForObject() + .property("buy_at_ratio", codecForDecimalNumber()) + .property("buy_in_fee", codecForDecimalNumber()) + .property("sell_at_ratio", codecForDecimalNumber()) + .property("sell_out_fee", codecForDecimalNumber()) + .build("TalerCorebankApi.ConversionRatesResponse"); + +export const codecForMonitorResponse = + (): Codec => + buildCodecForObject() + .property("cashinCount", codecForNumber()) + .property("cashinExternalVolume", codecForAmountString()) + .property("cashoutCount", codecForNumber()) + .property("cashoutExternalVolume", codecForAmountString()) + .property("talerPayoutCount", codecForNumber()) + .property("talerPayoutInternalVolume", codecForAmountString()) + .build("TalerCorebankApi.MonitorResponse"); + +export const codecForBankVersion = + (): Codec => + buildCodecForObject() + .property("currency", codecForCurrencyName()) + .property("currency_specification", codecForCurrencySpecificiation()) + .property("name", codecForConstString("taler-bank-integration")) + .property("version", codecForLibtoolVersion()) + .build("TalerBankIntegrationApi.BankVersion"); + +export const codecForBankWithdrawalOperationStatus = + (): Codec => + buildCodecForObject() + .property("aborted", codecForBoolean()) + .property("amount", codecForAmountString()) + .property("confirm_transfer_url", codecOptional(codecForURL())) + .property("selection_done", codecForBoolean()) + .property("sender_wire", codecForPaytoURI()) + .property("suggested_exchange", codecOptional(codecForString())) + .property("transfer_done", codecForBoolean()) + .property("wire_types", codecForList(codecForString())) + .build("TalerBankIntegrationApi.BankWithdrawalOperationStatus"); + +export const codecForBankWithdrawalOperationPostResponse = + (): Codec => + buildCodecForObject() + .property("confirm_transfer_url", codecOptional(codecForURL())) + .property("transfer_done", codecForBoolean()) + .build("TalerBankIntegrationApi.BankWithdrawalOperationPostResponse"); + +export const codecForMerchantIncomingHistory = + (): Codec => + buildCodecForObject() + .property("credit_account", codecForPaytoURI()) + .property("incoming_transactions", codecForList(codecForMerchantIncomingBankTransaction())) + .build("TalerRevenueApi.MerchantIncomingHistory"); + +export const codecForMerchantIncomingBankTransaction = + (): Codec => + buildCodecForObject() + .property("amount", codecForAmountString()) + .property("date", codecForTimestamp) + .property("debit_account", codecForPaytoURI()) + .property("exchange_url", codecForURL()) + .property("row_id", codecForNumber()) + .property("wtid", codecForString()) + .build("TalerRevenueApi.MerchantIncomingBankTransaction"); + +export const codecForTransferResponse = + (): Codec => + buildCodecForObject() + .property("row_id", codecForNumber()) + .property("timestamp", codecForTimestamp) + .build("TalerWireGatewayApi.TransferResponse"); + +export const codecForIncomingHistory = + (): Codec => + buildCodecForObject() + .property("credit_account", codecForString()) + .property("incoming_transactions", codecForList(codecForIncomingBankTransaction())) + .build("TalerWireGatewayApi.IncomingHistory"); + +export const codecForIncomingBankTransaction = (): Codec => buildCodecForUnion() + .discriminateOn("type") + .alternative("RESERVE", codecForIncomingReserveTransaction()) + .alternative("WAD", codecForIncomingWadTransaction()) + .build("TalerWireGatewayApi.IncomingBankTransaction"); + +export const codecForIncomingReserveTransaction = + (): Codec => + buildCodecForObject() + .property("amount", codecForAmountString()) + .property("date", codecForTimestamp) + .property("debit_account", codecForPaytoURI()) + .property("reserve_pub", codecForString()) + .property("row_id", codecForNumber()) + .property("type", codecForConstString("RESERVE")) + .build("TalerWireGatewayApi.IncomingReserveTransaction"); + +export const codecForIncomingWadTransaction = + (): Codec => + buildCodecForObject() + .property("amount", codecForAmountString()) + .property("credit_account", codecForPaytoURI()) + .property("date", codecForTimestamp) + .property("debit_account", codecForPaytoURI()) + .property("origin_exchange_url", codecForURL()) + .property("row_id", codecForNumber()) + .property("type", codecForConstString("WAD")) + .property("wad_id", codecForString()) + .build("TalerWireGatewayApi.IncomingWadTransaction"); + +export const codecForOutgoingHistory = + (): Codec => + buildCodecForObject() + .property("debit_account", codecForString()) + .property("outgoing_transactions", codecForList(codecForOutgoingBankTransaction())) + .build("TalerWireGatewayApi.OutgoingHistory"); + +export const codecForOutgoingBankTransaction = + (): Codec => + buildCodecForObject() + .property("amount", codecForAmountString()) + .property("credit_account", codecForPaytoURI()) + .property("date", codecForTimestamp) + .property("exchange_base_url", codecForURL()) + .property("row_id", codecForNumber()) + .property("wtid", codecForString()) + .build("TalerWireGatewayApi.OutgoingBankTransaction"); + +export const codecForAddIncomingResponse = + (): Codec => + buildCodecForObject() + .property("row_id", codecForNumber()) + .property("timestamp", codecForTimestamp) + .build("TalerWireGatewayApi.AddIncomingResponse"); + +// export const codecFor = +// (): Codec => +// buildCodecForObject() +// .property("", codecForString()) +// .build("TalerWireGatewayApi.PublicAccountsResponse"); + + +type EmailAddress = string; +type PhoneNumber = string; +type DecimalNumber = string; + +const codecForURL = codecForString +const codecForLibtoolVersion = codecForString +const codecForCurrencyName = codecForString +const codecForPaytoURI = codecForString +const codecForTalerWithdrawalURI = codecForString +const codecForDecimalNumber = codecForString + +enum TanChannel { + SMS = "sms", + EMAIL = "email", + FILE = "file" +} + +export namespace TalerWireGatewayApi { + + export interface TransferResponse { + + // Timestamp that indicates when the wire transfer will be executed. + // In cases where the wire transfer gateway is unable to know when + // the wire transfer will be executed, the time at which the request + // has been received and stored will be returned. + // The purpose of this field is for debugging (humans trying to find + // the transaction) as well as for taxation (determining which + // time period a transaction belongs to). + timestamp: Timestamp; + + // Opaque ID of the transaction that the bank has made. + row_id: SafeUint64; + } + + export interface TransferRequest { + // Nonce to make the request idempotent. Requests with the same + // transaction_uid that differ in any of the other fields + // are rejected. + request_uid: HashCode; + + // Amount to transfer. + amount: Amount; + + // Base URL of the exchange. Shall be included by the bank gateway + // in the appropriate section of the wire transfer details. + exchange_base_url: string; + + // Wire transfer identifier chosen by the exchange, + // used by the merchant to identify the Taler order(s) + // associated with this wire transfer. + wtid: ShortHashCode; + + // The recipient's account identifier as a payto URI. + credit_account: string; + } + + export interface IncomingHistory { + + // Array of incoming transactions. + incoming_transactions: IncomingBankTransaction[]; + + // Payto URI to identify the receiver of funds. + // This must be one of the exchange's bank accounts. + // Credit account is shared by all incoming transactions + // as per the nature of the request. + credit_account: string; + + } + + // Union discriminated by the "type" field. + export type IncomingBankTransaction = + | IncomingReserveTransaction + | IncomingWadTransaction; + + export interface IncomingReserveTransaction { + type: "RESERVE"; + + // Opaque identifier of the returned record. + row_id: SafeUint64; + + // Date of the transaction. + date: Timestamp; + + // Amount transferred. + amount: Amount; + + // Payto URI to identify the sender of funds. + debit_account: string; + + // The reserve public key extracted from the transaction details. + reserve_pub: EddsaPublicKey; + + } + + export interface IncomingWadTransaction { + type: "WAD"; + + // Opaque identifier of the returned record. + row_id: SafeUint64; + + // Date of the transaction. + date: Timestamp; + + // Amount transferred. + amount: Amount; + + // Payto URI to identify the receiver of funds. + // This must be one of the exchange's bank accounts. + credit_account: string; + + // Payto URI to identify the sender of funds. + debit_account: string; + + // Base URL of the exchange that originated the wad. + origin_exchange_url: string; + + // The reserve public key extracted from the transaction details. + wad_id: WadId; + } + + + export interface OutgoingHistory { + + // Array of outgoing transactions. + outgoing_transactions: OutgoingBankTransaction[]; + + // Payto URI to identify the sender of funds. + // This must be one of the exchange's bank accounts. + // Credit account is shared by all incoming transactions + // as per the nature of the request. + debit_account: string; + + } + + export interface OutgoingBankTransaction { + + // Opaque identifier of the returned record. + row_id: SafeUint64; + + // Date of the transaction. + date: Timestamp; + + // Amount transferred. + amount: Amount; + + // Payto URI to identify the receiver of funds. + credit_account: string; + + // The wire transfer ID in the outgoing transaction. + wtid: ShortHashCode; + + // Base URL of the exchange. + exchange_base_url: string; + } + + export interface AddIncomingRequest { + // Amount to transfer. + amount: Amount; + + // Reserve public key that is included in the wire transfer details + // to identify the reserve that is being topped up. + reserve_pub: EddsaPublicKey; + + // Account (as payto URI) that makes the wire transfer to the exchange. + // Usually this account must be created by the test harness before this API is + // used. An exception is the "exchange-fakebank", where any debit account can be + // specified, as it is automatically created. + debit_account: string; + } + + export interface AddIncomingResponse { + + // Timestamp that indicates when the wire transfer will be executed. + // In cases where the wire transfer gateway is unable to know when + // the wire transfer will be executed, the time at which the request + // has been received and stored will be returned. + // The purpose of this field is for debugging (humans trying to find + // the transaction) as well as for taxation (determining which + // time period a transaction belongs to). + timestamp: Timestamp; + + // Opaque ID of the transaction that the bank has made. + row_id: SafeUint64; + } + + + +} + +export namespace TalerRevenueApi { + export interface MerchantIncomingHistory { + + // Array of incoming transactions. + incoming_transactions: MerchantIncomingBankTransaction[]; + + // Payto URI to identify the receiver of funds. + // This must be one of the merchant's bank accounts. + // Credit account is shared by all incoming transactions + // as per the nature of the request. + credit_account: string; + + } + + export interface MerchantIncomingBankTransaction { + + // Opaque identifier of the returned record. + row_id: SafeUint64; + + // Date of the transaction. + date: Timestamp; + + // Amount transferred. + amount: Amount; + + // Payto URI to identify the sender of funds. + debit_account: string; + + // Base URL of the exchange where the transfer originated form. + exchange_url: string; + + // The wire transfer identifier. + wtid: WireTransferIdentifierRawP; + } +} + +export namespace TalerBankIntegrationApi { + export interface BankVersion { + // libtool-style representation of the Bank protocol version, see + // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning + // The format is "current:revision:age". + version: string; + + // Currency used by this bank. + currency: string; + + // How the bank SPA should render this currency. + currency_specification: CurrencySpecification; + + // Name of the API. + name: "taler-bank-integration"; + } + + export interface BankWithdrawalOperationStatus { + // Indicates whether the withdrawal was aborted. + aborted: boolean; + + // Has the wallet selected parameters for the withdrawal operation + // (exchange and reserve public key) and successfully sent it + // to the bank? + selection_done: boolean; + + // The transfer has been confirmed and registered by the bank. + // Does not guarantee that the funds have arrived at the exchange already. + transfer_done: boolean; + + // Amount that will be withdrawn with this operation + // (raw amount without fee considerations). + amount: Amount; + + // Bank account of the customer that is withdrawing, as a + // payto URI. + sender_wire?: string; + + // Suggestion for an exchange given by the bank. + suggested_exchange?: string; + + // URL that the user needs to navigate to in order to + // complete some final confirmation (e.g. 2FA). + // It may contain withdrawal operation id + confirm_transfer_url?: string; + + // Wire transfer types supported by the bank. + wire_types: string[]; + } + + export interface BankWithdrawalOperationPostRequest { + + // Reserve public key. + reserve_pub: string; + + // Payto address of the exchange selected for the withdrawal. + selected_exchange: string; + } + + export interface BankWithdrawalOperationPostResponse { + + // The transfer has been confirmed and registered by the bank. + // Does not guarantee that the funds have arrived at the exchange already. + transfer_done: boolean; + + // URL that the user needs to navigate to in order to + // complete some final confirmation (e.g. 2FA). + // + // Only applicable when transfer_done is false. + // It may contain withdrawal operation id + confirm_transfer_url?: string; + } + + +} +export namespace TalerCorebankApi { + + export interface Config { + // Name of this API, always "circuit". + name: string; + // API version in the form $n:$n:$n + version: string; + + // If 'true', the server provides local currency + // conversion support. + // If missing or false, some parts of the API + // are not supported and return 404. + have_cashout?: boolean; + + // How the bank SPA should render the currency. + currency: CurrencySpecification; + + // Fiat currency. That is the currency in which + // cash-out operations ultimately wire money. + // Only applicable if have_cashout=true. + fiat_currency?: CurrencySpecification; + } + + export interface BankAccountCreateWithdrawalRequest { + // Amount to withdraw. + amount: Amount; + } + export interface BankAccountCreateWithdrawalResponse { + // ID of the withdrawal, can be used to view/modify the withdrawal operation. + withdrawal_id: string; + + // URI that can be passed to the wallet to initiate the withdrawal. + taler_withdraw_uri: string; + } + export interface BankAccountGetWithdrawalResponse { + // Amount that will be withdrawn with this withdrawal operation. + amount: Amount; + + // Was the withdrawal aborted? + aborted: boolean; + + // Has the withdrawal been confirmed by the bank? + // The wire transfer for a withdrawal is only executed once + // both confirmation_done is true and selection_done is true. + confirmation_done: boolean; + + // Did the wallet select reserve details? + selection_done: boolean; + + // Reserve public key selected by the exchange, + // only non-null if selection_done is true. + selected_reserve_pub: string | undefined; + + // Exchange account selected by the wallet, or by the bank + // (with the default exchange) in case the wallet did not provide one + // through the Integration API. + selected_exchange_account: string | undefined; + } + + export interface BankAccountTransactionsResponse { + transactions: BankAccountTransactionInfo[]; + } + + export interface BankAccountTransactionInfo { + creditor_payto_uri: string; + debtor_payto_uri: string; + + amount: Amount; + direction: "debit" | "credit"; + + subject: string; + + // Transaction unique ID. Matches + // $transaction_id from the URI. + row_id: number; + date: Timestamp; + } + + export interface CreateBankAccountTransactionCreate { + // Address in the Payto format of the wire transfer receiver. + // It needs at least the 'message' query string parameter. + payto_uri: string; + + // Transaction amount (in the $currency:x.y format), optional. + // However, when not given, its value must occupy the 'amount' + // query string parameter of the 'payto' field. In case it + // is given in both places, the paytoUri's takes the precedence. + amount?: string; + } + + 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. + internal_payto_uri?: string; + } + export interface ChallengeContactData { + + // E-Mail address + email?: EmailAddress; + + // Phone number. + phone?: PhoneNumber; + } + + export interface AccountReconfiguration { + + // 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_address?: string; + + // Legal name associated with $username. + // When missing, the old name is kept. + name?: string; + + // If present, change the is_exchange configuration. + // See RegisterAccountRequest + is_exchange?: boolean; + } + + + export interface AccountPasswordChange { + + // New password. + new_password: string; + } + + export interface PublicAccountsResponse { + public_accounts: PublicAccount[]; + } + export interface PublicAccount { + payto_uri: string; + + balance: Balance; + + // The account name (=username) of the + // libeufin-bank account. + account_name: string; + } + + export interface ListBankAccountsResponse { + accounts: AccountMinimalData[]; + } + export interface Balance { + amount: Amount; + credit_debit_indicator: "credit" | "debit"; + } + export interface AccountMinimalData { + // Username + username: string; + + // Legal name of the account owner. + name: string; + + // current balance of the account + balance: Balance; + + // Number indicating the max debit allowed for the requesting user. + debit_threshold: Amount; + } + + export interface AccountData { + // Legal name of the account owner. + name: string; + + // Available balance on the account. + balance: Balance; + + // payto://-URI of the account. + payto_uri: string; + + // Number indicating the max debit allowed for the requesting user. + debit_threshold: Amount; + + 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 CashoutRequest { + + // Optional subject to associate to the + // cashout operation. This data will appear + // as the incoming wire transfer subject in + // the user's external bank account. + subject?: string; + + // That is the plain amount that the user specified + // to cashout. Its $currency is the (regional) currency of the + // bank instance. + amount_debit: Amount; + + // That is the amount that will effectively be + // transferred by the bank to the user's bank + // account, that is external to the regional currency. + // It is expressed in the fiat currency and + // is calculated after the cashout fee and the + // exchange rate. See the /cashout-rates call. + // The client needs to calculate this amount + // correctly based on the amount_debit and the cashout rate, + // otherwise the request will fail. + amount_credit: Amount; + + // Which channel the TAN should be sent to. If + // this field is missing, it defaults to SMS. + // The default choice prefers to change the communication + // channel respect to the one used to issue this request. + tan_channel?: TanChannel; + } + + export interface CashoutPending { + // ID identifying the operation being created + // and now waiting for the TAN confirmation. + cashout_id: string; + } + + export interface CashoutConfirmRequest { + // the TAN that confirms $CASHOUT_ID. + tan: string; + } + + export interface CashoutConversionResponse { + // Amount that the user will get deducted from their regional + // bank account, according to the 'amount_credit' value. + amount_debit: Amount; + // Amount that the user will receive in their fiat + // bank account, according to 'amount_debit'. + amount_credit: Amount; + } + + export interface Cashouts { + // Every string represents a cash-out operation ID. + cashouts: CashoutInfo[]; + } + + export interface CashoutInfo { + cashout_id: string; + status: "pending" | "confirmed"; + } + export interface GlobalCashouts { + // Every string represents a cash-out operation ID. + cashouts: GlobalCashoutInfo[]; + } + export interface GlobalCashoutInfo { + cashout_id: string; + username: string; + status: "pending" | "confirmed"; + } + + export interface CashoutStatusResponse { + status: "pending" | "confirmed"; + + // Amount debited to the internal + // regional currency bank account. + amount_debit: Amount; + + // Amount credited to the external bank account. + amount_credit: Amount; + + // Transaction subject. + subject: string; + + // Fiat bank account that will receive the cashed out amount. + // Specified as a payto URI. + credit_payto_uri: string; + + // Time when the cashout was created. + creation_time: Timestamp; + + // Time when the cashout was confirmed via its TAN. + // Missing when the operation wasn't confirmed yet. + confirmation_time?: Timestamp; + } + + export interface ConversionRatesResponse { + + // Exchange rate to buy the local currency from the external one + buy_at_ratio: DecimalNumber; + + // Exchange rate to sell the local currency for the external one + sell_at_ratio: DecimalNumber; + + // Fee to subtract after applying the buy ratio. + buy_in_fee: DecimalNumber; + + // Fee to subtract after applying the sell ratio. + sell_out_fee: DecimalNumber; + } + + export enum MonitorTimeframeParam { + hour, day, month, year, decade, + } + + export interface MonitorResponse { + + // This number identifies how many cashin operations + // took place in the timeframe specified in the request. + // This number corresponds to how many withdrawals have + // been initiated by a wallet owner. Note: wallet owners + // are NOT required to be customers of the libeufin-bank. + cashinCount: number; + + // This amount accounts how much external currency has been + // spent to withdraw Taler coins in the internal currency. + // The exact amount of internal currency being created can be + // calculated using the advertised conversion rates. + cashinExternalVolume: Amount; + + // This number identifies how many cashout operations were + // confirmed in the timeframe speficied in the request. + cashoutCount: number; + + // This amount corresponds to how much *external* currency was + // paid by the libeufin-bank administrator to fulfill all the + // confirmed cashouts related to the timeframe specified in the + // request. + cashoutExternalVolume: Amount; + + // This number identifies how many payments were made by a + // Taler exchange to a merchant bank account in the internal + // currency, in the timeframe specified in the request. + talerPayoutCount: number; + + // This amount accounts the overall *internal* currency that + // has been paid by a Taler exchange to a merchant internal + // bank account, in the timeframe specified in the request. + talerPayoutInternalVolume: Amount; + } + + +} diff --git a/packages/taler-util/src/http-client/utils.ts b/packages/taler-util/src/http-client/utils.ts new file mode 100644 index 000000000..4588f945c --- /dev/null +++ b/packages/taler-util/src/http-client/utils.ts @@ -0,0 +1,68 @@ +import { base64FromArrayBuffer } from "../base64.js"; +import { stringToBytes } from "../taler-crypto.js"; +import { AccessToken, TalerAuthentication } from "./types.js"; + +/** + * Helper function to generate the "Authorization" HTTP header. + */ +export function makeBasicAuthHeader(username: string, password: string): string { + const auth = `${username}:${password}`; + const authEncoded: string = base64FromArrayBuffer(stringToBytes(auth)); + return `Basic ${authEncoded}`; +} + +/** + * rfc8959 + * @param token + * @returns + */ +export function makeBearerTokenAuthHeader(token: AccessToken): string { + return `Bearer secret-token:${token}`; +} + +/** + * https://bugs.gnunet.org/view.php?id=7949 + */ +export function addPaginationParams(url: URL, pagination?: PaginationParams) { + if (!pagination) return; + if (pagination.timoutMs) { + url.searchParams.set("long_poll_ms", String(pagination.timoutMs)) + } + if (pagination.offset) { + url.searchParams.set("start", pagination.offset) + } + const order = !pagination || pagination.order === "asc" ? 1 : -1 + const limit = !pagination || !pagination.limit || pagination.limit === 0 ? 5 : Math.abs(pagination.limit) + //always send delta + url.searchParams.set("delta", String(order * limit)) +} + +export type UserAndPassword = { + username: string, + password: string, +} + +export type UserAndToken = { + username: string, + token: AccessToken, +} + +export type PaginationParams = { + /** + * row identifier as the starting point of the query + */ + offset?: string, + /** + * max number of element in the result response + * always greater than 0 + */ + limit?: number, + /** + * milliseconds the server should wait for at least one result to be shown + */ + timoutMs?: number, + /** + * order + */ + order: "asc" | "dec" +} diff --git a/packages/taler-util/src/http-common.ts b/packages/taler-util/src/http-common.ts index f25705545..da2fbb9da 100644 --- a/packages/taler-util/src/http-common.ts +++ b/packages/taler-util/src/http-common.ts @@ -50,7 +50,7 @@ export interface HttpResponse { export const DEFAULT_REQUEST_TIMEOUT_MS = 60000; export interface HttpRequestOptions { - method?: "POST" | "PUT" | "GET" | "DELETE"; + method?: "POST" | "PATCH" | "PUT" | "GET" | "DELETE"; headers?: { [name: string]: string }; /** diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts index 568e2f438..71d4253f0 100644 --- a/packages/taler-util/src/index.ts +++ b/packages/taler-util/src/index.ts @@ -42,3 +42,8 @@ export * from "./transaction-test-data.js"; export * from "./libeufin-api-types.js"; export * from "./MerchantApiClient.js"; export * from "./bank-api-client.js"; +export * from "./http-client/bank-core.js"; +export * from "./http-client/bank-integration.js"; +export * from "./http-client/bank-revenue.js"; +export * from "./http-client/bank-wire.js"; +export * from "./http-client/types.js"; diff --git a/packages/taler-util/src/taler-crypto.ts b/packages/taler-util/src/taler-crypto.ts index de5be71a1..b68dc1ada 100644 --- a/packages/taler-util/src/taler-crypto.ts +++ b/packages/taler-util/src/taler-crypto.ts @@ -123,7 +123,7 @@ function getValue(chr: string): number { switch (chr) { case "O": case "o": - a = "0;"; + a = "0"; break; case "i": case "I": diff --git a/packages/taler-util/src/taler-types.ts b/packages/taler-util/src/taler-types.ts index ebb0291f5..07b989fc1 100644 --- a/packages/taler-util/src/taler-types.ts +++ b/packages/taler-util/src/taler-types.ts @@ -951,7 +951,7 @@ export interface AccountInfo { /** * @deprecated */ -export interface ExchangeWireJson {} +export interface ExchangeWireJson { } /** * Proposal returned from the contract URL. @@ -1006,8 +1006,8 @@ export class WithdrawOperationStatusResponse { /** * Response from the merchant. */ -export class TipPickupGetResponse { - tip_amount: string; +export class RewardPickupGetResponse { + reward_amount: string; exchange_url: string; @@ -1310,12 +1310,6 @@ export const codecForDenominationPubKey = () => .alternative(DenomKeyType.ClauseSchnorr, codecForCsDenominationPubKey()) .build("DenominationPubKey"); -export const codecForBankWithdrawalOperationPostResponse = - (): Codec => - buildCodecForObject() - .property("transfer_done", codecForBoolean()) - .build("BankWithdrawalOperationPostResponse"); - export type AmountString = string; export type Base32String = string; export type EddsaSignatureString = string; @@ -1572,9 +1566,9 @@ export const codecForWithdrawOperationStatusResponse = .property("wire_types", codecForList(codecForString())) .build("WithdrawOperationStatusResponse"); -export const codecForTipPickupGetResponse = (): Codec => - buildCodecForObject() - .property("tip_amount", codecForString()) +export const codecForRewardPickupGetResponse = (): Codec => + buildCodecForObject() + .property("reward_amount", codecForString()) .property("exchange_url", codecForString()) .property("next_url", codecOptional(codecForString())) .property("expiration", codecForTimestamp) @@ -1591,7 +1585,7 @@ export const codecForWithdrawResponse = (): Codec => .property("ev_sig", codecForBlindedDenominationSignature()) .build("WithdrawResponse"); -export const codecForWithdrawBatchResponse = +export const codecForExchangeWithdrawBatchResponse = (): Codec => buildCodecForObject() .property("ev_sigs", codecForList(codecForWithdrawResponse())) diff --git a/packages/taler-util/src/transactions-types.ts b/packages/taler-util/src/transactions-types.ts index 63db206bd..88830d82b 100644 --- a/packages/taler-util/src/transactions-types.ts +++ b/packages/taler-util/src/transactions-types.ts @@ -43,6 +43,8 @@ import { codecForList, codecForAny, codecForBoolean, + codecForEither, + codecForConstString, } from "./codec.js"; import { RefreshReason, @@ -62,6 +64,13 @@ export interface TransactionsRequest { */ search?: string; + /** + * Sort order of the transaction items. + * By default, items are sorted ascending by their + * main timestamp. + */ + sort?: "ascending" | "descending"; + /** * If true, include all refreshes in the transactions list. */ @@ -690,6 +699,15 @@ export const codecForTransactionsRequest = (): Codec => buildCodecForObject() .property("currency", codecOptional(codecForString())) .property("search", codecOptional(codecForString())) + .property( + "sort", + codecOptional( + codecForEither( + codecForConstString("ascending"), + codecForConstString("descending"), + ), + ), + ) .property("includeRefreshes", codecOptional(codecForBoolean())) .build("TransactionsRequest"); diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts index 9a4e15ae1..49dae9bd4 100644 --- a/packages/taler-util/src/wallet-types.ts +++ b/packages/taler-util/src/wallet-types.ts @@ -46,6 +46,7 @@ import { codecOptional, renderContext, } from "./codec.js"; +import { CurrencySpecification } from "./index.js"; import { VersionMatchResult } from "./libtool-version.js"; import { PaytoUri } from "./payto.js"; import { AgeCommitmentProof } from "./taler-crypto.js"; @@ -429,16 +430,6 @@ export interface GetCurrencySpecificationResponse { currencySpecification: CurrencySpecification; } -export interface CurrencySpecification { - decimal_separator: string; - fractional_input_digits: number; - fractional_normal_digits: number; - fractional_trailing_zero_digits: number; - is_currency_name_leading: boolean; - name: string; - alt_unit_names: { [n: number]: string }; -} - export interface InitRequest { skipDefaults?: boolean; } @@ -556,11 +547,11 @@ export interface CoinDumpJson { withdrawal_reserve_pub: string | undefined; coin_status: CoinStatus; spend_allocation: - | { - id: string; - amount: string; - } - | undefined; + | { + id: string; + amount: string; + } + | undefined; /** * Information about the age restriction */ diff --git a/packages/taler-wallet-core/src/dbless.ts b/packages/taler-wallet-core/src/dbless.ts index 4fc890788..b6b80009f 100644 --- a/packages/taler-wallet-core/src/dbless.ts +++ b/packages/taler-wallet-core/src/dbless.ts @@ -49,6 +49,9 @@ import { Logger, parsePaytoUri, UnblindedSignature, + ExchangeBatchWithdrawRequest, + ExchangeWithdrawBatchResponse, + codecForExchangeWithdrawBatchResponse, } from "@gnu-taler/taler-util"; import { HttpRequestLibrary, @@ -165,25 +168,29 @@ export async function withdrawCoin(args: { value: Amounts.parseOrThrow(denom.value), }); - const reqBody: ExchangeWithdrawRequest = { - denom_pub_hash: planchet.denomPubHash, - reserve_sig: planchet.withdrawSig, - coin_ev: planchet.coinEv, + const reqBody: ExchangeBatchWithdrawRequest = { + planchets: [ + { + denom_pub_hash: planchet.denomPubHash, + reserve_sig: planchet.withdrawSig, + coin_ev: planchet.coinEv, + }, + ], }; const reqUrl = new URL( - `reserves/${planchet.reservePub}/withdraw`, + `reserves/${planchet.reservePub}/batch-withdraw`, exchangeBaseUrl, ).href; - const resp = await http.postJson(reqUrl, reqBody); - const r = await readSuccessResponseJsonOrThrow( + const resp = await http.fetch(reqUrl, { method: "POST", body: reqBody }); + const rBatch = await readSuccessResponseJsonOrThrow( resp, - codecForWithdrawResponse(), + codecForExchangeWithdrawBatchResponse(), ); const ubSig = await cryptoApi.unblindDenominationSignature({ planchet, - evSig: r.ev_sig, + evSig: rBatch.ev_sigs[0].ev_sig, }); return { diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts index 1819aa1b8..7590280bc 100644 --- a/packages/taler-wallet-core/src/operations/pending.ts +++ b/packages/taler-wallet-core/src/operations/pending.ts @@ -101,14 +101,14 @@ async function gatherExchangePending( case ExchangeEntryDbUpdateStatus.Failed: return; } - const opTag = TaskIdentifiers.forExchangeUpdate(exch); - let opr = await tx.operationRetries.get(opTag); + const opUpdateExchangeTag = TaskIdentifiers.forExchangeUpdate(exch); + let opr = await tx.operationRetries.get(opUpdateExchangeTag); const timestampDue = opr?.retryInfo.nextRetry ?? exch.nextRefreshCheckStamp; resp.pendingOperations.push({ type: PendingTaskType.ExchangeUpdate, ...getPendingCommon( ws, - opTag, + opUpdateExchangeTag, AbsoluteTime.fromPreciseTimestamp(timestampPreciseFromDb(timestampDue)), ), givesLifeness: false, @@ -119,11 +119,12 @@ async function gatherExchangePending( // We only schedule a check for auto-refresh if the exchange update // was successful. if (!opr?.lastError) { + const opCheckRefreshTag = TaskIdentifiers.forExchangeCheckRefresh(exch); resp.pendingOperations.push({ type: PendingTaskType.ExchangeCheckRefresh, ...getPendingCommon( ws, - opTag, + opCheckRefreshTag, AbsoluteTime.fromPreciseTimestamp( timestampPreciseFromDb(timestampDue), ), diff --git a/packages/taler-wallet-core/src/operations/reward.ts b/packages/taler-wallet-core/src/operations/reward.ts index 4e16d977d..ed9927bab 100644 --- a/packages/taler-wallet-core/src/operations/reward.ts +++ b/packages/taler-wallet-core/src/operations/reward.ts @@ -23,7 +23,7 @@ import { Amounts, BlindedDenominationSignature, codecForMerchantTipResponseV2, - codecForTipPickupGetResponse, + codecForRewardPickupGetResponse, CoinStatus, DenomKeyType, encodeCrock, @@ -168,11 +168,11 @@ export async function prepareTip( const merchantResp = await ws.http.fetch(tipStatusUrl.href); const tipPickupStatus = await readSuccessResponseJsonOrThrow( merchantResp, - codecForTipPickupGetResponse(), + codecForRewardPickupGetResponse(), ); logger.trace(`status ${j2s(tipPickupStatus)}`); - const amount = Amounts.parseOrThrow(tipPickupStatus.tip_amount); + const amount = Amounts.parseOrThrow(tipPickupStatus.reward_amount); logger.trace("new tip, creating tip record"); await updateExchangeFromUrl(ws, tipPickupStatus.exchange_url); diff --git a/packages/taler-wallet-core/src/operations/testing.ts b/packages/taler-wallet-core/src/operations/testing.ts index 607d03470..f5bed13dd 100644 --- a/packages/taler-wallet-core/src/operations/testing.ts +++ b/packages/taler-wallet-core/src/operations/testing.ts @@ -74,6 +74,7 @@ import { import { initiatePeerPushDebit } from "./pay-peer-push-debit.js"; import { OpenedPromise, openPromise } from "../index.js"; import { getTransactionById, getTransactions } from "./transactions.js"; +import { getPendingOperations } from "./pending.js"; const logger = new Logger("operations/testing.ts"); @@ -290,7 +291,7 @@ export async function runIntegrationTest( corebankApiBaseUrl: args.corebankApiBaseUrl, exchangeBaseUrl: args.exchangeBaseUrl, }); - await waitUntilDone(ws); + await waitUntilTransactionsFinal(ws); logger.info("done withdrawing test balance"); const balance = await getBalances(ws); @@ -305,7 +306,7 @@ export async function runIntegrationTest( await makePayment(ws, myMerchant, args.amountToSpend, "hello world"); // Wait until the refresh is done - await waitUntilDone(ws); + await waitUntilTransactionsFinal(ws); logger.trace("withdrawing test balance for refund"); const withdrawAmountTwo = Amounts.parseOrThrow(`${currency}:18`); @@ -320,7 +321,7 @@ export async function runIntegrationTest( }); // Wait until the withdraw is done - await waitUntilDone(ws); + await waitUntilTransactionsFinal(ws); const { orderId: refundOrderId } = await makePayment( ws, @@ -344,7 +345,7 @@ export async function runIntegrationTest( logger.trace("integration test: applied refund"); // Wait until the refund is done - await waitUntilDone(ws); + await waitUntilTransactionsFinal(ws); logger.trace("integration test: making payment after refund"); @@ -357,12 +358,17 @@ export async function runIntegrationTest( logger.trace("integration test: make payment done"); - await waitUntilDone(ws); + await waitUntilTransactionsFinal(ws); logger.trace("integration test: all done!"); } -export async function waitUntilDone(ws: InternalWalletState): Promise { +/** + * Wait until all transactions are in a final state. + */ +export async function waitUntilTransactionsFinal( + ws: InternalWalletState, +): Promise { logger.info("waiting until all transactions are in a final state"); ws.ensureTaskLoopRunning(); let p: OpenedPromise | undefined = undefined; @@ -410,6 +416,44 @@ export async function waitUntilDone(ws: InternalWalletState): Promise { logger.info("done waiting until all transactions are in a final state"); } +/** + * Wait until pending work is processed. + */ +export async function waitUntilTasksProcessed( + ws: InternalWalletState, +): Promise { + logger.info("waiting until pending work is processed"); + ws.ensureTaskLoopRunning(); + let p: OpenedPromise | undefined = undefined; + const cancelNotifs = ws.addNotificationListener((notif) => { + if (!p) { + return; + } + if (notif.type === NotificationType.PendingOperationProcessed) { + p.resolve(); + } + }); + while (1) { + p = openPromise(); + const pendingTasksResp = await getPendingOperations(ws); + logger.info(`waiting on pending ops: ${j2s(pendingTasksResp)}`); + let finished = true; + for (const task of pendingTasksResp.pendingOperations) { + if (task.isDue) { + finished = false; + } + logger.info(`continuing waiting for task ${task.id}`); + } + if (finished) { + break; + } + // Wait until task is done + await p.promise; + } + logger.info("done waiting until pending work is processed"); + cancelNotifs(); +} + export async function waitUntilRefreshesDone( ws: InternalWalletState, ): Promise { @@ -463,7 +507,7 @@ export async function waitUntilRefreshesDone( logger.info("done waiting until all refreshes are in a final state"); } -async function waitUntilPendingReady( +async function waitUntilTransactionPendingReady( ws: InternalWalletState, transactionId: string, ): Promise { @@ -560,7 +604,7 @@ export async function runIntegrationTest2( corebankApiBaseUrl: args.corebankApiBaseUrl, exchangeBaseUrl: args.exchangeBaseUrl, }); - await waitUntilDone(ws); + await waitUntilTransactionsFinal(ws); logger.info("done withdrawing test balance"); const balance = await getBalances(ws); @@ -580,7 +624,7 @@ export async function runIntegrationTest2( ); // Wait until the refresh is done - await waitUntilDone(ws); + await waitUntilTransactionsFinal(ws); logger.trace("withdrawing test balance for refund"); const withdrawAmountTwo = Amounts.parseOrThrow(`${currency}:18`); @@ -595,7 +639,7 @@ export async function runIntegrationTest2( }); // Wait until the withdraw is done - await waitUntilDone(ws); + await waitUntilTransactionsFinal(ws); const { orderId: refundOrderId } = await makePayment( ws, @@ -619,7 +663,7 @@ export async function runIntegrationTest2( logger.trace("integration test: applied refund"); // Wait until the refund is done - await waitUntilDone(ws); + await waitUntilTransactionsFinal(ws); logger.trace("integration test: making payment after refund"); @@ -632,7 +676,7 @@ export async function runIntegrationTest2( logger.trace("integration test: make payment done"); - await waitUntilDone(ws); + await waitUntilTransactionsFinal(ws); const peerPushInit = await initiatePeerPushDebit(ws, { partialContractTerms: { @@ -647,7 +691,7 @@ export async function runIntegrationTest2( }, }); - await waitUntilPendingReady(ws, peerPushInit.transactionId); + await waitUntilTransactionPendingReady(ws, peerPushInit.transactionId); const peerPushCredit = await preparePeerPushCredit(ws, { talerUri: peerPushInit.talerUri, @@ -670,7 +714,7 @@ export async function runIntegrationTest2( }, }); - await waitUntilPendingReady(ws, peerPullInit.transactionId); + await waitUntilTransactionPendingReady(ws, peerPullInit.transactionId); const peerPullInc = await preparePeerPullDebit(ws, { talerUri: peerPullInit.talerUri, @@ -680,7 +724,7 @@ export async function runIntegrationTest2( peerPullDebitId: peerPullInc.peerPullDebitId, }); - await waitUntilDone(ws); + await waitUntilTransactionsFinal(ws); logger.trace("integration test: all done!"); } diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index 72c67b153..bebb3d60b 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -1290,9 +1290,16 @@ export async function getTransactions( const txPending = transactions.filter((x) => isPending(x)); const txNotPending = transactions.filter((x) => !isPending(x)); + let sortSign: number; + if (transactionsRequest?.sort == "descending") { + sortSign = -1; + } else { + sortSign = 1; + } + const txCmp = (h1: Transaction, h2: Transaction) => { // Order transactions by timestamp. Newest transactions come first. - const tsCmp = -AbsoluteTime.cmp( + const tsCmp = AbsoluteTime.cmp( AbsoluteTime.fromPreciseTimestamp(h1.timestamp), AbsoluteTime.fromPreciseTimestamp(h2.timestamp), ); @@ -1300,7 +1307,7 @@ export async function getTransactions( if (tsCmp === 0) { return Math.sign(txOrder[h1.type] - txOrder[h2.type]); } - return tsCmp; + return sortSign * tsCmp; }; txPending.sort(txCmp); diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index eff427bec..245eec4fa 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -33,7 +33,7 @@ import { codecForReserveStatus, codecForTalerConfigResponse, codecForWalletKycUuid, - codecForWithdrawBatchResponse, + codecForExchangeWithdrawBatchResponse, codecForWithdrawOperationStatusResponse, codecForWithdrawResponse, CoinStatus, @@ -944,7 +944,7 @@ async function processPlanchetExchangeBatchRequest( } const r = await readSuccessResponseJsonOrThrow( resp, - codecForWithdrawBatchResponse(), + codecForExchangeWithdrawBatchResponse(), ); return { coinIdxs: requestCoinIdxs, diff --git a/packages/taler-wallet-core/src/versions.ts b/packages/taler-wallet-core/src/versions.ts index e46c7ebe1..8e0e5561b 100644 --- a/packages/taler-wallet-core/src/versions.ts +++ b/packages/taler-wallet-core/src/versions.ts @@ -40,4 +40,17 @@ export const WALLET_BANK_INTEGRATION_PROTOCOL_VERSION = "0:0:0"; * Will be replaced with the value from package.json in a * post-compilation step (inside lib/). */ -export const WALLET_CORE_IMPLEMENTATION_VERSION = "0:0:0"; +export const WALLET_CORE_IMPLEMENTATION_VERSION = "1:0:0"; + +/** + * Libtool rules: + * + * If the library source code has changed at all since the last update, + * then increment revision (‘c:r:a’ becomes ‘c:r+1:a’). + * If any interfaces have been added, removed, or changed since the last + * update, increment current, and set revision to 0. + * If any interfaces have been added since the last public release, then + * increment age. + * If any interfaces have been removed or changed since the last public + * release, then set age to 0. + */ \ No newline at end of file diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts index fadc7aa7f..a8de9ac03 100644 --- a/packages/taler-wallet-core/src/wallet-api-types.ts +++ b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -224,6 +224,7 @@ export enum WalletApiOperation { DeleteStoredBackup = "deleteStoredBackup", RecoverStoredBackup = "recoverStoredBackup", UpdateExchangeEntry = "updateExchangeEntry", + TestingWaitTasksProcessed = "testingWaitTasksProcessed", } // group: Initialization @@ -1007,7 +1008,7 @@ export type TestingSetTimetravelOp = { /** * Wait until all transactions are in a final state. */ -export type TestingWaitTransactionsFinal = { +export type TestingWaitTransactionsFinalOp = { op: WalletApiOperation.TestingWaitTransactionsFinal; request: EmptyObject; response: EmptyObject; @@ -1016,12 +1017,21 @@ export type TestingWaitTransactionsFinal = { /** * Wait until all refresh transactions are in a final state. */ -export type TestingWaitRefreshesFinal = { +export type TestingWaitRefreshesFinalOp = { op: WalletApiOperation.TestingWaitRefreshesFinal; request: EmptyObject; response: EmptyObject; }; +/** + * Wait until all tasks have been processed and the wallet is idle. + */ +export type TestingWaitTasksProcessedOp = { + op: WalletApiOperation.TestingWaitTasksProcessed; + request: EmptyObject; + response: EmptyObject; +}; + /** * Wait until a transaction is in a particular state. */ @@ -1132,8 +1142,9 @@ export type WalletOperations = { [WalletApiOperation.Recycle]: RecycleOp; [WalletApiOperation.ApplyDevExperiment]: ApplyDevExperimentOp; [WalletApiOperation.ValidateIban]: ValidateIbanOp; - [WalletApiOperation.TestingWaitTransactionsFinal]: TestingWaitTransactionsFinal; - [WalletApiOperation.TestingWaitRefreshesFinal]: TestingWaitRefreshesFinal; + [WalletApiOperation.TestingWaitTransactionsFinal]: TestingWaitTransactionsFinalOp; + [WalletApiOperation.TestingWaitRefreshesFinal]: TestingWaitRefreshesFinalOp; + [WalletApiOperation.TestingWaitTasksProcessed]: TestingWaitTasksProcessedOp; [WalletApiOperation.TestingSetTimetravel]: TestingSetTimetravelOp; [WalletApiOperation.TestingWaitTransactionState]: TestingWaitTransactionStateOp; [WalletApiOperation.GetCurrencySpecification]: GetCurrencySpecificationOp; diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 571bf07ee..06d9bb9e8 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -252,9 +252,10 @@ import { runIntegrationTest2, testPay, waitTransactionState, - waitUntilDone, + waitUntilTransactionsFinal, waitUntilRefreshesDone, withdrawTestBalance, + waitUntilTasksProcessed, } from "./operations/testing.js"; import { acceptTip, @@ -1427,6 +1428,10 @@ async function dispatchRequestInternal( await waitTransactionState(ws, req.transactionId, req.txState); return {}; } + case WalletApiOperation.TestingWaitTasksProcessed: { + await waitUntilTasksProcessed(ws); + return {}; + } case WalletApiOperation.GetCurrencySpecification: { // Ignore result, just validate in this mock implementation const req = codecForGetCurrencyInfoRequest().decode(payload); @@ -1436,9 +1441,9 @@ async function dispatchRequestInternal( currencySpecification: { decimal_separator: ",", name: "Kudos (Taler Demonstrator)", - fractional_input_digits: 2, - fractional_normal_digits: 2, - fractional_trailing_zero_digits: 2, + num_fractional_input_digits: 2, + num_fractional_normal_digits: 2, + num_fractional_trailing_zero_digits: 2, is_currency_name_leading: true, alt_unit_names: { "0": "ク", @@ -1451,9 +1456,9 @@ async function dispatchRequestInternal( currencySpecification: { decimal_separator: ",", name: "Test (Taler Unstable Demonstrator)", - fractional_input_digits: 0, - fractional_normal_digits: 0, - fractional_trailing_zero_digits: 0, + num_fractional_input_digits: 0, + num_fractional_normal_digits: 0, + num_fractional_trailing_zero_digits: 0, is_currency_name_leading: false, alt_unit_names: {}, }, @@ -1464,9 +1469,9 @@ async function dispatchRequestInternal( currencySpecification: { decimal_separator: ",", name: "Unknown", - fractional_input_digits: 2, - fractional_normal_digits: 2, - fractional_trailing_zero_digits: 2, + num_fractional_input_digits: 2, + num_fractional_normal_digits: 2, + num_fractional_trailing_zero_digits: 2, is_currency_name_leading: true, alt_unit_names: {}, }, @@ -1600,7 +1605,7 @@ async function dispatchRequestInternal( return getVersion(ws); } case WalletApiOperation.TestingWaitTransactionsFinal: - return await waitUntilDone(ws); + return await waitUntilTransactionsFinal(ws); case WalletApiOperation.TestingWaitRefreshesFinal: return await waitUntilRefreshesDone(ws); case WalletApiOperation.TestingSetTimetravel: {