Merge branch 'master' into age-withdraw

This commit is contained in:
Özgür Kesim 2023-10-17 12:04:44 +02:00
commit fba664f9a3
Signed by: oec
GPG Key ID: 3D76A56D79EDD9D7
26 changed files with 2144 additions and 90 deletions

View File

@ -52,6 +52,41 @@ export function WithdrawalQRCode({
if (result.loading) { if (result.loading) {
return <Loading />; return <Loading />;
} }
if (result.type === ErrorType.CLIENT && result.status === HttpStatusCode.NotFound) {
return <div class="relative ml-auto mr-auto transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
<div>
<div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100 ">
<svg class="h-6 w-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
</svg>
</div>
<div class="mt-3 text-center sm:mt-5">
<h3 class="text-base font-semibold leading-6 text-gray-900" id="modal-title">
<i18n.Translate>Operation not found</i18n.Translate>
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500">
<i18n.Translate>
This operation is not known by the server. The operation id is wrong or the
server deleted the operation information before reaching here.
</i18n.Translate>
</p>
</div>
</div>
</div>
<div class="mt-5 sm:mt-6">
<button type="button"
class="inline-flex w-full justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
onClick={async (e) => {
e.preventDefault();
onClose()
}}>
<i18n.Translate>Cotinue to dashboard</i18n.Translate>
</button>
</div>
</div>
}
return handleNotOkResult(i18n)(result); return handleNotOkResult(i18n)(result);
} }
const { data } = result; const { data } = result;

View File

@ -654,6 +654,9 @@ export class FakebankService
return this.baseUrl; 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( async createExchangeAccount(
accountName: string, accountName: string,
password: string, password: string,

View File

@ -136,20 +136,22 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) {
}, },
); );
await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
let p: PendingOperationsResponse; let p: PendingOperationsResponse;
p = await walletClient.call(WalletApiOperation.GetPendingOperations, {}); p = await walletClient.call(WalletApiOperation.GetPendingOperations, {});
console.log("pending operations after first time travel"); console.log("pending operations after first time travel");
console.log(JSON.stringify(p, undefined, 2)); 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, walletClient,
bank, bank,
exchange, exchange,
amount: "TESTKUDOS:20", amount: "TESTKUDOS:20",
}); });
await wres2.withdrawalFinishedCond;
await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); 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. // At this point, the original coins should've been refreshed.
// It would be too late to refresh them now, as we're past // It would be too late to refresh them now, as we're past
// the two year deposit expiration. // the two year deposit expiration.
await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
const orderResp = await merchantClient.createOrder({ const orderResp = await merchantClient.createOrder({
order: { order: {
fulfillment_url: "http://example.com", fulfillment_url: "http://example.com",
@ -195,7 +198,7 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) {
t.assertTrue(r.status === PreparePayResultType.PaymentPossible); t.assertTrue(r.status === PreparePayResultType.PaymentPossible);
const cpr = await walletClient.call(WalletApiOperation.ConfirmPay, { const cpr = await walletClient.call(WalletApiOperation.ConfirmPay, {
proposalId: r.proposalId, transactionId: r.transactionId,
}); });
t.assertTrue(cpr.type === ConfirmPayResultType.Done); t.assertTrue(cpr.type === ConfirmPayResultType.Done);

View File

@ -55,12 +55,14 @@ export async function runWithdrawalFakebankTest(t: GlobalTestState) {
accountName: "exchange", accountName: "exchange",
accountPassword: "x", accountPassword: "x",
wireGatewayApiBaseUrl: new URL( wireGatewayApiBaseUrl: new URL(
"/accounts/exchange/taler-wire-gateway", "/accounts/exchange/taler-wire-gateway/",
bank.baseUrl, bank.baseUrl,
).href, ).href,
accountPaytoUri: "payto://x-taler-bank/localhost/exchange", accountPaytoUri: "payto://x-taler-bank/localhost/exchange",
}); });
await bank.createExchangeAccount("exchange", "x");
await bank.start(); await bank.start();
await bank.pingUntilAvailable(); await bank.pingUntilAvailable();
@ -93,8 +95,6 @@ export async function runWithdrawalFakebankTest(t: GlobalTestState) {
const balResp = await wallet.client.call(WalletApiOperation.GetBalances, {}); const balResp = await wallet.client.call(WalletApiOperation.GetBalances, {});
t.assertAmountEquals("TESTKUDOS:9.72", balResp.balances[0].available); t.assertAmountEquals("TESTKUDOS:9.72", balResp.balances[0].available);
await t.shutdown();
} }
runWithdrawalFakebankTest.suites = ["wallet"]; runWithdrawalFakebankTest.suites = ["wallet"];

View File

@ -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 <http://www.gnu.org/licenses/>
*/
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<TalerAuthentication.TokenSuccessResponse> {
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<void> {
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<TalerCorebankApi.Config> {
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<void> {
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<void> {
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<void> {
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<void> {
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<TalerCorebankApi.PublicAccountsResponse> {
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<TalerCorebankApi.ListBankAccountsResponse> {
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<TalerCorebankApi.AccountData> {
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<TalerCorebankApi.BankAccountTransactionsResponse> {
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<TalerCorebankApi.BankAccountTransactionInfo> {
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<void> {
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<TalerCorebankApi.BankAccountCreateWithdrawalResponse> {
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<TalerCorebankApi.BankAccountGetWithdrawalResponse> {
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<void> {
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<void> {
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<TalerCorebankApi.CashoutPending> {
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<void> {
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<void> {
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<TalerCorebankApi.CashoutConversionResponse> {
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<TalerCorebankApi.Cashouts> {
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<TalerCorebankApi.GlobalCashouts> {
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<TalerCorebankApi.CashoutStatusResponse> {
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<TalerCorebankApi.ConversionRatesResponse> {
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<TalerCorebankApi.MonitorResponse> {
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,)
}
}

View File

@ -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<TalerBankIntegrationApi.BankWithdrawalOperationStatus> {
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<TalerBankIntegrationApi.BankWithdrawalOperationPostResponse> {
const url = new URL(`withdrawal-operation/${woid}`, this.baseUrl);
const resp = await this.httpLib.fetch(url.href, {
method: "POST",
});
return readSuccessResponseJsonOrThrow(resp, codecForBankWithdrawalOperationPostResponse());
}
}

View File

@ -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<TalerRevenueApi.MerchantIncomingHistory> {
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());
}
}

View File

@ -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<TalerWireGatewayApi.TransferResponse> {
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<TalerWireGatewayApi.IncomingHistory> {
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<TalerWireGatewayApi.OutgoingHistory> {
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<TalerWireGatewayApi.AddIncomingResponse> {
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());
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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"
}

View File

@ -50,7 +50,7 @@ export interface HttpResponse {
export const DEFAULT_REQUEST_TIMEOUT_MS = 60000; export const DEFAULT_REQUEST_TIMEOUT_MS = 60000;
export interface HttpRequestOptions { export interface HttpRequestOptions {
method?: "POST" | "PUT" | "GET" | "DELETE"; method?: "POST" | "PATCH" | "PUT" | "GET" | "DELETE";
headers?: { [name: string]: string }; headers?: { [name: string]: string };
/** /**

View File

@ -42,3 +42,8 @@ export * from "./transaction-test-data.js";
export * from "./libeufin-api-types.js"; export * from "./libeufin-api-types.js";
export * from "./MerchantApiClient.js"; export * from "./MerchantApiClient.js";
export * from "./bank-api-client.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";

View File

@ -123,7 +123,7 @@ function getValue(chr: string): number {
switch (chr) { switch (chr) {
case "O": case "O":
case "o": case "o":
a = "0;"; a = "0";
break; break;
case "i": case "i":
case "I": case "I":

View File

@ -951,7 +951,7 @@ export interface AccountInfo {
/** /**
* @deprecated * @deprecated
*/ */
export interface ExchangeWireJson {} export interface ExchangeWireJson { }
/** /**
* Proposal returned from the contract URL. * Proposal returned from the contract URL.
@ -1006,8 +1006,8 @@ export class WithdrawOperationStatusResponse {
/** /**
* Response from the merchant. * Response from the merchant.
*/ */
export class TipPickupGetResponse { export class RewardPickupGetResponse {
tip_amount: string; reward_amount: string;
exchange_url: string; exchange_url: string;
@ -1310,12 +1310,6 @@ export const codecForDenominationPubKey = () =>
.alternative(DenomKeyType.ClauseSchnorr, codecForCsDenominationPubKey()) .alternative(DenomKeyType.ClauseSchnorr, codecForCsDenominationPubKey())
.build("DenominationPubKey"); .build("DenominationPubKey");
export const codecForBankWithdrawalOperationPostResponse =
(): Codec<BankWithdrawalOperationPostResponse> =>
buildCodecForObject<BankWithdrawalOperationPostResponse>()
.property("transfer_done", codecForBoolean())
.build("BankWithdrawalOperationPostResponse");
export type AmountString = string; export type AmountString = string;
export type Base32String = string; export type Base32String = string;
export type EddsaSignatureString = string; export type EddsaSignatureString = string;
@ -1572,9 +1566,9 @@ export const codecForWithdrawOperationStatusResponse =
.property("wire_types", codecForList(codecForString())) .property("wire_types", codecForList(codecForString()))
.build("WithdrawOperationStatusResponse"); .build("WithdrawOperationStatusResponse");
export const codecForTipPickupGetResponse = (): Codec<TipPickupGetResponse> => export const codecForRewardPickupGetResponse = (): Codec<RewardPickupGetResponse> =>
buildCodecForObject<TipPickupGetResponse>() buildCodecForObject<RewardPickupGetResponse>()
.property("tip_amount", codecForString()) .property("reward_amount", codecForString())
.property("exchange_url", codecForString()) .property("exchange_url", codecForString())
.property("next_url", codecOptional(codecForString())) .property("next_url", codecOptional(codecForString()))
.property("expiration", codecForTimestamp) .property("expiration", codecForTimestamp)
@ -1591,7 +1585,7 @@ export const codecForWithdrawResponse = (): Codec<ExchangeWithdrawResponse> =>
.property("ev_sig", codecForBlindedDenominationSignature()) .property("ev_sig", codecForBlindedDenominationSignature())
.build("WithdrawResponse"); .build("WithdrawResponse");
export const codecForWithdrawBatchResponse = export const codecForExchangeWithdrawBatchResponse =
(): Codec<ExchangeWithdrawBatchResponse> => (): Codec<ExchangeWithdrawBatchResponse> =>
buildCodecForObject<ExchangeWithdrawBatchResponse>() buildCodecForObject<ExchangeWithdrawBatchResponse>()
.property("ev_sigs", codecForList(codecForWithdrawResponse())) .property("ev_sigs", codecForList(codecForWithdrawResponse()))

View File

@ -43,6 +43,8 @@ import {
codecForList, codecForList,
codecForAny, codecForAny,
codecForBoolean, codecForBoolean,
codecForEither,
codecForConstString,
} from "./codec.js"; } from "./codec.js";
import { import {
RefreshReason, RefreshReason,
@ -62,6 +64,13 @@ export interface TransactionsRequest {
*/ */
search?: string; 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. * If true, include all refreshes in the transactions list.
*/ */
@ -690,6 +699,15 @@ export const codecForTransactionsRequest = (): Codec<TransactionsRequest> =>
buildCodecForObject<TransactionsRequest>() buildCodecForObject<TransactionsRequest>()
.property("currency", codecOptional(codecForString())) .property("currency", codecOptional(codecForString()))
.property("search", codecOptional(codecForString())) .property("search", codecOptional(codecForString()))
.property(
"sort",
codecOptional(
codecForEither(
codecForConstString("ascending"),
codecForConstString("descending"),
),
),
)
.property("includeRefreshes", codecOptional(codecForBoolean())) .property("includeRefreshes", codecOptional(codecForBoolean()))
.build("TransactionsRequest"); .build("TransactionsRequest");

View File

@ -46,6 +46,7 @@ import {
codecOptional, codecOptional,
renderContext, renderContext,
} from "./codec.js"; } from "./codec.js";
import { CurrencySpecification } from "./index.js";
import { VersionMatchResult } from "./libtool-version.js"; import { VersionMatchResult } from "./libtool-version.js";
import { PaytoUri } from "./payto.js"; import { PaytoUri } from "./payto.js";
import { AgeCommitmentProof } from "./taler-crypto.js"; import { AgeCommitmentProof } from "./taler-crypto.js";
@ -429,16 +430,6 @@ export interface GetCurrencySpecificationResponse {
currencySpecification: CurrencySpecification; 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 { export interface InitRequest {
skipDefaults?: boolean; skipDefaults?: boolean;
} }
@ -556,11 +547,11 @@ export interface CoinDumpJson {
withdrawal_reserve_pub: string | undefined; withdrawal_reserve_pub: string | undefined;
coin_status: CoinStatus; coin_status: CoinStatus;
spend_allocation: spend_allocation:
| { | {
id: string; id: string;
amount: string; amount: string;
} }
| undefined; | undefined;
/** /**
* Information about the age restriction * Information about the age restriction
*/ */

View File

@ -49,6 +49,9 @@ import {
Logger, Logger,
parsePaytoUri, parsePaytoUri,
UnblindedSignature, UnblindedSignature,
ExchangeBatchWithdrawRequest,
ExchangeWithdrawBatchResponse,
codecForExchangeWithdrawBatchResponse,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { import {
HttpRequestLibrary, HttpRequestLibrary,
@ -165,25 +168,29 @@ export async function withdrawCoin(args: {
value: Amounts.parseOrThrow(denom.value), value: Amounts.parseOrThrow(denom.value),
}); });
const reqBody: ExchangeWithdrawRequest = { const reqBody: ExchangeBatchWithdrawRequest = {
denom_pub_hash: planchet.denomPubHash, planchets: [
reserve_sig: planchet.withdrawSig, {
coin_ev: planchet.coinEv, denom_pub_hash: planchet.denomPubHash,
reserve_sig: planchet.withdrawSig,
coin_ev: planchet.coinEv,
},
],
}; };
const reqUrl = new URL( const reqUrl = new URL(
`reserves/${planchet.reservePub}/withdraw`, `reserves/${planchet.reservePub}/batch-withdraw`,
exchangeBaseUrl, exchangeBaseUrl,
).href; ).href;
const resp = await http.postJson(reqUrl, reqBody); const resp = await http.fetch(reqUrl, { method: "POST", body: reqBody });
const r = await readSuccessResponseJsonOrThrow( const rBatch = await readSuccessResponseJsonOrThrow(
resp, resp,
codecForWithdrawResponse(), codecForExchangeWithdrawBatchResponse(),
); );
const ubSig = await cryptoApi.unblindDenominationSignature({ const ubSig = await cryptoApi.unblindDenominationSignature({
planchet, planchet,
evSig: r.ev_sig, evSig: rBatch.ev_sigs[0].ev_sig,
}); });
return { return {

View File

@ -101,14 +101,14 @@ async function gatherExchangePending(
case ExchangeEntryDbUpdateStatus.Failed: case ExchangeEntryDbUpdateStatus.Failed:
return; return;
} }
const opTag = TaskIdentifiers.forExchangeUpdate(exch); const opUpdateExchangeTag = TaskIdentifiers.forExchangeUpdate(exch);
let opr = await tx.operationRetries.get(opTag); let opr = await tx.operationRetries.get(opUpdateExchangeTag);
const timestampDue = opr?.retryInfo.nextRetry ?? exch.nextRefreshCheckStamp; const timestampDue = opr?.retryInfo.nextRetry ?? exch.nextRefreshCheckStamp;
resp.pendingOperations.push({ resp.pendingOperations.push({
type: PendingTaskType.ExchangeUpdate, type: PendingTaskType.ExchangeUpdate,
...getPendingCommon( ...getPendingCommon(
ws, ws,
opTag, opUpdateExchangeTag,
AbsoluteTime.fromPreciseTimestamp(timestampPreciseFromDb(timestampDue)), AbsoluteTime.fromPreciseTimestamp(timestampPreciseFromDb(timestampDue)),
), ),
givesLifeness: false, givesLifeness: false,
@ -119,11 +119,12 @@ async function gatherExchangePending(
// We only schedule a check for auto-refresh if the exchange update // We only schedule a check for auto-refresh if the exchange update
// was successful. // was successful.
if (!opr?.lastError) { if (!opr?.lastError) {
const opCheckRefreshTag = TaskIdentifiers.forExchangeCheckRefresh(exch);
resp.pendingOperations.push({ resp.pendingOperations.push({
type: PendingTaskType.ExchangeCheckRefresh, type: PendingTaskType.ExchangeCheckRefresh,
...getPendingCommon( ...getPendingCommon(
ws, ws,
opTag, opCheckRefreshTag,
AbsoluteTime.fromPreciseTimestamp( AbsoluteTime.fromPreciseTimestamp(
timestampPreciseFromDb(timestampDue), timestampPreciseFromDb(timestampDue),
), ),

View File

@ -23,7 +23,7 @@ import {
Amounts, Amounts,
BlindedDenominationSignature, BlindedDenominationSignature,
codecForMerchantTipResponseV2, codecForMerchantTipResponseV2,
codecForTipPickupGetResponse, codecForRewardPickupGetResponse,
CoinStatus, CoinStatus,
DenomKeyType, DenomKeyType,
encodeCrock, encodeCrock,
@ -168,11 +168,11 @@ export async function prepareTip(
const merchantResp = await ws.http.fetch(tipStatusUrl.href); const merchantResp = await ws.http.fetch(tipStatusUrl.href);
const tipPickupStatus = await readSuccessResponseJsonOrThrow( const tipPickupStatus = await readSuccessResponseJsonOrThrow(
merchantResp, merchantResp,
codecForTipPickupGetResponse(), codecForRewardPickupGetResponse(),
); );
logger.trace(`status ${j2s(tipPickupStatus)}`); 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"); logger.trace("new tip, creating tip record");
await updateExchangeFromUrl(ws, tipPickupStatus.exchange_url); await updateExchangeFromUrl(ws, tipPickupStatus.exchange_url);

View File

@ -74,6 +74,7 @@ import {
import { initiatePeerPushDebit } from "./pay-peer-push-debit.js"; import { initiatePeerPushDebit } from "./pay-peer-push-debit.js";
import { OpenedPromise, openPromise } from "../index.js"; import { OpenedPromise, openPromise } from "../index.js";
import { getTransactionById, getTransactions } from "./transactions.js"; import { getTransactionById, getTransactions } from "./transactions.js";
import { getPendingOperations } from "./pending.js";
const logger = new Logger("operations/testing.ts"); const logger = new Logger("operations/testing.ts");
@ -290,7 +291,7 @@ export async function runIntegrationTest(
corebankApiBaseUrl: args.corebankApiBaseUrl, corebankApiBaseUrl: args.corebankApiBaseUrl,
exchangeBaseUrl: args.exchangeBaseUrl, exchangeBaseUrl: args.exchangeBaseUrl,
}); });
await waitUntilDone(ws); await waitUntilTransactionsFinal(ws);
logger.info("done withdrawing test balance"); logger.info("done withdrawing test balance");
const balance = await getBalances(ws); const balance = await getBalances(ws);
@ -305,7 +306,7 @@ export async function runIntegrationTest(
await makePayment(ws, myMerchant, args.amountToSpend, "hello world"); await makePayment(ws, myMerchant, args.amountToSpend, "hello world");
// Wait until the refresh is done // Wait until the refresh is done
await waitUntilDone(ws); await waitUntilTransactionsFinal(ws);
logger.trace("withdrawing test balance for refund"); logger.trace("withdrawing test balance for refund");
const withdrawAmountTwo = Amounts.parseOrThrow(`${currency}:18`); const withdrawAmountTwo = Amounts.parseOrThrow(`${currency}:18`);
@ -320,7 +321,7 @@ export async function runIntegrationTest(
}); });
// Wait until the withdraw is done // Wait until the withdraw is done
await waitUntilDone(ws); await waitUntilTransactionsFinal(ws);
const { orderId: refundOrderId } = await makePayment( const { orderId: refundOrderId } = await makePayment(
ws, ws,
@ -344,7 +345,7 @@ export async function runIntegrationTest(
logger.trace("integration test: applied refund"); logger.trace("integration test: applied refund");
// Wait until the refund is done // Wait until the refund is done
await waitUntilDone(ws); await waitUntilTransactionsFinal(ws);
logger.trace("integration test: making payment after refund"); logger.trace("integration test: making payment after refund");
@ -357,12 +358,17 @@ export async function runIntegrationTest(
logger.trace("integration test: make payment done"); logger.trace("integration test: make payment done");
await waitUntilDone(ws); await waitUntilTransactionsFinal(ws);
logger.trace("integration test: all done!"); logger.trace("integration test: all done!");
} }
export async function waitUntilDone(ws: InternalWalletState): Promise<void> { /**
* Wait until all transactions are in a final state.
*/
export async function waitUntilTransactionsFinal(
ws: InternalWalletState,
): Promise<void> {
logger.info("waiting until all transactions are in a final state"); logger.info("waiting until all transactions are in a final state");
ws.ensureTaskLoopRunning(); ws.ensureTaskLoopRunning();
let p: OpenedPromise<void> | undefined = undefined; let p: OpenedPromise<void> | undefined = undefined;
@ -410,6 +416,44 @@ export async function waitUntilDone(ws: InternalWalletState): Promise<void> {
logger.info("done waiting until all transactions are in a final state"); 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<void> {
logger.info("waiting until pending work is processed");
ws.ensureTaskLoopRunning();
let p: OpenedPromise<void> | 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( export async function waitUntilRefreshesDone(
ws: InternalWalletState, ws: InternalWalletState,
): Promise<void> { ): Promise<void> {
@ -463,7 +507,7 @@ export async function waitUntilRefreshesDone(
logger.info("done waiting until all refreshes are in a final state"); logger.info("done waiting until all refreshes are in a final state");
} }
async function waitUntilPendingReady( async function waitUntilTransactionPendingReady(
ws: InternalWalletState, ws: InternalWalletState,
transactionId: string, transactionId: string,
): Promise<void> { ): Promise<void> {
@ -560,7 +604,7 @@ export async function runIntegrationTest2(
corebankApiBaseUrl: args.corebankApiBaseUrl, corebankApiBaseUrl: args.corebankApiBaseUrl,
exchangeBaseUrl: args.exchangeBaseUrl, exchangeBaseUrl: args.exchangeBaseUrl,
}); });
await waitUntilDone(ws); await waitUntilTransactionsFinal(ws);
logger.info("done withdrawing test balance"); logger.info("done withdrawing test balance");
const balance = await getBalances(ws); const balance = await getBalances(ws);
@ -580,7 +624,7 @@ export async function runIntegrationTest2(
); );
// Wait until the refresh is done // Wait until the refresh is done
await waitUntilDone(ws); await waitUntilTransactionsFinal(ws);
logger.trace("withdrawing test balance for refund"); logger.trace("withdrawing test balance for refund");
const withdrawAmountTwo = Amounts.parseOrThrow(`${currency}:18`); const withdrawAmountTwo = Amounts.parseOrThrow(`${currency}:18`);
@ -595,7 +639,7 @@ export async function runIntegrationTest2(
}); });
// Wait until the withdraw is done // Wait until the withdraw is done
await waitUntilDone(ws); await waitUntilTransactionsFinal(ws);
const { orderId: refundOrderId } = await makePayment( const { orderId: refundOrderId } = await makePayment(
ws, ws,
@ -619,7 +663,7 @@ export async function runIntegrationTest2(
logger.trace("integration test: applied refund"); logger.trace("integration test: applied refund");
// Wait until the refund is done // Wait until the refund is done
await waitUntilDone(ws); await waitUntilTransactionsFinal(ws);
logger.trace("integration test: making payment after refund"); logger.trace("integration test: making payment after refund");
@ -632,7 +676,7 @@ export async function runIntegrationTest2(
logger.trace("integration test: make payment done"); logger.trace("integration test: make payment done");
await waitUntilDone(ws); await waitUntilTransactionsFinal(ws);
const peerPushInit = await initiatePeerPushDebit(ws, { const peerPushInit = await initiatePeerPushDebit(ws, {
partialContractTerms: { partialContractTerms: {
@ -647,7 +691,7 @@ export async function runIntegrationTest2(
}, },
}); });
await waitUntilPendingReady(ws, peerPushInit.transactionId); await waitUntilTransactionPendingReady(ws, peerPushInit.transactionId);
const peerPushCredit = await preparePeerPushCredit(ws, { const peerPushCredit = await preparePeerPushCredit(ws, {
talerUri: peerPushInit.talerUri, 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, { const peerPullInc = await preparePeerPullDebit(ws, {
talerUri: peerPullInit.talerUri, talerUri: peerPullInit.talerUri,
@ -680,7 +724,7 @@ export async function runIntegrationTest2(
peerPullDebitId: peerPullInc.peerPullDebitId, peerPullDebitId: peerPullInc.peerPullDebitId,
}); });
await waitUntilDone(ws); await waitUntilTransactionsFinal(ws);
logger.trace("integration test: all done!"); logger.trace("integration test: all done!");
} }

View File

@ -1290,9 +1290,16 @@ export async function getTransactions(
const txPending = transactions.filter((x) => isPending(x)); const txPending = transactions.filter((x) => isPending(x));
const txNotPending = 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) => { const txCmp = (h1: Transaction, h2: Transaction) => {
// Order transactions by timestamp. Newest transactions come first. // Order transactions by timestamp. Newest transactions come first.
const tsCmp = -AbsoluteTime.cmp( const tsCmp = AbsoluteTime.cmp(
AbsoluteTime.fromPreciseTimestamp(h1.timestamp), AbsoluteTime.fromPreciseTimestamp(h1.timestamp),
AbsoluteTime.fromPreciseTimestamp(h2.timestamp), AbsoluteTime.fromPreciseTimestamp(h2.timestamp),
); );
@ -1300,7 +1307,7 @@ export async function getTransactions(
if (tsCmp === 0) { if (tsCmp === 0) {
return Math.sign(txOrder[h1.type] - txOrder[h2.type]); return Math.sign(txOrder[h1.type] - txOrder[h2.type]);
} }
return tsCmp; return sortSign * tsCmp;
}; };
txPending.sort(txCmp); txPending.sort(txCmp);

View File

@ -33,7 +33,7 @@ import {
codecForReserveStatus, codecForReserveStatus,
codecForTalerConfigResponse, codecForTalerConfigResponse,
codecForWalletKycUuid, codecForWalletKycUuid,
codecForWithdrawBatchResponse, codecForExchangeWithdrawBatchResponse,
codecForWithdrawOperationStatusResponse, codecForWithdrawOperationStatusResponse,
codecForWithdrawResponse, codecForWithdrawResponse,
CoinStatus, CoinStatus,
@ -944,7 +944,7 @@ async function processPlanchetExchangeBatchRequest(
} }
const r = await readSuccessResponseJsonOrThrow( const r = await readSuccessResponseJsonOrThrow(
resp, resp,
codecForWithdrawBatchResponse(), codecForExchangeWithdrawBatchResponse(),
); );
return { return {
coinIdxs: requestCoinIdxs, coinIdxs: requestCoinIdxs,

View File

@ -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 * Will be replaced with the value from package.json in a
* post-compilation step (inside lib/). * 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.
*/

View File

@ -224,6 +224,7 @@ export enum WalletApiOperation {
DeleteStoredBackup = "deleteStoredBackup", DeleteStoredBackup = "deleteStoredBackup",
RecoverStoredBackup = "recoverStoredBackup", RecoverStoredBackup = "recoverStoredBackup",
UpdateExchangeEntry = "updateExchangeEntry", UpdateExchangeEntry = "updateExchangeEntry",
TestingWaitTasksProcessed = "testingWaitTasksProcessed",
} }
// group: Initialization // group: Initialization
@ -1007,7 +1008,7 @@ export type TestingSetTimetravelOp = {
/** /**
* Wait until all transactions are in a final state. * Wait until all transactions are in a final state.
*/ */
export type TestingWaitTransactionsFinal = { export type TestingWaitTransactionsFinalOp = {
op: WalletApiOperation.TestingWaitTransactionsFinal; op: WalletApiOperation.TestingWaitTransactionsFinal;
request: EmptyObject; request: EmptyObject;
response: EmptyObject; response: EmptyObject;
@ -1016,12 +1017,21 @@ export type TestingWaitTransactionsFinal = {
/** /**
* Wait until all refresh transactions are in a final state. * Wait until all refresh transactions are in a final state.
*/ */
export type TestingWaitRefreshesFinal = { export type TestingWaitRefreshesFinalOp = {
op: WalletApiOperation.TestingWaitRefreshesFinal; op: WalletApiOperation.TestingWaitRefreshesFinal;
request: EmptyObject; request: EmptyObject;
response: 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. * Wait until a transaction is in a particular state.
*/ */
@ -1132,8 +1142,9 @@ export type WalletOperations = {
[WalletApiOperation.Recycle]: RecycleOp; [WalletApiOperation.Recycle]: RecycleOp;
[WalletApiOperation.ApplyDevExperiment]: ApplyDevExperimentOp; [WalletApiOperation.ApplyDevExperiment]: ApplyDevExperimentOp;
[WalletApiOperation.ValidateIban]: ValidateIbanOp; [WalletApiOperation.ValidateIban]: ValidateIbanOp;
[WalletApiOperation.TestingWaitTransactionsFinal]: TestingWaitTransactionsFinal; [WalletApiOperation.TestingWaitTransactionsFinal]: TestingWaitTransactionsFinalOp;
[WalletApiOperation.TestingWaitRefreshesFinal]: TestingWaitRefreshesFinal; [WalletApiOperation.TestingWaitRefreshesFinal]: TestingWaitRefreshesFinalOp;
[WalletApiOperation.TestingWaitTasksProcessed]: TestingWaitTasksProcessedOp;
[WalletApiOperation.TestingSetTimetravel]: TestingSetTimetravelOp; [WalletApiOperation.TestingSetTimetravel]: TestingSetTimetravelOp;
[WalletApiOperation.TestingWaitTransactionState]: TestingWaitTransactionStateOp; [WalletApiOperation.TestingWaitTransactionState]: TestingWaitTransactionStateOp;
[WalletApiOperation.GetCurrencySpecification]: GetCurrencySpecificationOp; [WalletApiOperation.GetCurrencySpecification]: GetCurrencySpecificationOp;

View File

@ -252,9 +252,10 @@ import {
runIntegrationTest2, runIntegrationTest2,
testPay, testPay,
waitTransactionState, waitTransactionState,
waitUntilDone, waitUntilTransactionsFinal,
waitUntilRefreshesDone, waitUntilRefreshesDone,
withdrawTestBalance, withdrawTestBalance,
waitUntilTasksProcessed,
} from "./operations/testing.js"; } from "./operations/testing.js";
import { import {
acceptTip, acceptTip,
@ -1427,6 +1428,10 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
await waitTransactionState(ws, req.transactionId, req.txState); await waitTransactionState(ws, req.transactionId, req.txState);
return {}; return {};
} }
case WalletApiOperation.TestingWaitTasksProcessed: {
await waitUntilTasksProcessed(ws);
return {};
}
case WalletApiOperation.GetCurrencySpecification: { case WalletApiOperation.GetCurrencySpecification: {
// Ignore result, just validate in this mock implementation // Ignore result, just validate in this mock implementation
const req = codecForGetCurrencyInfoRequest().decode(payload); const req = codecForGetCurrencyInfoRequest().decode(payload);
@ -1436,9 +1441,9 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
currencySpecification: { currencySpecification: {
decimal_separator: ",", decimal_separator: ",",
name: "Kudos (Taler Demonstrator)", name: "Kudos (Taler Demonstrator)",
fractional_input_digits: 2, num_fractional_input_digits: 2,
fractional_normal_digits: 2, num_fractional_normal_digits: 2,
fractional_trailing_zero_digits: 2, num_fractional_trailing_zero_digits: 2,
is_currency_name_leading: true, is_currency_name_leading: true,
alt_unit_names: { alt_unit_names: {
"0": "ク", "0": "ク",
@ -1451,9 +1456,9 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
currencySpecification: { currencySpecification: {
decimal_separator: ",", decimal_separator: ",",
name: "Test (Taler Unstable Demonstrator)", name: "Test (Taler Unstable Demonstrator)",
fractional_input_digits: 0, num_fractional_input_digits: 0,
fractional_normal_digits: 0, num_fractional_normal_digits: 0,
fractional_trailing_zero_digits: 0, num_fractional_trailing_zero_digits: 0,
is_currency_name_leading: false, is_currency_name_leading: false,
alt_unit_names: {}, alt_unit_names: {},
}, },
@ -1464,9 +1469,9 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
currencySpecification: { currencySpecification: {
decimal_separator: ",", decimal_separator: ",",
name: "Unknown", name: "Unknown",
fractional_input_digits: 2, num_fractional_input_digits: 2,
fractional_normal_digits: 2, num_fractional_normal_digits: 2,
fractional_trailing_zero_digits: 2, num_fractional_trailing_zero_digits: 2,
is_currency_name_leading: true, is_currency_name_leading: true,
alt_unit_names: {}, alt_unit_names: {},
}, },
@ -1600,7 +1605,7 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
return getVersion(ws); return getVersion(ws);
} }
case WalletApiOperation.TestingWaitTransactionsFinal: case WalletApiOperation.TestingWaitTransactionsFinal:
return await waitUntilDone(ws); return await waitUntilTransactionsFinal(ws);
case WalletApiOperation.TestingWaitRefreshesFinal: case WalletApiOperation.TestingWaitRefreshesFinal:
return await waitUntilRefreshesDone(ws); return await waitUntilRefreshesDone(ws);
case WalletApiOperation.TestingSetTimetravel: { case WalletApiOperation.TestingSetTimetravel: {