aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/anastasis-cli/package.json2
-rw-r--r--packages/idb-bridge/package.json2
-rw-r--r--packages/pogen/package.json2
-rw-r--r--packages/taler-harness/package.json5
-rw-r--r--packages/taler-harness/src/bench1.ts9
-rw-r--r--packages/taler-harness/src/harness/harness.ts396
-rw-r--r--packages/taler-harness/src/harness/helpers.ts13
-rw-r--r--packages/taler-harness/src/harness/libeufin-apis.ts519
-rw-r--r--packages/taler-harness/src/harness/libeufin.ts177
-rw-r--r--packages/taler-harness/src/index.ts6
-rw-r--r--packages/taler-harness/src/integrationtests/test-exchange-purse.ts224
-rw-r--r--packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts85
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-api-bankaccount.ts2
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-api-bankconnection.ts4
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-api-facade-bad-request.ts25
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-api-facade.ts10
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-api-permissions.ts5
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-api-sandbox-transactions.ts2
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-api-scheduling.ts12
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-api-users.ts4
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-c5x.ts18
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-facade-anastasis.ts4
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-refund.ts2
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-sandbox-wire-transfer-cli.ts4
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts16
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts22
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-instances-urls.ts9
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-instances.ts48
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-longpolling.ts33
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-refund-api.ts34
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-spec-public-orders.ts83
-rw-r--r--packages/taler-harness/src/integrationtests/test-pay-paid.ts24
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-abort.ts21
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-claim.ts4
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-idempotency.ts4
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-template.ts3
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-transient.ts37
-rw-r--r--packages/taler-harness/src/integrationtests/test-paywall-flow.ts20
-rw-r--r--packages/taler-harness/src/integrationtests/test-refund-auto.ts4
-rw-r--r--packages/taler-harness/src/integrationtests/test-refund-incremental.ts4
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-balance.ts19
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-dbless.ts8
-rw-r--r--packages/taler-harness/src/integrationtests/testrunner.ts42
-rw-r--r--packages/taler-harness/src/lint.ts6
-rw-r--r--packages/taler-util/package.json2
-rw-r--r--packages/taler-util/src/http-common.ts29
-rw-r--r--packages/taler-util/src/payto.ts22
-rw-r--r--packages/taler-util/src/taler-crypto.ts5
-rw-r--r--packages/taler-util/src/taler-types.ts135
-rw-r--r--packages/taler-wallet-cli/package.json4
-rw-r--r--packages/taler-wallet-core/package.json3
-rw-r--r--packages/taler-wallet-core/src/bank-api-client.ts2
-rw-r--r--packages/taler-wallet-core/src/crypto/cryptoImplementation.ts5
-rw-r--r--packages/taler-wallet-core/src/db.ts1
-rw-r--r--packages/taler-wallet-core/src/dbless.ts9
-rw-r--r--packages/taler-wallet-core/src/dev-experiments.ts2
-rw-r--r--packages/taler-wallet-core/src/index.ts8
-rw-r--r--packages/taler-wallet-core/src/operations/backup/index.ts2
-rw-r--r--packages/taler-wallet-core/src/operations/exchanges.ts170
-rw-r--r--packages/taler-wallet-core/src/operations/merchants.ts2
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-common.ts287
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts2
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts5
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts4
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts3
-rw-r--r--packages/taler-wallet-core/src/operations/recoup.ts2
-rw-r--r--packages/taler-wallet-core/src/operations/reward.ts2
-rw-r--r--packages/taler-wallet-core/src/operations/testing.ts2
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw.ts6
-rw-r--r--packages/taler-wallet-core/src/util/coinSelection.test.ts742
-rw-r--r--packages/taler-wallet-core/src/util/coinSelection.ts1012
-rw-r--r--packages/taler-wallet-core/src/util/instructedAmountConversion.test.ts763
-rw-r--r--packages/taler-wallet-core/src/util/instructedAmountConversion.ts849
-rw-r--r--packages/taler-wallet-core/src/versions.ts2
-rw-r--r--packages/taler-wallet-core/src/wallet.ts14
-rw-r--r--packages/web-util/package.json1
-rw-r--r--pnpm-lock.yaml35
77 files changed, 3156 insertions, 2949 deletions
diff --git a/packages/anastasis-cli/package.json b/packages/anastasis-cli/package.json
index 1821cd696..2bbf32c3b 100644
--- a/packages/anastasis-cli/package.json
+++ b/packages/anastasis-cli/package.json
@@ -16,7 +16,7 @@
},
"type": "module",
"scripts": {
- "compile": "tsc --build && ./build-node.mjs",
+ "compile": "tsc && ./build-node.mjs",
"test": "tsc",
"clean": "rimraf lib dist tsconfig.tsbuildinfo",
"pretty": "prettier --write src"
diff --git a/packages/idb-bridge/package.json b/packages/idb-bridge/package.json
index 2677c302f..1c9c46b02 100644
--- a/packages/idb-bridge/package.json
+++ b/packages/idb-bridge/package.json
@@ -11,7 +11,7 @@
"private": false,
"scripts": {
"test": "tsc && ava",
- "compile": "tsc --build",
+ "compile": "tsc",
"clean": "rimraf dist lib tsconfig.tsbuildinfo",
"pretty": "prettier --write src"
},
diff --git a/packages/pogen/package.json b/packages/pogen/package.json
index c5252cd62..ae1dfac3b 100644
--- a/packages/pogen/package.json
+++ b/packages/pogen/package.json
@@ -7,7 +7,7 @@
"author": "Florian Dold",
"license": "GPL-2.0+",
"scripts": {
- "compile": "tsc --build"
+ "compile": "tsc"
},
"devDependencies": {
"po2json": "^0.4.5",
diff --git a/packages/taler-harness/package.json b/packages/taler-harness/package.json
index 6dfe9b74b..8916e5e9e 100644
--- a/packages/taler-harness/package.json
+++ b/packages/taler-harness/package.json
@@ -16,7 +16,7 @@
},
"type": "module",
"scripts": {
- "compile": "tsc --build && ./build.mjs",
+ "compile": "tsc && ./build.mjs",
"check": "tsc",
"test": "tsc",
"clean": "rimraf lib dist tsconfig.tsbuildinfo",
@@ -40,7 +40,6 @@
"dependencies": {
"@gnu-taler/taler-util": "workspace:*",
"@gnu-taler/taler-wallet-core": "workspace:*",
- "axios": "^0.27.2",
"tslib": "^2.5.3"
}
-} \ No newline at end of file
+}
diff --git a/packages/taler-harness/src/bench1.ts b/packages/taler-harness/src/bench1.ts
index 6aa444e0a..618eb683e 100644
--- a/packages/taler-harness/src/bench1.ts
+++ b/packages/taler-harness/src/bench1.ts
@@ -26,13 +26,13 @@ import {
j2s,
Logger,
} from "@gnu-taler/taler-util";
-import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
import {
AccessStats,
createNativeWalletHost2,
Wallet,
WalletApiOperation,
} from "@gnu-taler/taler-wallet-core";
+import { harnessHttpLib } from "./harness/harness.js";
/**
* Entry point for the benchmark.
@@ -46,11 +46,6 @@ export async function runBench1(configJson: any): Promise<void> {
// Validate the configuration file for this benchmark.
const b1conf = codecForBench1Config().decode(configJson);
- const myHttpLib = createPlatformHttpLib({
- enableThrottling: false,
- allowHttp: true,
- });
-
const numIter = b1conf.iterations ?? 1;
const numDeposits = b1conf.deposits ?? 5;
const restartWallet = b1conf.restartAfter ?? 20;
@@ -85,7 +80,7 @@ export async function runBench1(configJson: any): Promise<void> {
const res = await createNativeWalletHost2({
// No persistent DB storage.
persistentStoragePath: undefined,
- httpLib: myHttpLib,
+ httpLib: harnessHttpLib,
config: {
testing: {
insecureTrustExchange: trustExchange,
diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts
index 926a0c93b..7db9d82bd 100644
--- a/packages/taler-harness/src/harness/harness.ts
+++ b/packages/taler-harness/src/harness/harness.ts
@@ -55,9 +55,11 @@ import {
RewardCreateRequest,
TippingReserveStatus,
WalletNotification,
+ codecForAny,
} from "@gnu-taler/taler-util";
import {
createPlatformHttpLib,
+ expectSuccessResponseOrThrow,
readSuccessResponseJsonOrThrow,
} from "@gnu-taler/taler-util/http";
import {
@@ -78,7 +80,6 @@ import {
WalletNotificationWaiter,
} from "@gnu-taler/taler-wallet-core/remote";
import { deepStrictEqual } from "assert";
-import axiosImp, { AxiosError } from "axios";
import { ChildProcess, spawn } from "child_process";
import * as fs from "fs";
import * as http from "http";
@@ -87,12 +88,9 @@ import * as path from "path";
import * as readline from "readline";
import { URL } from "url";
import { CoinConfig } from "./denomStructures.js";
-import { LibeufinNexusApi, LibeufinSandboxApi } from "./libeufin-apis.js";
const logger = new Logger("harness.ts");
-const axios = axiosImp.default;
-
export async function delayMs(ms: number): Promise<void> {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(), ms);
@@ -322,12 +320,6 @@ export class GlobalTestState {
);
}
- assertAxiosError(e: any): asserts e is AxiosError {
- if (!e.isAxiosError) {
- throw Error("expected axios error");
- }
- }
-
assertTrue(b: boolean): asserts b {
if (!b) {
throw Error("test assertion failed");
@@ -558,7 +550,10 @@ export async function pingProc(
while (true) {
try {
logger.trace(`pinging ${serviceName} at ${url}`);
- const resp = await axios.get(url);
+ const resp = await harnessHttpLib.fetch(url);
+ if (resp.status !== 200) {
+ throw Error("non-200 status code");
+ }
logger.trace(`service ${serviceName} available`);
return;
} catch (e: any) {
@@ -584,289 +579,6 @@ class BankServiceBase {
}
/**
- * Work in progress. The key point is that both Sandbox and Nexus
- * will be configured and started by this class.
- */
-class LibEuFinBankService extends BankServiceBase implements BankServiceHandle {
- sandboxProc: ProcessWrapper | undefined;
- nexusProc: ProcessWrapper | undefined;
-
- http = createPlatformHttpLib({
- allowHttp: true,
- enableThrottling: false,
- });
-
- static async create(
- gc: GlobalTestState,
- bc: BankConfig,
- ): Promise<LibEuFinBankService> {
- return new LibEuFinBankService(gc, bc, "foo");
- }
-
- get port() {
- return this.bankConfig.httpPort;
- }
- get nexusPort() {
- return this.bankConfig.httpPort + 1000;
- }
-
- get nexusDbConn(): string {
- return `jdbc:sqlite:${this.globalTestState.testDir}/libeufin-nexus.sqlite3`;
- }
-
- get sandboxDbConn(): string {
- return `jdbc:sqlite:${this.globalTestState.testDir}/libeufin-sandbox.sqlite3`;
- }
-
- get nexusBaseUrl(): string {
- return `http://localhost:${this.nexusPort}`;
- }
-
- get baseUrlDemobank(): string {
- let url = new URL("demobanks/default/", this.baseUrlNetloc);
- return url.href;
- }
-
- get bankAccessApiBaseUrl(): string {
- let url = new URL("access-api/", this.baseUrlDemobank);
- return url.href;
- }
-
- get baseUrlNetloc(): string {
- return `http://localhost:${this.bankConfig.httpPort}/`;
- }
-
- get baseUrl(): string {
- return this.bankAccessApiBaseUrl;
- }
-
- async setSuggestedExchange(
- e: ExchangeServiceInterface,
- exchangePayto: string,
- ) {
- await sh(
- this.globalTestState,
- "libeufin-sandbox-set-default-exchange",
- `libeufin-sandbox default-exchange ${e.baseUrl} ${exchangePayto}`,
- {
- ...process.env,
- LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxDbConn,
- },
- );
- }
-
- // Create one at both sides: Sandbox and Nexus.
- async createExchangeAccount(
- accountName: string,
- password: string,
- ): Promise<HarnessExchangeBankAccount> {
- logger.info("Create Exchange account(s)!");
- /**
- * Many test cases try to create a Exchange account before
- * starting the bank; that's because the Pybank did it entirely
- * via the configuration file.
- */
- await this.start();
- await this.pingUntilAvailable();
- await LibeufinSandboxApi.createDemobankAccount(accountName, password, {
- baseUrl: this.bankAccessApiBaseUrl,
- });
- let bankAccountLabel = accountName;
- await LibeufinSandboxApi.createDemobankEbicsSubscriber(
- {
- hostID: "talertestEbicsHost",
- userID: "exchangeEbicsUser",
- partnerID: "exchangeEbicsPartner",
- },
- bankAccountLabel,
- { baseUrl: this.baseUrlDemobank },
- );
-
- await LibeufinNexusApi.createUser(
- { baseUrl: this.nexusBaseUrl },
- {
- username: accountName,
- password: password,
- },
- );
- await LibeufinNexusApi.createEbicsBankConnection(
- { baseUrl: this.nexusBaseUrl },
- {
- name: "ebics-connection", // connection name.
- ebicsURL: new URL("ebicsweb", this.baseUrlNetloc).href,
- hostID: "talertestEbicsHost",
- userID: "exchangeEbicsUser",
- partnerID: "exchangeEbicsPartner",
- },
- );
- await LibeufinNexusApi.connectBankConnection(
- { baseUrl: this.nexusBaseUrl },
- "ebics-connection",
- );
- await LibeufinNexusApi.fetchAccounts(
- { baseUrl: this.nexusBaseUrl },
- "ebics-connection",
- );
- await LibeufinNexusApi.importConnectionAccount(
- { baseUrl: this.nexusBaseUrl },
- "ebics-connection", // connection name
- accountName, // offered account label
- `${accountName}-nexus-label`, // bank account label at Nexus
- );
- await LibeufinNexusApi.createTwgFacade(
- { baseUrl: this.nexusBaseUrl },
- {
- name: "exchange-facade",
- connectionName: "ebics-connection",
- accountName: `${accountName}-nexus-label`,
- currency: "EUR",
- reserveTransferLevel: "report",
- },
- );
- await LibeufinNexusApi.postPermission(
- { baseUrl: this.nexusBaseUrl },
- {
- action: "grant",
- permission: {
- subjectId: accountName,
- subjectType: "user",
- resourceType: "facade",
- resourceId: "exchange-facade", // facade name
- permissionName: "facade.talerWireGateway.transfer",
- },
- },
- );
- await LibeufinNexusApi.postPermission(
- { baseUrl: this.nexusBaseUrl },
- {
- action: "grant",
- permission: {
- subjectId: accountName,
- subjectType: "user",
- resourceType: "facade",
- resourceId: "exchange-facade", // facade name
- permissionName: "facade.talerWireGateway.history",
- },
- },
- );
- // Set fetch task.
- await LibeufinNexusApi.postTask(
- { baseUrl: this.nexusBaseUrl },
- `${accountName}-nexus-label`,
- {
- name: "wirewatch-task",
- cronspec: "* * *",
- type: "fetch",
- params: {
- level: "all",
- rangeType: "all",
- },
- },
- );
- await LibeufinNexusApi.postTask(
- { baseUrl: this.nexusBaseUrl },
- `${accountName}-nexus-label`,
- {
- name: "aggregator-task",
- cronspec: "* * *",
- type: "submit",
- params: {},
- },
- );
- let facadesResp = await LibeufinNexusApi.getAllFacades({
- baseUrl: this.nexusBaseUrl,
- });
- let accountInfoResp = await LibeufinSandboxApi.demobankAccountInfo(
- "admin",
- "secret",
- { baseUrl: this.bankAccessApiBaseUrl },
- accountName, // bank account label.
- );
- return {
- accountName: accountName,
- accountPassword: password,
- accountPaytoUri: accountInfoResp.data.paytoUri,
- wireGatewayApiBaseUrl: facadesResp.data.facades[0].baseUrl,
- };
- }
-
- async start(): Promise<void> {
- /**
- * Because many test cases try to create a Exchange bank
- * account _before_ starting the bank (Pybank did it only via
- * the config), it is possible that at this point Sandbox and
- * Nexus are already running. Hence, this method only launches
- * them if they weren't launched earlier.
- */
-
- // Only go ahead if BOTH aren't running.
- if (this.sandboxProc || this.nexusProc) {
- logger.info("Nexus or Sandbox already running, not taking any action.");
- return;
- }
- await sh(
- this.globalTestState,
- "libeufin-sandbox-config-demobank",
- `libeufin-sandbox config --currency=${this.bankConfig.currency} default`,
- {
- ...process.env,
- LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxDbConn,
- LIBEUFIN_SANDBOX_ADMIN_PASSWORD: "secret",
- },
- );
- this.sandboxProc = this.globalTestState.spawnService(
- "libeufin-sandbox",
- ["serve", "--port", `${this.port}`],
- "libeufin-sandbox",
- {
- ...process.env,
- LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxDbConn,
- LIBEUFIN_SANDBOX_ADMIN_PASSWORD: "secret",
- },
- );
- await runCommand(
- this.globalTestState,
- "libeufin-nexus-superuser",
- "libeufin-nexus",
- ["superuser", "admin", "--password", "test"],
- {
- ...process.env,
- LIBEUFIN_NEXUS_DB_CONNECTION: this.nexusDbConn,
- },
- );
- this.nexusProc = this.globalTestState.spawnService(
- "libeufin-nexus",
- ["serve", "--port", `${this.nexusPort}`],
- "libeufin-nexus",
- {
- ...process.env,
- LIBEUFIN_NEXUS_DB_CONNECTION: this.nexusDbConn,
- },
- );
- // need to wait here, because at this point
- // a Ebics host needs to be created (RESTfully)
- await this.pingUntilAvailable();
- LibeufinSandboxApi.createEbicsHost(
- { baseUrl: this.baseUrlNetloc },
- "talertestEbicsHost",
- );
- }
-
- async pingUntilAvailable(): Promise<void> {
- await pingProc(
- this.sandboxProc,
- `http://localhost:${this.bankConfig.httpPort}`,
- "libeufin-sandbox",
- );
- await pingProc(
- this.nexusProc,
- `${this.nexusBaseUrl}/config`,
- "libeufin-nexus",
- );
- }
-}
-
-/**
* Implementation of the bank service using the "taler-fakebank-run" tool.
*/
export class FakebankService
@@ -1152,6 +864,9 @@ export class ExchangeService implements ExchangeServiceInterface {
"currency_round_unit",
e.roundUnit ?? `${e.currency}:0.01`,
);
+ // Set to a high value to not break existing test cases where the merchant
+ // would cover all fees.
+ config.setString("exchange", "STEFAN_ABS", `${e.currency}:1`);
config.setString(
"exchange",
"revocation_dir",
@@ -1636,20 +1351,30 @@ export interface DeleteTippingReserveArgs {
purge?: boolean;
}
+/**
+ * Default HTTP client handle for the integration test harness.
+ */
+export const harnessHttpLib = createPlatformHttpLib({
+ allowHttp: true,
+ enableThrottling: false,
+});
+
export class MerchantApiClient {
constructor(
private baseUrl: string,
public readonly auth: MerchantAuthConfiguration,
) {}
- // FIXME: Migrate everything to this in favor of axios
- http = createPlatformHttpLib({ allowHttp: true, enableThrottling: false });
+ httpClient = createPlatformHttpLib({ allowHttp: true, enableThrottling: false });
async changeAuth(auth: MerchantAuthConfiguration): Promise<void> {
const url = new URL("private/auth", this.baseUrl);
- await axios.post(url.href, auth, {
+ const res = await this.httpClient.fetch(url.href, {
+ method: "POST",
+ body: auth,
headers: this.makeAuthHeader(),
});
+ await expectSuccessResponseOrThrow(res);
}
async deleteTippingReserve(req: DeleteTippingReserveArgs): Promise<void> {
@@ -1657,7 +1382,8 @@ export class MerchantApiClient {
if (req.purge) {
url.searchParams.set("purge", "YES");
}
- const resp = await axios.delete(url.href, {
+ const resp = await this.httpClient.fetch(url.href, {
+ method: "DELETE",
headers: this.makeAuthHeader(),
});
logger.info(`delete status: ${resp.status}`);
@@ -1668,7 +1394,7 @@ export class MerchantApiClient {
req: CreateMerchantTippingReserveRequest,
): Promise<MerchantReserveCreateConfirmation> {
const url = new URL("private/reserves", this.baseUrl);
- const resp = await this.http.fetch(url.href, {
+ const resp = await this.httpClient.fetch(url.href, {
method: "POST",
body: req,
headers: this.makeAuthHeader(),
@@ -1684,7 +1410,7 @@ export class MerchantApiClient {
console.log(this.makeAuthHeader());
const url = new URL("private", this.baseUrl);
logger.info(`request url ${url.href}`);
- const resp = await this.http.fetch(url.href, {
+ const resp = await this.httpClient.fetch(url.href, {
method: "GET",
headers: this.makeAuthHeader(),
});
@@ -1694,7 +1420,7 @@ export class MerchantApiClient {
async getPrivateTipReserves(): Promise<TippingReserveStatus> {
console.log(this.makeAuthHeader());
const url = new URL("private/reserves", this.baseUrl);
- const resp = await this.http.fetch(url.href, {
+ const resp = await this.httpClient.fetch(url.href, {
method: "GET",
headers: this.makeAuthHeader(),
});
@@ -1704,33 +1430,37 @@ export class MerchantApiClient {
async deleteInstance(instanceId: string) {
const url = new URL(`management/instances/${instanceId}`, this.baseUrl);
- await axios.delete(url.href, {
+ const resp = await this.httpClient.fetch(url.href, {
+ method: "DELETE",
headers: this.makeAuthHeader(),
});
+ await expectSuccessResponseOrThrow(resp);
}
async createInstance(req: MerchantInstanceConfig): Promise<void> {
const url = new URL("management/instances", this.baseUrl);
- await axios.post(url.href, req, {
+ await this.httpClient.fetch(url.href, {
+ method: "POST",
+ body: req,
headers: this.makeAuthHeader(),
});
}
async getInstances(): Promise<MerchantInstancesResponse> {
const url = new URL("management/instances", this.baseUrl);
- const resp = await axios.get(url.href, {
+ const resp = await this.httpClient.fetch(url.href, {
headers: this.makeAuthHeader(),
});
- return resp.data;
+ return resp.json();
}
async getInstanceFullDetails(instanceId: string): Promise<any> {
const url = new URL(`management/instances/${instanceId}`, this.baseUrl);
try {
- const resp = await axios.get(url.href, {
+ const resp = await this.httpClient.fetch(url.href, {
headers: this.makeAuthHeader(),
});
- return resp.data;
+ return resp.json();
} catch (e) {
throw e;
}
@@ -1750,6 +1480,8 @@ export class MerchantApiClient {
/**
* FIXME: This should be deprecated in favor of MerchantApiClient
+ *
+ * @deprecated use MerchantApiClient instead
*/
export namespace MerchantPrivateApi {
export async function createOrder(
@@ -1760,10 +1492,15 @@ export namespace MerchantPrivateApi {
): Promise<MerchantPostOrderResponse> {
const baseUrl = merchantService.makeInstanceBaseUrl(instanceName);
let url = new URL("private/orders", baseUrl);
- const resp = await axios.post(url.href, req, {
+ const resp = await harnessHttpLib.fetch(url.href, {
+ method: "POST",
+ body: req,
headers: withAuthorization as Record<string, string>,
});
- return codecForMerchantPostOrderResponse().decode(resp.data);
+ return readSuccessResponseJsonOrThrow(
+ resp,
+ codecForMerchantPostOrderResponse(),
+ );
}
export async function createTemplate(
@@ -1774,7 +1511,9 @@ export namespace MerchantPrivateApi {
) {
const baseUrl = merchantService.makeInstanceBaseUrl(instanceName);
let url = new URL("private/templates", baseUrl);
- const resp = await axios.post(url.href, req, {
+ const resp = await harnessHttpLib.fetch(url.href, {
+ method: "POST",
+ body: req,
headers: withAuthorization as Record<string, string>,
});
if (resp.status !== 204) {
@@ -1794,10 +1533,13 @@ export namespace MerchantPrivateApi {
if (query.sessionId) {
reqUrl.searchParams.set("session_id", query.sessionId);
}
- const resp = await axios.get(reqUrl.href, {
+ const resp = await harnessHttpLib.fetch(reqUrl.href, {
headers: withAuthorization as Record<string, string>,
});
- return codecForMerchantOrderPrivateStatusResponse().decode(resp.data);
+ return readSuccessResponseJsonOrThrow(
+ resp,
+ codecForMerchantOrderPrivateStatusResponse(),
+ );
}
export async function giveRefund(
@@ -1813,12 +1555,16 @@ export namespace MerchantPrivateApi {
`private/orders/${r.orderId}/refund`,
merchantService.makeInstanceBaseUrl(r.instance),
);
- const resp = await axios.post(reqUrl.href, {
- refund: r.amount,
- reason: r.justification,
+ const resp = await harnessHttpLib.fetch(reqUrl.href, {
+ method: "POST",
+ body: {
+ refund: r.amount,
+ reason: r.justification,
+ },
});
+ const respBody = await resp.json();
return {
- talerRefundUri: resp.data.taler_refund_uri,
+ talerRefundUri: respBody.taler_refund_uri,
};
}
@@ -1830,9 +1576,9 @@ export namespace MerchantPrivateApi {
`private/reserves`,
merchantService.makeInstanceBaseUrl(instance),
);
- const resp = await axios.get(reqUrl.href);
+ const resp = await harnessHttpLib.fetch(reqUrl.href);
// FIXME: validate
- return resp.data;
+ return resp.json();
}
export async function giveTip(
@@ -1844,9 +1590,12 @@ export namespace MerchantPrivateApi {
`private/tips`,
merchantService.makeInstanceBaseUrl(instance),
);
- const resp = await axios.post(reqUrl.href, req);
+ const resp = await harnessHttpLib.fetch(reqUrl.href, {
+ method: "POST",
+ body: req,
+ });
// FIXME: validate
- return resp.data;
+ return resp.json();
}
}
@@ -2052,7 +1801,12 @@ export class MerchantService implements MerchantServiceInterface {
instanceConfig.defaultPayDelay ??
Duration.toTalerProtocolDuration(Duration.getForever()),
};
- await axios.post(url, body);
+ const httpLib = createPlatformHttpLib({
+ allowHttp: true,
+ enableThrottling: false,
+ });
+ const resp = await httpLib.fetch(url, { method: "POST", body });
+ await expectSuccessResponseOrThrow(resp);
}
makeInstanceBaseUrl(instanceName?: string): string {
diff --git a/packages/taler-harness/src/harness/helpers.ts b/packages/taler-harness/src/harness/helpers.ts
index d41ffdd00..d1d0ea104 100644
--- a/packages/taler-harness/src/harness/helpers.ts
+++ b/packages/taler-harness/src/harness/helpers.ts
@@ -215,10 +215,14 @@ export async function createSimpleTestkudosEnvironment(
};
}
+export function getSharedTestDir(): string {
+ return `/tmp/taler-harness@${process.env.USER}`;
+}
+
export async function useSharedTestkudosEnvironment(t: GlobalTestState) {
const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS"));
- const sharedDir = `/tmp/taler-harness@${process.env.USER}`;
+ const sharedDir = getSharedTestDir();
fs.mkdirSync(sharedDir, { recursive: true });
@@ -230,10 +234,9 @@ export async function useSharedTestkudosEnvironment(t: GlobalTestState) {
logger.info(`previous setup done: ${prevSetupDone}`);
-
// Wallet has longer startup-time and no dependencies,
// so we start it rather early.
- const walletStartProm = createWalletDaemonWithClient(t, { name: "wallet" })
+ const walletStartProm = createWalletDaemonWithClient(t, { name: "wallet" });
if (fs.existsSync(sharedDir + "/bank.conf")) {
logger.info("reusing existing bank");
@@ -361,7 +364,7 @@ export async function useSharedTestkudosEnvironment(t: GlobalTestState) {
}
};
- await bankStart()
+ await bankStart();
const res = await Promise.all([
exchangeStart(),
@@ -803,7 +806,7 @@ export async function applyTimeTravel(
/**
* Make a simple payment and check that it succeeded.
- *
+ *
* @deprecated
*/
export async function makeTestPayment(
diff --git a/packages/taler-harness/src/harness/libeufin-apis.ts b/packages/taler-harness/src/harness/libeufin-apis.ts
index cb9acdaa4..3c57eee07 100644
--- a/packages/taler-harness/src/harness/libeufin-apis.ts
+++ b/packages/taler-harness/src/harness/libeufin-apis.ts
@@ -6,8 +6,21 @@
*/
import { URL } from "@gnu-taler/taler-util";
-import axiosImp from "axios";
-const axios = axiosImp.default;
+import {
+ createPlatformHttpLib,
+ makeBasicAuthHeader,
+} from "@gnu-taler/taler-util/http";
+import {
+ LibeufinNexusTransactions,
+ LibeufinSandboxAdminBankAccountBalance,
+ NexusBankConnections,
+ NexusFacadeListResponse,
+ NexusGetPermissionsResponse,
+ NexusNewTransactionsInfo,
+ NexusTask,
+ NexusTaskCollection,
+ NexusUserResponse,
+} from "./libeufin.js";
export interface LibeufinSandboxServiceInterface {
baseUrl: string;
@@ -163,30 +176,13 @@ export interface LibeufinSandboxAddIncomingRequest {
direction: string;
}
+const libeufinHttpLib = createPlatformHttpLib();
+
/**
* APIs spread across Legacy and Access, it is therefore
* the "base URL" relative to which API every call addresses.
*/
export namespace LibeufinSandboxApi {
- // Need Access API base URL.
- export async function demobankAccountInfo(
- username: string,
- password: string,
- libeufinSandboxService: LibeufinSandboxServiceInterface,
- accountLabel: string,
- ) {
- let url = new URL(
- `accounts/${accountLabel}`,
- libeufinSandboxService.baseUrl,
- );
- return await axios.get(url.href, {
- auth: {
- username: username,
- password: password,
- },
- });
- }
-
// Creates one bank account via the Access API.
// Need the /demobanks/$id/access-api as the base URL
export async function createDemobankAccount(
@@ -194,12 +190,15 @@ export namespace LibeufinSandboxApi {
password: string,
libeufinSandboxService: LibeufinSandboxServiceInterface,
iban: string | null = null,
- ) {
+ ): Promise<void> {
let url = new URL("testing/register", libeufinSandboxService.baseUrl);
- await axios.post(url.href, {
- username: username,
- password: password,
- iban: iban,
+ await libeufinHttpLib.fetch(url.href, {
+ method: "POST",
+ body: {
+ username: username,
+ password: password,
+ iban: iban,
+ },
});
}
// Need /demobanks/$id as the base URL
@@ -209,75 +208,57 @@ export namespace LibeufinSandboxApi {
libeufinSandboxService: LibeufinSandboxServiceInterface,
username: string = "admin",
password: string = "secret",
- ) {
+ ): Promise<void> {
// baseUrl should already be pointed to one demobank.
let url = new URL("ebics/subscribers", libeufinSandboxService.baseUrl);
- await axios.post(
- url.href,
- {
+ await libeufinHttpLib.fetch(url.href, {
+ method: "POST",
+ body: {
userID: req.userID,
hostID: req.hostID,
partnerID: req.partnerID,
demobankAccountLabel: demobankAccountLabel,
},
- {
- auth: {
- username: "admin",
- password: "secret",
- },
- },
- );
+ });
}
export async function rotateKeys(
libeufinSandboxService: LibeufinSandboxServiceInterface,
hostID: string,
- ) {
+ ): Promise<void> {
const baseUrl = libeufinSandboxService.baseUrl;
let url = new URL(`admin/ebics/hosts/${hostID}/rotate-keys`, baseUrl);
- await axios.post(
- url.href,
- {},
- {
- auth: {
- username: "admin",
- password: "secret",
- },
- },
- );
+ await libeufinHttpLib.fetch(url.href, {
+ method: "POST",
+ body: {},
+ });
}
export async function createEbicsHost(
libeufinSandboxService: LibeufinSandboxServiceInterface,
hostID: string,
- ) {
+ ): Promise<void> {
const baseUrl = libeufinSandboxService.baseUrl;
let url = new URL("admin/ebics/hosts", baseUrl);
- await axios.post(
- url.href,
- {
+ await libeufinHttpLib.fetch(url.href, {
+ method: "POST",
+ body: {
hostID,
ebicsVersion: "2.5",
},
- {
- auth: {
- username: "admin",
- password: "secret",
- },
- },
- );
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
+ });
}
export async function createBankAccount(
libeufinSandboxService: LibeufinSandboxServiceInterface,
req: BankAccountInfo,
- ) {
+ ): Promise<void> {
const baseUrl = libeufinSandboxService.baseUrl;
let url = new URL(`admin/bank-accounts/${req.label}`, baseUrl);
- await axios.post(url.href, req, {
- auth: {
- username: "admin",
- password: "secret",
- },
+ await libeufinHttpLib.fetch(url.href, {
+ method: "POST",
+ body: req,
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
});
}
@@ -288,14 +269,13 @@ export namespace LibeufinSandboxApi {
export async function createEbicsSubscriber(
libeufinSandboxService: LibeufinSandboxServiceInterface,
req: CreateEbicsSubscriberRequest,
- ) {
+ ): Promise<void> {
const baseUrl = libeufinSandboxService.baseUrl;
let url = new URL("admin/ebics/subscribers", baseUrl);
- await axios.post(url.href, req, {
- auth: {
- username: "admin",
- password: "secret",
- },
+ await libeufinHttpLib.fetch(url.href, {
+ method: "POST",
+ body: req,
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
});
}
@@ -306,14 +286,13 @@ export namespace LibeufinSandboxApi {
export async function createEbicsBankAccount(
libeufinSandboxService: LibeufinSandboxServiceInterface,
req: CreateEbicsBankAccountRequest,
- ) {
+ ): Promise<void> {
const baseUrl = libeufinSandboxService.baseUrl;
let url = new URL("admin/ebics/bank-accounts", baseUrl);
- await axios.post(url.href, req, {
- auth: {
- username: "admin",
- password: "secret",
- },
+ await libeufinHttpLib.fetch(url.href, {
+ method: "POST",
+ body: req,
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
});
}
@@ -321,17 +300,16 @@ export namespace LibeufinSandboxApi {
libeufinSandboxService: LibeufinSandboxServiceInterface,
accountLabel: string,
req: SimulateIncomingTransactionRequest,
- ) {
+ ): Promise<void> {
const baseUrl = libeufinSandboxService.baseUrl;
let url = new URL(
`admin/bank-accounts/${accountLabel}/simulate-incoming-transaction`,
baseUrl,
);
- await axios.post(url.href, req, {
- auth: {
- username: "admin",
- password: "secret",
- },
+ await libeufinHttpLib.fetch(url.href, {
+ method: "POST",
+ body: req,
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
});
}
@@ -344,13 +322,10 @@ export namespace LibeufinSandboxApi {
`admin/bank-accounts/${accountLabel}/transactions`,
baseUrl,
);
- const res = await axios.get(url.href, {
- auth: {
- username: "admin",
- password: "secret",
- },
+ const res = await libeufinHttpLib.fetch(url.href, {
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
});
- return res.data as SandboxAccountTransactions;
+ return (await res.json()) as SandboxAccountTransactions;
}
export async function getCamt053(
@@ -359,61 +334,50 @@ export namespace LibeufinSandboxApi {
): Promise<any> {
const baseUrl = libeufinSandboxService.baseUrl;
let url = new URL("admin/payments/camt", baseUrl);
- return await axios.post(
- url.href,
- {
+ return await libeufinHttpLib.fetch(url.href, {
+ method: "POST",
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
+ body: {
bankaccount: accountLabel,
type: 53,
},
- {
- auth: {
- username: "admin",
- password: "secret",
- },
- },
- );
+ });
}
export async function getAccountInfoWithBalance(
libeufinSandboxService: LibeufinSandboxServiceInterface,
accountLabel: string,
- ): Promise<any> {
+ ): Promise<LibeufinSandboxAdminBankAccountBalance> {
const baseUrl = libeufinSandboxService.baseUrl;
let url = new URL(`admin/bank-accounts/${accountLabel}`, baseUrl);
- return await axios.get(url.href, {
- auth: {
- username: "admin",
- password: "secret",
- },
+ const res = await libeufinHttpLib.fetch(url.href, {
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
});
+ return res.json();
}
}
export namespace LibeufinNexusApi {
export async function getAllConnections(
nexus: LibeufinNexusServiceInterface,
- ): Promise<any> {
+ ): Promise<NexusBankConnections> {
let url = new URL("bank-connections", nexus.baseUrl);
- const res = await axios.get(url.href, {
- auth: {
- username: "admin",
- password: "test",
- },
+ const res = await libeufinHttpLib.fetch(url.href, {
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
});
- return res;
+ return res.json();
}
export async function deleteBankConnection(
libeufinNexusService: LibeufinNexusServiceInterface,
req: DeleteBankConnectionRequest,
- ): Promise<any> {
+ ): Promise<void> {
const baseUrl = libeufinNexusService.baseUrl;
let url = new URL("bank-connections/delete-connection", baseUrl);
- return await axios.post(url.href, req, {
- auth: {
- username: "admin",
- password: "test",
- },
+ await libeufinHttpLib.fetch(url.href, {
+ method: "POST",
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
+ body: req,
});
}
@@ -423,9 +387,10 @@ export namespace LibeufinNexusApi {
): Promise<void> {
const baseUrl = libeufinNexusService.baseUrl;
let url = new URL("bank-connections", baseUrl);
- await axios.post(
- url.href,
- {
+ await libeufinHttpLib.fetch(url.href, {
+ method: "POST",
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
+ body: {
source: "new",
type: "ebics",
name: req.name,
@@ -437,13 +402,7 @@ export namespace LibeufinNexusApi {
systemID: req.systemID,
},
},
- {
- auth: {
- username: "admin",
- password: "test",
- },
- },
- );
+ });
}
export async function getBankAccount(
@@ -452,12 +411,10 @@ export namespace LibeufinNexusApi {
): Promise<any> {
const baseUrl = libeufinNexusService.baseUrl;
let url = new URL(`bank-accounts/${accountName}`, baseUrl);
- return await axios.get(url.href, {
- auth: {
- username: "admin",
- password: "test",
- },
+ const resp = await libeufinHttpLib.fetch(url.href, {
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
});
+ return resp.json();
}
export async function submitInitiatedPayment(
@@ -470,16 +427,11 @@ export namespace LibeufinNexusApi {
`bank-accounts/${accountName}/payment-initiations/${paymentId}/submit`,
baseUrl,
);
- await axios.post(
- url.href,
- {},
- {
- auth: {
- username: "admin",
- password: "test",
- },
- },
- );
+ await libeufinHttpLib.fetch(url.href, {
+ method: "POST",
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
+ body: {},
+ });
}
export async function fetchAccounts(
@@ -491,16 +443,11 @@ export namespace LibeufinNexusApi {
`bank-connections/${connectionName}/fetch-accounts`,
baseUrl,
);
- await axios.post(
- url.href,
- {},
- {
- auth: {
- username: "admin",
- password: "test",
- },
- },
- );
+ await libeufinHttpLib.fetch(url.href, {
+ method: "POST",
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
+ body: {},
+ });
}
export async function importConnectionAccount(
@@ -514,37 +461,27 @@ export namespace LibeufinNexusApi {
`bank-connections/${connectionName}/import-account`,
baseUrl,
);
- await axios.post(
- url.href,
- {
+ await libeufinHttpLib.fetch(url.href, {
+ method: "POST",
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
+ body: {
offeredAccountId,
nexusBankAccountId,
},
- {
- auth: {
- username: "admin",
- password: "test",
- },
- },
- );
+ });
}
export async function connectBankConnection(
libeufinNexusService: LibeufinNexusServiceInterface,
connectionName: string,
- ) {
+ ): Promise<void> {
const baseUrl = libeufinNexusService.baseUrl;
let url = new URL(`bank-connections/${connectionName}/connect`, baseUrl);
- await axios.post(
- url.href,
- {},
- {
- auth: {
- username: "admin",
- password: "test",
- },
- },
- );
+ await libeufinHttpLib.fetch(url.href, {
+ method: "POST",
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
+ body: {},
+ });
}
export async function getPaymentInitiations(
@@ -558,43 +495,33 @@ export namespace LibeufinNexusApi {
`/bank-accounts/${accountName}/payment-initiations`,
baseUrl,
);
- let response = await axios.get(url.href, {
- auth: {
- username: username,
- password: password,
- },
+ let response = await libeufinHttpLib.fetch(url.href, {
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
});
+ const respJson = await response.json();
console.log(
`Payment initiations of: ${accountName}`,
- JSON.stringify(response.data, null, 2),
+ JSON.stringify(respJson, null, 2),
);
}
- export async function getConfig(
- libeufinNexusService: LibeufinNexusServiceInterface,
- ): Promise<void> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(`/config`, baseUrl);
- let response = await axios.get(url.href);
- }
-
// Uses the Anastasis API to get a list of transactions.
export async function getAnastasisTransactions(
libeufinNexusService: LibeufinNexusServiceInterface,
anastasisBaseUrl: string,
+ // FIXME: Nail down type!
params: {}, // of the request: {delta: 5, ..}
username: string = "admin",
password: string = "test",
): Promise<any> {
let url = new URL("history/incoming", anastasisBaseUrl);
- let response = await axios.get(url.href, {
- params: params,
- auth: {
- username: username,
- password: password,
- },
+ for (const [k, v] of Object.entries(params)) {
+ url.searchParams.set(k, String(v));
+ }
+ let response = await libeufinHttpLib.fetch(url.href, {
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
});
- return response;
+ return response.json();
}
// FIXME: this function should return some structured
@@ -604,16 +531,13 @@ export namespace LibeufinNexusApi {
accountName: string,
username: string = "admin",
password: string = "test",
- ): Promise<any> {
+ ): Promise<LibeufinNexusTransactions> {
const baseUrl = libeufinNexusService.baseUrl;
let url = new URL(`/bank-accounts/${accountName}/transactions`, baseUrl);
- let response = await axios.get(url.href, {
- auth: {
- username: username,
- password: password,
- },
+ let response = await libeufinHttpLib.fetch(url.href, {
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
});
- return response;
+ return response.json();
}
export async function fetchTransactions(
@@ -623,25 +547,21 @@ export namespace LibeufinNexusApi {
level: string = "report",
username: string = "admin",
password: string = "test",
- ): Promise<any> {
+ ): Promise<NexusNewTransactionsInfo> {
const baseUrl = libeufinNexusService.baseUrl;
let url = new URL(
`/bank-accounts/${accountName}/fetch-transactions`,
baseUrl,
);
- return await axios.post(
- url.href,
- {
+ const resp = await libeufinHttpLib.fetch(url.href, {
+ method: "POST",
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
+ body: {
rangeType: rangeType,
level: level,
},
- {
- auth: {
- username: username,
- password: password,
- },
- },
- );
+ });
+ return resp.json();
}
export async function changePassword(
@@ -649,97 +569,109 @@ export namespace LibeufinNexusApi {
username: string,
req: UpdateNexusUserRequest,
auth: NexusAuth,
- ) {
+ ): Promise<void> {
const baseUrl = libeufinNexusService.baseUrl;
let url = new URL(`/users/${username}/password`, baseUrl);
- await axios.post(url.href, req, auth);
+ await libeufinHttpLib.fetch(url.href, {
+ method: "POST",
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
+ body: req,
+ });
}
export async function getUser(
libeufinNexusService: LibeufinNexusServiceInterface,
auth: NexusAuth,
- ): Promise<any> {
+ ): Promise<NexusUserResponse> {
const baseUrl = libeufinNexusService.baseUrl;
let url = new URL(`/user`, baseUrl);
- return await axios.get(url.href, auth);
+ const resp = await libeufinHttpLib.fetch(url.href, {
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
+ });
+ return resp.json();
}
export async function createUser(
libeufinNexusService: LibeufinNexusServiceInterface,
req: CreateNexusUserRequest,
- ) {
+ ): Promise<void> {
const baseUrl = libeufinNexusService.baseUrl;
let url = new URL(`/users`, baseUrl);
- await axios.post(url.href, req, {
- auth: {
- username: "admin",
- password: "test",
- },
+ await libeufinHttpLib.fetch(url.href, {
+ method: "POST",
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
+ body: req,
});
}
export async function getAllPermissions(
libeufinNexusService: LibeufinNexusServiceInterface,
- ): Promise<any> {
+ ): Promise<NexusGetPermissionsResponse> {
const baseUrl = libeufinNexusService.baseUrl;
let url = new URL(`/permissions`, baseUrl);
- return await axios.get(url.href, {
- auth: {
- username: "admin",
- password: "test",
- },
+ const resp = await libeufinHttpLib.fetch(url.href, {
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
});
+ return resp.json();
}
export async function postPermission(
libeufinNexusService: LibeufinNexusServiceInterface,
req: PostNexusPermissionRequest,
- ) {
+ ): Promise<void> {
const baseUrl = libeufinNexusService.baseUrl;
let url = new URL(`/permissions`, baseUrl);
- await axios.post(url.href, req, {
- auth: {
- username: "admin",
- password: "test",
- },
+ await libeufinHttpLib.fetch(url.href, {
+ method: "POST",
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
+ body: req,
+ });
+ }
+
+ export async function getAllTasks(
+ libeufinNexusService: LibeufinNexusServiceInterface,
+ bankAccountName: string,
+ ): Promise<NexusTaskCollection> {
+ const baseUrl = libeufinNexusService.baseUrl;
+ let url = new URL(`/bank-accounts/${bankAccountName}/schedule`, baseUrl);
+ const resp = await libeufinHttpLib.fetch(url.href, {
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
});
+ return resp.json();
}
- export async function getTasks(
+ export async function getTask(
libeufinNexusService: LibeufinNexusServiceInterface,
bankAccountName: string,
// When void, the request returns the list of all the
// tasks under this bank account.
- taskName: string | void,
- ): Promise<any> {
+ taskName: string,
+ ): Promise<NexusTask> {
const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(`/bank-accounts/${bankAccountName}/schedule`, baseUrl);
+ let url = new URL(
+ `/bank-accounts/${bankAccountName}/schedule/${taskName}`,
+ baseUrl,
+ );
if (taskName) url = new URL(taskName, `${url.href}/`);
-
- // It's caller's responsibility to interpret the response.
- return await axios.get(url.href, {
- auth: {
- username: "admin",
- password: "test",
- },
+ const resp = await libeufinHttpLib.fetch(url.href, {
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
});
+ return resp.json();
}
export async function deleteTask(
libeufinNexusService: LibeufinNexusServiceInterface,
bankAccountName: string,
taskName: string,
- ) {
+ ): Promise<void> {
const baseUrl = libeufinNexusService.baseUrl;
let url = new URL(
`/bank-accounts/${bankAccountName}/schedule/${taskName}`,
baseUrl,
);
- await axios.delete(url.href, {
- auth: {
- username: "admin",
- password: "test",
- },
+ await libeufinHttpLib.fetch(url.href, {
+ method: "DELETE",
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
});
}
@@ -747,53 +679,50 @@ export namespace LibeufinNexusApi {
libeufinNexusService: LibeufinNexusServiceInterface,
bankAccountName: string,
req: PostNexusTaskRequest,
- ): Promise<any> {
+ ): Promise<void> {
const baseUrl = libeufinNexusService.baseUrl;
let url = new URL(`/bank-accounts/${bankAccountName}/schedule`, baseUrl);
- return await axios.post(url.href, req, {
- auth: {
- username: "admin",
- password: "test",
- },
+ await libeufinHttpLib.fetch(url.href, {
+ method: "POST",
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
+ body: req,
});
}
export async function deleteFacade(
libeufinNexusService: LibeufinNexusServiceInterface,
facadeName: string,
- ): Promise<any> {
+ ): Promise<void> {
const baseUrl = libeufinNexusService.baseUrl;
let url = new URL(`facades/${facadeName}`, baseUrl);
- return await axios.delete(url.href, {
- auth: {
- username: "admin",
- password: "test",
- },
+ await libeufinHttpLib.fetch(url.href, {
+ method: "DELETE",
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
});
}
export async function getAllFacades(
libeufinNexusService: LibeufinNexusServiceInterface,
- ): Promise<any> {
+ ): Promise<NexusFacadeListResponse> {
const baseUrl = libeufinNexusService.baseUrl;
let url = new URL("facades", baseUrl);
- return await axios.get(url.href, {
- auth: {
- username: "admin",
- password: "test",
- },
+ const resp = await libeufinHttpLib.fetch(url.href, {
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
});
+ // FIXME: Just return validated, typed response here!
+ return resp.json();
}
export async function createAnastasisFacade(
libeufinNexusService: LibeufinNexusServiceInterface,
req: CreateAnastasisFacadeRequest,
- ) {
+ ): Promise<void> {
const baseUrl = libeufinNexusService.baseUrl;
let url = new URL("facades", baseUrl);
- await axios.post(
- url.href,
- {
+ await libeufinHttpLib.fetch(url.href, {
+ method: "POST",
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
+ body: {
name: req.name,
type: "anastasis",
config: {
@@ -803,24 +732,19 @@ export namespace LibeufinNexusApi {
reserveTransferLevel: req.reserveTransferLevel,
},
},
- {
- auth: {
- username: "admin",
- password: "test",
- },
- },
- );
+ });
}
export async function createTwgFacade(
libeufinNexusService: LibeufinNexusServiceInterface,
req: CreateTalerWireGatewayFacadeRequest,
- ) {
+ ): Promise<void> {
const baseUrl = libeufinNexusService.baseUrl;
let url = new URL("facades", baseUrl);
- await axios.post(
- url.href,
- {
+ await libeufinHttpLib.fetch(url.href, {
+ method: "POST",
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
+ body: {
name: req.name,
type: "taler-wire-gateway",
config: {
@@ -830,33 +754,22 @@ export namespace LibeufinNexusApi {
reserveTransferLevel: req.reserveTransferLevel,
},
},
- {
- auth: {
- username: "admin",
- password: "test",
- },
- },
- );
+ });
}
export async function submitAllPaymentInitiations(
libeufinNexusService: LibeufinNexusServiceInterface,
accountId: string,
- ) {
+ ): Promise<void> {
const baseUrl = libeufinNexusService.baseUrl;
let url = new URL(
`/bank-accounts/${accountId}/submit-all-payment-initiations`,
baseUrl,
);
- await axios.post(
- url.href,
- {},
- {
- auth: {
- username: "admin",
- password: "test",
- },
- },
- );
+ await libeufinHttpLib.fetch(url.href, {
+ method: "POST",
+ headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
+ body: {},
+ });
}
}
diff --git a/packages/taler-harness/src/harness/libeufin.ts b/packages/taler-harness/src/harness/libeufin.ts
index 8fd276fad..9f3e7a5a0 100644
--- a/packages/taler-harness/src/harness/libeufin.ts
+++ b/packages/taler-harness/src/harness/libeufin.ts
@@ -26,39 +26,32 @@
/**
* Imports.
*/
-import axios from "axios";
-import { URL, Logger } from "@gnu-taler/taler-util";
+import { AmountString, Logger } from "@gnu-taler/taler-util";
import {
- GlobalTestState,
DbInfo,
- pingProc,
+ GlobalTestState,
ProcessWrapper,
+ getRandomIban,
+ pingProc,
runCommand,
setupDb,
sh,
- getRandomIban,
} from "../harness/harness.js";
import {
- LibeufinSandboxApi,
- LibeufinNexusApi,
+ CreateAnastasisFacadeRequest,
CreateEbicsBankAccountRequest,
- LibeufinSandboxServiceInterface,
- CreateTalerWireGatewayFacadeRequest,
- SimulateIncomingTransactionRequest,
- SandboxAccountTransactions,
- DeleteBankConnectionRequest,
CreateEbicsBankConnectionRequest,
- UpdateNexusUserRequest,
- NexusAuth,
- CreateAnastasisFacadeRequest,
- PostNexusTaskRequest,
- PostNexusPermissionRequest,
CreateNexusUserRequest,
+ CreateTalerWireGatewayFacadeRequest,
+ LibeufinNexusApi,
+ LibeufinSandboxApi,
+ LibeufinSandboxServiceInterface,
+ PostNexusPermissionRequest,
} from "../harness/libeufin-apis.js";
const logger = new Logger("libeufin.ts");
-export { LibeufinSandboxApi, LibeufinNexusApi };
+export { LibeufinNexusApi, LibeufinSandboxApi };
export interface LibeufinServices {
libeufinSandbox: LibeufinSandboxService;
@@ -76,7 +69,7 @@ export interface LibeufinNexusConfig {
databaseJdbcUri: string;
}
-interface LibeufinNexusMoneyMovement {
+export interface LibeufinNexusMoneyMovement {
amount: string;
creditDebitIndicator: string;
details: {
@@ -103,11 +96,11 @@ interface LibeufinNexusMoneyMovement {
};
}
-interface LibeufinNexusBatches {
+export interface LibeufinNexusBatches {
batchTransactions: Array<LibeufinNexusMoneyMovement>;
}
-interface LibeufinNexusTransaction {
+export interface LibeufinNexusTransaction {
amount: string;
creditDebitIndicator: string;
status: string;
@@ -118,7 +111,7 @@ interface LibeufinNexusTransaction {
batches: Array<LibeufinNexusBatches>;
}
-interface LibeufinNexusTransactions {
+export interface LibeufinNexusTransactions {
transactions: Array<LibeufinNexusTransaction>;
}
@@ -182,6 +175,146 @@ export interface LibeufinPreparedPaymentDetails {
nexusBankAccountName: string;
}
+export interface NexusBankConnection {
+ // connection type. For example "ebics".
+ type: string;
+
+ // connection name as given by the user at
+ // the moment of creation.
+ name: string;
+}
+
+export interface NexusBankConnections {
+ bankConnections: NexusBankConnection[];
+}
+
+export interface FacadeShowInfo {
+ // Name of the facade, same as the "fcid" parameter.
+ name: string;
+
+ // Type of the facade.
+ // For example, "taler-wire-gateway".
+ type: string;
+
+ // Bas URL of the facade.
+ baseUrl: string;
+
+ // details depending on the facade type.
+ config: any;
+}
+
+export interface FetchParams {
+ // Because transactions are delivered by banks in "batches",
+ // then every batch can have different qualities. This value
+ // lets the request specify which type of batch ought to be
+ // returned. Currently, the following two type are supported:
+ //
+ // 'report': typically includes only non booked transactions.
+ // 'statement': typically includes only booked transactions.
+ level: "report" | "statement" | "all";
+
+ // This type indicates the time range of the query.
+ // It allows the following values:
+ //
+ // 'latest': retrieves the last transactions from the bank.
+ // If there are older unread transactions, those will *not*
+ // be downloaded.
+ //
+ // 'all': retrieves all the transactions from the bank,
+ // until the oldest.
+ //
+ // 'previous-days': currently *not* implemented, it will allow
+ // the request to download transactions from
+ // today until N days before.
+ //
+ // 'since-last': retrieves all the transactions since the last
+ // time one was downloaded.
+ //
+ rangeType: "latest" | "all" | "previous-days" | "since-last";
+}
+
+export interface NexusTask {
+ // The resource being impacted by this operation.
+ // Typically a (Nexus) bank account being fetched
+ // or whose payments are submitted. In this cases,
+ // this value is the "bank-account" constant.
+ resourceType: string;
+ // Name of the resource. In case of "bank-account", that
+ // is the name under which the bank account was imported
+ // from the bank.
+ resourceId: string;
+ // Task name, equals 'taskId'
+ taskName: string;
+ // Values allowed are "fetch" or "submit".
+ taskType: string;
+ // FIXME: describe.
+ taskCronSpec: string;
+ // Only meaningful for "fetch" types.
+ taskParams: FetchParams;
+ // Timestamp in secons when the next iteration will run.
+ nextScheduledExecutionSec: number;
+ // Timestamp in seconds when the previous iteration ran.
+ prevScheduledExecutionSec: number;
+}
+
+export interface NexusNewTransactionsInfo {
+ // How many transactions are new to Nexus.
+ newTransactions: number;
+ // How many transactions got downloaded by the request.
+ // Note that a transaction can be downloaded multiple
+ // times but only counts as new once.
+ downloadedTransactions: number;
+}
+
+
+export interface NexusUserResponse {
+ // User name
+ username: string;
+
+ // Is this a super user?
+ superuser: boolean;
+}
+
+export interface NexusTaskShortInfo {
+ cronspec: string;
+ type: "fetch" | "submit";
+ params: FetchParams;
+}
+
+export interface NexusTaskCollection {
+ // This field can contain *multiple* objects of the type sampled below.
+ schedule: {
+ [taskName: string]: NexusTaskShortInfo;
+ };
+}
+
+export interface NexusFacadeListResponse {
+ facades: FacadeShowInfo[];
+}
+
+export interface LibeufinSandboxAdminBankAccountBalance {
+ // Balance in the $currency:$amount format.
+ balance: AmountString;
+ // IBAN of the bank account identified by $accountLabel
+ iban: string;
+ // BIC of the bank account identified by $accountLabel
+ bic: string;
+ // Mentions $accountLabel
+ label: string;
+}
+
+export interface LibeufinPermission {
+ subjectType: string;
+ subjectId: string;
+ resourceType: string;
+ resourceId: string;
+ permissionName: string;
+}
+
+export interface NexusGetPermissionsResponse {
+ permissions: LibeufinPermission[];
+}
+
export class LibeufinSandboxService implements LibeufinSandboxServiceInterface {
static async create(
gc: GlobalTestState,
diff --git a/packages/taler-harness/src/index.ts b/packages/taler-harness/src/index.ts
index 0efaea9ad..cd688ed89 100644
--- a/packages/taler-harness/src/index.ts
+++ b/packages/taler-harness/src/index.ts
@@ -191,12 +191,12 @@ configCli
const config = Configuration.load();
let res;
if (args.get.file) {
- res = config.getString(args.get.section, args.get.option);
- } else {
res = config.getPath(args.get.section, args.get.option);
+ } else {
+ res = config.getString(args.get.section, args.get.option);
}
if (res.isDefined()) {
- console.log(res.getValue());
+ console.log(res.required());
} else {
console.warn("not found");
process.exit(1);
diff --git a/packages/taler-harness/src/integrationtests/test-exchange-purse.ts b/packages/taler-harness/src/integrationtests/test-exchange-purse.ts
new file mode 100644
index 000000000..5a1d02692
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-exchange-purse.ts
@@ -0,0 +1,224 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 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/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AbsoluteTime,
+ ContractTermsUtil,
+ decodeCrock,
+ Duration,
+ encodeCrock,
+ getRandomBytes,
+ hash,
+ j2s,
+ PeerContractTerms,
+ TalerError,
+ TalerPreciseTimestamp,
+} from "@gnu-taler/taler-util";
+import {
+ checkReserve,
+ CryptoDispatcher,
+ downloadExchangeInfo,
+ EncryptContractRequest,
+ findDenomOrThrow,
+ SpendCoinDetails,
+ SynchronousCryptoWorkerFactoryPlain,
+ topupReserveWithDemobank,
+ Wallet,
+ withdrawCoin,
+} from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState, harnessHttpLib } from "../harness/harness.js";
+import { createSimpleTestkudosEnvironmentV2 } from "../harness/helpers.js";
+
+/**
+ * Test the exchange's purse API.
+ */
+export async function runExchangePurseTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { bank, exchange } = await createSimpleTestkudosEnvironmentV2(t);
+
+ const http = harnessHttpLib;
+ const cryptoDisp = new CryptoDispatcher(
+ new SynchronousCryptoWorkerFactoryPlain(),
+ );
+ const cryptoApi = cryptoDisp.cryptoApi;
+
+ try {
+ // Withdraw digital cash into the wallet.
+
+ const exchangeInfo = await downloadExchangeInfo(exchange.baseUrl, http);
+
+ const reserveKeyPair = await cryptoApi.createEddsaKeypair({});
+
+ let reserveUrl = new URL(
+ `reserves/${reserveKeyPair.pub}`,
+ exchange.baseUrl,
+ );
+ reserveUrl.searchParams.set("timeout_ms", "30000");
+ const longpollReq = http.fetch(reserveUrl.href, {
+ method: "GET",
+ });
+
+ await topupReserveWithDemobank({
+ amount: "TESTKUDOS:10",
+ http,
+ reservePub: reserveKeyPair.pub,
+ bankAccessApiBaseUrl: bank.bankAccessApiBaseUrl,
+ exchangeInfo,
+ });
+
+ console.log("waiting for longpoll request");
+ const resp = await longpollReq;
+ console.log(`got response, status ${resp.status}`);
+
+ console.log(exchangeInfo);
+
+ await checkReserve(http, exchange.baseUrl, reserveKeyPair.pub);
+
+ const d1 = findDenomOrThrow(exchangeInfo, "TESTKUDOS:8", {
+ denomselAllowLate: Wallet.defaultConfig.testing.denomselAllowLate,
+ });
+
+ const coin = await withdrawCoin({
+ http,
+ cryptoApi,
+ reserveKeyPair: {
+ reservePriv: reserveKeyPair.priv,
+ reservePub: reserveKeyPair.pub,
+ },
+ denom: d1,
+ exchangeBaseUrl: exchange.baseUrl,
+ });
+
+ const amount = "TESTKUDOS:5";
+ const purseFee = "TESTKUDOS:0";
+
+ const mergeTimestamp = TalerPreciseTimestamp.now();
+
+ const contractTerms: PeerContractTerms = {
+ amount,
+ summary: "Hello",
+ purse_expiration: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ ),
+ };
+
+ const mergeReservePair = await cryptoApi.createEddsaKeypair({});
+ const pursePair = await cryptoApi.createEddsaKeypair({});
+ const mergePair = await cryptoApi.createEddsaKeypair({});
+ const contractPair = await cryptoApi.createEddsaKeypair({});
+ const contractEncNonce = encodeCrock(getRandomBytes(24));
+
+ const pursePub = pursePair.pub;
+
+ const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms);
+
+ const purseSigResp = await cryptoApi.signPurseCreation({
+ hContractTerms,
+ mergePub: mergePair.pub,
+ minAge: 0,
+ purseAmount: amount,
+ purseExpiration: contractTerms.purse_expiration,
+ pursePriv: pursePair.priv,
+ });
+
+ const coinSpend: SpendCoinDetails = {
+ ageCommitmentProof: undefined,
+ coinPriv: coin.coinPriv,
+ coinPub: coin.coinPub,
+ contribution: amount,
+ denomPubHash: coin.denomPubHash,
+ denomSig: coin.denomSig,
+ };
+
+ const depositSigsResp = await cryptoApi.signPurseDeposits({
+ exchangeBaseUrl: exchange.baseUrl,
+ pursePub: pursePair.pub,
+ coins: [coinSpend],
+ });
+
+ const encryptContractRequest: EncryptContractRequest = {
+ contractTerms: contractTerms,
+ mergePriv: mergePair.priv,
+ pursePriv: pursePair.priv,
+ pursePub: pursePair.pub,
+ contractPriv: contractPair.priv,
+ contractPub: contractPair.pub,
+ nonce: contractEncNonce,
+ };
+
+ const econtractResp = await cryptoApi.encryptContractForMerge(
+ encryptContractRequest,
+ );
+
+ const econtractHash = encodeCrock(
+ hash(decodeCrock(econtractResp.econtract.econtract)),
+ );
+
+ const createPurseUrl = new URL(
+ `purses/${pursePair.pub}/create`,
+ exchange.baseUrl,
+ );
+
+ const reqBody = {
+ amount: amount,
+ merge_pub: mergePair.pub,
+ purse_sig: purseSigResp.sig,
+ h_contract_terms: hContractTerms,
+ purse_expiration: contractTerms.purse_expiration,
+ deposits: depositSigsResp.deposits,
+ min_age: 0,
+ econtract: econtractResp.econtract,
+ };
+
+ const httpResp = await http.fetch(createPurseUrl.href, {
+ method: "POST",
+ body: reqBody,
+ });
+
+ const respBody = await httpResp.json();
+
+ console.log("status", httpResp.status);
+
+ console.log(j2s(respBody));
+
+ const mergeUrl = new URL(`purses/${pursePub}/merge`, exchange.baseUrl);
+ mergeUrl.searchParams.set("timeout_ms", "300");
+ const statusResp = await http.fetch(mergeUrl.href, {});
+
+ const statusRespBody = await statusResp.json();
+
+ console.log(j2s(statusRespBody));
+
+ t.assertTrue(statusRespBody.merge_timestamp === undefined);
+ } catch (e) {
+ if (e instanceof TalerError) {
+ console.log(e);
+ console.log(j2s(e.errorDetail));
+ } else {
+ console.log(e);
+ }
+ throw e;
+ }
+}
+
+runExchangePurseTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts b/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts
index dee00d1ff..5ae97c3da 100644
--- a/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts
+++ b/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts
@@ -21,8 +21,11 @@ import {
AbsoluteTime,
codecForExchangeKeysJson,
DenominationPubKey,
+ DenomKeyType,
Duration,
durationFromSpec,
+ ExchangeKeysJson,
+ Logger,
} from "@gnu-taler/taler-util";
import {
createPlatformHttpLib,
@@ -32,14 +35,60 @@ import { makeNoFeeCoinConfig } from "../harness/denomStructures.js";
import {
BankService,
ExchangeService,
+ getPayto,
GlobalTestState,
MerchantService,
setupDb,
WalletCli,
- getPayto,
} from "../harness/harness.js";
import { withdrawViaBank } from "../harness/helpers.js";
+const logger = new Logger("test-exchange-timetravel.ts");
+
+interface DenomInfo {
+ denomPub: DenominationPubKey;
+ expireDeposit: string;
+}
+
+function getDenomInfoFromKeys(ek: ExchangeKeysJson): DenomInfo[] {
+ const denomInfos: DenomInfo[] = [];
+ for (const denomGroup of ek.denominations) {
+ switch (denomGroup.cipher) {
+ case "RSA":
+ case "RSA+age_restricted": {
+ let ageMask = 0;
+ if (denomGroup.cipher === "RSA+age_restricted") {
+ ageMask = denomGroup.age_mask;
+ }
+ for (const denomIn of denomGroup.denoms) {
+ const denomPub: DenominationPubKey = {
+ age_mask: ageMask,
+ cipher: DenomKeyType.Rsa,
+ rsa_public_key: denomIn.rsa_pub,
+ };
+ denomInfos.push({
+ denomPub,
+ expireDeposit: AbsoluteTime.stringify(
+ AbsoluteTime.fromProtocolTimestamp(denomIn.stamp_expire_deposit),
+ ),
+ });
+ }
+ break;
+ }
+ case "CS+age_restricted":
+ case "CS":
+ logger.warn("Clause-Schnorr denominations not supported");
+ continue;
+ default:
+ logger.warn(
+ `denomination type ${(denomGroup as any).cipher} not supported`,
+ );
+ continue;
+ }
+ }
+ return denomInfos;
+}
+
async function applyTimeTravel(
timetravelDuration: Duration,
s: {
@@ -144,7 +193,7 @@ export async function runExchangeTimetravelTest(t: GlobalTestState) {
await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:15" });
- const keysResp1 = await http.get(exchange.baseUrl + "keys");
+ const keysResp1 = await http.fetch(exchange.baseUrl + "keys");
const keys1 = await readSuccessResponseJsonOrThrow(
keysResp1,
codecForExchangeKeysJson(),
@@ -163,7 +212,7 @@ export async function runExchangeTimetravelTest(t: GlobalTestState) {
merchant,
});
- const keysResp2 = await http.get(exchange.baseUrl + "keys");
+ const keysResp2 = await http.fetch(exchange.baseUrl + "keys");
const keys2 = await readSuccessResponseJsonOrThrow(
keysResp2,
codecForExchangeKeysJson(),
@@ -173,41 +222,31 @@ export async function runExchangeTimetravelTest(t: GlobalTestState) {
JSON.stringify(keys2, undefined, 2),
);
- const denomPubs1 = keys1.denoms.map((x) => {
- return {
- denomPub: x.denom_pub,
- expireDeposit: AbsoluteTime.stringify(
- AbsoluteTime.fromProtocolTimestamp(x.stamp_expire_deposit),
- ),
- };
- });
+ const denomPubs1 = getDenomInfoFromKeys(keys1);
+ const denomPubs2 = getDenomInfoFromKeys(keys2);
- const denomPubs2 = keys2.denoms.map((x) => {
- return {
- denomPub: x.denom_pub,
- expireDeposit: AbsoluteTime.stringify(
- AbsoluteTime.fromProtocolTimestamp(x.stamp_expire_deposit),
- ),
- };
- });
const dps2 = new Set(denomPubs2.map((x) => x.denomPub));
console.log("=== KEYS RESPONSE 1 ===");
console.log(
"list issue date",
- AbsoluteTime.stringify(AbsoluteTime.fromProtocolTimestamp(keys1.list_issue_date)),
+ AbsoluteTime.stringify(
+ AbsoluteTime.fromProtocolTimestamp(keys1.list_issue_date),
+ ),
);
- console.log("num denoms", keys1.denoms.length);
+ console.log("num denoms", denomPubs1.length);
console.log("denoms", JSON.stringify(denomPubs1, undefined, 2));
console.log("=== KEYS RESPONSE 2 ===");
console.log(
"list issue date",
- AbsoluteTime.stringify(AbsoluteTime.fromProtocolTimestamp(keys2.list_issue_date)),
+ AbsoluteTime.stringify(
+ AbsoluteTime.fromProtocolTimestamp(keys2.list_issue_date),
+ ),
);
- console.log("num denoms", keys2.denoms.length);
+ console.log("num denoms", denomPubs2.length);
console.log("denoms", JSON.stringify(denomPubs2, undefined, 2));
for (const da of denomPubs1) {
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-api-bankaccount.ts b/packages/taler-harness/src/integrationtests/test-libeufin-api-bankaccount.ts
index f36168301..e5e3dfe64 100644
--- a/packages/taler-harness/src/integrationtests/test-libeufin-api-bankaccount.ts
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-api-bankaccount.ts
@@ -101,7 +101,7 @@ export async function runLibeufinApiBankaccountTest(t: GlobalTestState) {
nexus,
"local-mock",
);
- let el = findNexusPayment("mock subject", transactions.data);
+ let el = findNexusPayment("mock subject", transactions);
t.assertTrue(el instanceof Object);
}
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-api-bankconnection.ts b/packages/taler-harness/src/integrationtests/test-libeufin-api-bankconnection.ts
index 912b7b2ac..243500dc9 100644
--- a/packages/taler-harness/src/integrationtests/test-libeufin-api-bankconnection.ts
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-api-bankconnection.ts
@@ -45,12 +45,12 @@ export async function runLibeufinApiBankconnectionTest(t: GlobalTestState) {
});
let connections = await LibeufinNexusApi.getAllConnections(nexus);
- t.assertTrue(connections.data["bankConnections"].length == 1);
+ t.assertTrue(connections.bankConnections.length == 1);
await LibeufinNexusApi.deleteBankConnection(nexus, {
bankConnectionId: "bankconnection-api-test-connection",
});
connections = await LibeufinNexusApi.getAllConnections(nexus);
- t.assertTrue(connections.data["bankConnections"].length == 0);
+ t.assertTrue(connections.bankConnections.length == 0);
}
runLibeufinApiBankconnectionTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-api-facade-bad-request.ts b/packages/taler-harness/src/integrationtests/test-libeufin-api-facade-bad-request.ts
index a1da9e0da..27cc81588 100644
--- a/packages/taler-harness/src/integrationtests/test-libeufin-api-facade-bad-request.ts
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-api-facade-bad-request.ts
@@ -18,15 +18,16 @@
* Imports.
*/
import { URL } from "@gnu-taler/taler-util";
-import axiosImp from "axios";
-import { GlobalTestState } from "../harness/harness.js";
+import { GlobalTestState, harnessHttpLib } from "../harness/harness.js";
import {
launchLibeufinServices,
NexusUserBundle,
SandboxUserBundle,
} from "../harness/libeufin.js";
-
-const axios = axiosImp.default;
+import {
+ createPlatformHttpLib,
+ makeBasicAuthHeader,
+} from "@gnu-taler/taler-util/http";
export async function runLibeufinApiFacadeBadRequestTest(t: GlobalTestState) {
/**
@@ -50,21 +51,17 @@ export async function runLibeufinApiFacadeBadRequestTest(t: GlobalTestState) {
console.log("malformed facade");
const baseUrl = libeufinServices.libeufinNexus.baseUrl;
let url = new URL("facades", baseUrl);
- let resp = await axios.post(
- url.href,
- {
+ let resp = await harnessHttpLib.fetch(url.href, {
+ method: "POST",
+ body: {
name: "malformed-facade",
type: "taler-wire-gateway",
config: {}, // malformation here.
},
- {
- auth: {
- username: "admin",
- password: "test",
- },
- validateStatus: () => true,
+ headers: {
+ Authorization: makeBasicAuthHeader("admin", "test"),
},
- );
+ });
t.assertTrue(resp.status == 400);
}
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-api-facade.ts b/packages/taler-harness/src/integrationtests/test-libeufin-api-facade.ts
index 946c565d4..a819dd481 100644
--- a/packages/taler-harness/src/integrationtests/test-libeufin-api-facade.ts
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-api-facade.ts
@@ -51,20 +51,20 @@ export async function runLibeufinApiFacadeTest(t: GlobalTestState) {
libeufinServices.libeufinNexus,
);
// check that original facade shows up.
- t.assertTrue(resp.data["facades"][0]["name"] == user01nexus.twgReq["name"]);
+ t.assertTrue(resp.facades[0].name == user01nexus.twgReq["name"]);
- const twgBaseUrl: string = resp.data["facades"][0]["baseUrl"];
+ const twgBaseUrl: string = resp.facades[0]["baseUrl"];
t.assertTrue(typeof twgBaseUrl === "string");
t.assertTrue(twgBaseUrl.startsWith("http://"));
t.assertTrue(twgBaseUrl.endsWith("/"));
// delete it.
- resp = await LibeufinNexusApi.deleteFacade(
+ await LibeufinNexusApi.deleteFacade(
libeufinServices.libeufinNexus,
user01nexus.twgReq["name"],
);
- // check that no facades show up.
- t.assertTrue(!resp.data.hasOwnProperty("facades"));
+ resp = await LibeufinNexusApi.getAllFacades(libeufinServices.libeufinNexus);
+ t.assertTrue(!resp.hasOwnProperty("facades"));
}
runLibeufinApiFacadeTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-api-permissions.ts b/packages/taler-harness/src/integrationtests/test-libeufin-api-permissions.ts
index f8f2d7d80..56443c20a 100644
--- a/packages/taler-harness/src/integrationtests/test-libeufin-api-permissions.ts
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-api-permissions.ts
@@ -46,7 +46,8 @@ export async function runLibeufinApiPermissionsTest(t: GlobalTestState) {
user01nexus.twgTransferPermission,
);
let transferPermission = await LibeufinNexusApi.getAllPermissions(nexus);
- let element = transferPermission.data["permissions"].pop();
+ let element = transferPermission["permissions"].pop();
+ t.assertTrue(!!element);
t.assertTrue(
element["permissionName"] == "facade.talerwiregateway.transfer" &&
element["subjectId"] == "username-01",
@@ -58,7 +59,7 @@ export async function runLibeufinApiPermissionsTest(t: GlobalTestState) {
await LibeufinNexusApi.postPermission(nexus, denyTransfer);
transferPermission = await LibeufinNexusApi.getAllPermissions(nexus);
- t.assertTrue(transferPermission.data["permissions"].length == 0);
+ t.assertTrue(transferPermission["permissions"].length == 0);
}
runLibeufinApiPermissionsTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-api-sandbox-transactions.ts b/packages/taler-harness/src/integrationtests/test-libeufin-api-sandbox-transactions.ts
index f1ff69a6d..6cfc55aa6 100644
--- a/packages/taler-harness/src/integrationtests/test-libeufin-api-sandbox-transactions.ts
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-api-sandbox-transactions.ts
@@ -64,6 +64,6 @@ export async function runLibeufinApiSandboxTransactionsTest(
sandbox,
"mock-account",
);
- t.assertAmountEquals(ret.data.balance, "EUR:2.1");
+ t.assertAmountEquals(ret.balance, "EUR:2.1");
}
runLibeufinApiSandboxTransactionsTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-api-scheduling.ts b/packages/taler-harness/src/integrationtests/test-libeufin-api-scheduling.ts
index 95f4bfaa0..15ed2ab78 100644
--- a/packages/taler-harness/src/integrationtests/test-libeufin-api-scheduling.ts
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-api-scheduling.ts
@@ -54,19 +54,19 @@ export async function runLibeufinApiSchedulingTest(t: GlobalTestState) {
rangeType: "all",
},
});
- let resp = await LibeufinNexusApi.getTasks(
+ let resp = await LibeufinNexusApi.getTask(
nexus,
user01nexus.localAccountName,
"test-task",
);
- t.assertTrue(resp.data["taskName"] == "test-task");
+ t.assertTrue(resp.taskName == "test-task");
await LibeufinNexusApi.deleteTask(
nexus,
user01nexus.localAccountName,
"test-task",
);
try {
- await LibeufinNexusApi.getTasks(
+ await LibeufinNexusApi.getTask(
nexus,
user01nexus.localAccountName,
"test-task",
@@ -82,19 +82,19 @@ export async function runLibeufinApiSchedulingTest(t: GlobalTestState) {
type: "submit",
params: {},
});
- resp = await LibeufinNexusApi.getTasks(
+ resp = await LibeufinNexusApi.getTask(
nexus,
user01nexus.localAccountName,
"test-task",
);
- t.assertTrue(resp.data["taskName"] == "test-task");
+ t.assertTrue(resp.taskName == "test-task");
await LibeufinNexusApi.deleteTask(
nexus,
user01nexus.localAccountName,
"test-task",
);
try {
- await LibeufinNexusApi.getTasks(
+ await LibeufinNexusApi.getTask(
nexus,
user01nexus.localAccountName,
"test-task",
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-api-users.ts b/packages/taler-harness/src/integrationtests/test-libeufin-api-users.ts
index bc3103c7e..662b22bbe 100644
--- a/packages/taler-harness/src/integrationtests/test-libeufin-api-users.ts
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-api-users.ts
@@ -56,8 +56,8 @@ export async function runLibeufinApiUsersTest(t: GlobalTestState) {
password: "got-changed",
},
});
- console.log(resp.data);
- t.assertTrue(resp.data["username"] == "one" && !resp.data["superuser"]);
+ console.log(resp);
+ t.assertTrue(resp["username"] == "one" && !resp["superuser"]);
}
runLibeufinApiUsersTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-c5x.ts b/packages/taler-harness/src/integrationtests/test-libeufin-c5x.ts
index c6dfca99b..5097bc4d3 100644
--- a/packages/taler-harness/src/integrationtests/test-libeufin-c5x.ts
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-c5x.ts
@@ -79,7 +79,7 @@ export async function runLibeufinC5xTest(t: GlobalTestState) {
libeufinServices.libeufinNexus,
user01nexus.localAccountName,
);
- t.assertTrue(nexusTxs.data["transactions"].length == 0);
+ t.assertTrue(nexusTxs["transactions"].length == 0);
// Addressing one payment to user 01
await libeufinServices.libeufinSandbox.makeTransaction(
@@ -95,8 +95,8 @@ export async function runLibeufinC5xTest(t: GlobalTestState) {
"all", // range
"report", // C52
);
- t.assertTrue(expectOne.data.newTransactions == 1);
- t.assertTrue(expectOne.data.downloadedTransactions == 1);
+ t.assertTrue(expectOne.newTransactions == 1);
+ t.assertTrue(expectOne.downloadedTransactions == 1);
/* Expect zero payments being downloaded because the
* previous request consumed already the one pending
@@ -108,8 +108,8 @@ export async function runLibeufinC5xTest(t: GlobalTestState) {
"all", // range
"report", // C52
);
- t.assertTrue(expectZero.data.newTransactions == 0);
- t.assertTrue(expectZero.data.downloadedTransactions == 0);
+ t.assertTrue(expectZero.newTransactions == 0);
+ t.assertTrue(expectZero.downloadedTransactions == 0);
/**
* A statement should still account zero payments because
@@ -121,8 +121,8 @@ export async function runLibeufinC5xTest(t: GlobalTestState) {
"all", // range
"statement", // C53
);
- t.assertTrue(expectZero.data.newTransactions == 0);
- t.assertTrue(expectZero.data.downloadedTransactions == 0);
+ t.assertTrue(expectZero.newTransactions == 0);
+ t.assertTrue(expectZero.downloadedTransactions == 0);
/**
* Ticking now. That books any pending transaction.
@@ -141,7 +141,7 @@ export async function runLibeufinC5xTest(t: GlobalTestState) {
"all", // range
"statement", // C53
);
- t.assertTrue(expectOne.data.downloadedTransactions == 1);
- t.assertTrue(expectOne.data.newTransactions == 0);
+ t.assertTrue(expectOne.downloadedTransactions == 1);
+ t.assertTrue(expectOne.newTransactions == 0);
}
runLibeufinC5xTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-facade-anastasis.ts b/packages/taler-harness/src/integrationtests/test-libeufin-facade-anastasis.ts
index 10c73fed5..0efd55f44 100644
--- a/packages/taler-harness/src/integrationtests/test-libeufin-facade-anastasis.ts
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-facade-anastasis.ts
@@ -53,9 +53,9 @@ export async function runLibeufinAnastasisFacadeTest(t: GlobalTestState) {
);
// check that original facade shows up.
t.assertTrue(
- resp.data["facades"][0]["name"] == user01nexus.anastasisReq["name"],
+ resp["facades"][0]["name"] == user01nexus.anastasisReq["name"],
);
- const anastasisBaseUrl: string = resp.data["facades"][0]["baseUrl"];
+const anastasisBaseUrl: string = resp["facades"][0]["baseUrl"];
t.assertTrue(typeof anastasisBaseUrl === "string");
t.assertTrue(anastasisBaseUrl.startsWith("http://"));
t.assertTrue(anastasisBaseUrl.endsWith("/"));
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-refund.ts b/packages/taler-harness/src/integrationtests/test-libeufin-refund.ts
index 9d90121a0..d37363bab 100644
--- a/packages/taler-harness/src/integrationtests/test-libeufin-refund.ts
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-refund.ts
@@ -77,7 +77,7 @@ export async function runLibeufinRefundTest(t: GlobalTestState) {
libeufinServices.libeufinNexus,
user01nexus.localAccountName,
);
- t.assertTrue(nexusTxs.data["transactions"].length == 1);
+ t.assertTrue(nexusTxs["transactions"].length == 1);
// Submit the reimbursement
await LibeufinNexusApi.submitInitiatedPayment(
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-sandbox-wire-transfer-cli.ts b/packages/taler-harness/src/integrationtests/test-libeufin-sandbox-wire-transfer-cli.ts
index e6b074d3f..be467e2f1 100644
--- a/packages/taler-harness/src/integrationtests/test-libeufin-sandbox-wire-transfer-cli.ts
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-sandbox-wire-transfer-cli.ts
@@ -79,7 +79,7 @@ export async function runLibeufinSandboxWireTransferCliTest(
sandbox,
"mock-account-2",
);
- console.log(ret.data.balance);
- t.assertTrue(ret.data.balance == "EUR:1.89");
+ console.log(ret.balance);
+ t.assertTrue(ret.balance == "EUR:1.89");
}
runLibeufinSandboxWireTransferCliTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts b/packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts
index 2fccd0b97..e18cd7a0f 100644
--- a/packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts
+++ b/packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts
@@ -23,8 +23,6 @@ import {
PreparePayResultType,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import axiosImp from "axios";
-const axios = axiosImp.default;
import { URL } from "url";
import { defaultCoinConfig } from "../harness/denomStructures.js";
import {
@@ -36,6 +34,7 @@ import {
ExchangeService,
getPayto,
GlobalTestState,
+ harnessHttpLib,
MerchantPrivateApi,
MerchantService,
setupDb,
@@ -45,6 +44,7 @@ import {
FaultyMerchantTestEnvironment,
withdrawViaBank,
} from "../harness/helpers.js";
+import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
/**
* Run a test case with a simple TESTKUDOS Taler environment, consisting
@@ -186,9 +186,7 @@ export async function runMerchantExchangeConfusionTest(t: GlobalTestState) {
t.assertTrue(orderStatus.already_paid_order_id === undefined);
let publicOrderStatusUrl = orderStatus.order_status_url;
- let publicOrderStatusResp = await axios.get(publicOrderStatusUrl, {
- validateStatus: () => true,
- });
+ let publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl);
if (publicOrderStatusResp.status != 402) {
throw Error(
@@ -197,7 +195,7 @@ export async function runMerchantExchangeConfusionTest(t: GlobalTestState) {
}
let pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
- publicOrderStatusResp.data,
+ await publicOrderStatusResp.json(),
);
console.log(pubUnpaidStatus);
@@ -221,9 +219,7 @@ export async function runMerchantExchangeConfusionTest(t: GlobalTestState) {
console.log("requesting", orderUrlWithHash.href);
- publicOrderStatusResp = await axios.get(orderUrlWithHash.href, {
- validateStatus: () => true,
- });
+ publicOrderStatusResp = await harnessHttpLib.fetch(orderUrlWithHash.href);
if (publicOrderStatusResp.status != 402) {
throw Error(
@@ -232,7 +228,7 @@ export async function runMerchantExchangeConfusionTest(t: GlobalTestState) {
}
pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
- publicOrderStatusResp.data,
+ await publicOrderStatusResp.json(),
);
const confirmPayRes = await wallet.client.call(
diff --git a/packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts b/packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts
index 09231cdd8..e6e5bff76 100644
--- a/packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts
+++ b/packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts
@@ -17,9 +17,7 @@
/**
* Imports.
*/
-import { URL } from "@gnu-taler/taler-util";
-import axiosImp from "axios";
-const axios = axiosImp.default;
+import { TalerError, URL } from "@gnu-taler/taler-util";
import {
ExchangeService,
GlobalTestState,
@@ -27,7 +25,9 @@ import {
MerchantService,
setupDb,
getPayto,
+ harnessHttpLib,
} from "../harness/harness.js";
+import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
/**
* Test instance deletion and authentication for it
@@ -61,15 +61,17 @@ export async function runMerchantInstancesDeleteTest(t: GlobalTestState) {
const baseUrl = merchant.makeInstanceBaseUrl();
{
- const r = await axios.get(new URL("config", baseUrl).href);
- console.log(r.data);
- t.assertDeepEqual(r.data.currency, "TESTKUDOS");
+ const r = await harnessHttpLib.fetch(new URL("config", baseUrl).href);
+ const data = await r.json();
+ console.log(data);
+ t.assertDeepEqual(data.currency, "TESTKUDOS");
}
// Instances should initially be empty
{
- const r = await axios.get(new URL("management/instances", baseUrl).href);
- t.assertDeepEqual(r.data.instances, []);
+ const r = await harnessHttpLib.fetch(new URL("management/instances", baseUrl).href);
+ const data = await r.json();
+ t.assertDeepEqual(data.instances, []);
}
// Add an instance, no auth!
@@ -121,8 +123,8 @@ export async function runMerchantInstancesDeleteTest(t: GlobalTestState) {
await unauthMerchantClient.deleteInstance("myinst");
});
console.log("Got expected exception", exc);
- t.assertAxiosError(exc);
- t.assertDeepEqual(exc.response?.status, 401);
+ t.assertTrue(exc instanceof TalerError);
+ t.assertDeepEqual(exc.errorDetail.httpStatusCode, 401);
}
}
diff --git a/packages/taler-harness/src/integrationtests/test-merchant-instances-urls.ts b/packages/taler-harness/src/integrationtests/test-merchant-instances-urls.ts
index 534b35278..18a09c76b 100644
--- a/packages/taler-harness/src/integrationtests/test-merchant-instances-urls.ts
+++ b/packages/taler-harness/src/integrationtests/test-merchant-instances-urls.ts
@@ -18,8 +18,6 @@
* Imports.
*/
import { Duration } from "@gnu-taler/taler-util";
-import axiosImp from "axios";
-const axios = axiosImp.default;
import {
ExchangeService,
GlobalTestState,
@@ -27,14 +25,14 @@ import {
MerchantService,
setupDb,
getPayto,
+ harnessHttpLib,
} from "../harness/harness.js";
+import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
/**
* Do basic checks on instance management and authentication.
*/
export async function runMerchantInstancesUrlsTest(t: GlobalTestState) {
- // Set up test environment
-
const db = await setupDb(t);
const exchange = ExchangeService.create(t, {
@@ -111,11 +109,10 @@ export async function runMerchantInstancesUrlsTest(t: GlobalTestState) {
});
async function check(url: string, token: string, expectedStatus: number) {
- const resp = await axios.get(url, {
+ const resp = await harnessHttpLib.fetch(url, {
headers: {
Authorization: `Bearer ${token}`,
},
- validateStatus: () => true,
});
console.log(
`checking ${url}, expected ${expectedStatus}, got ${resp.status}`,
diff --git a/packages/taler-harness/src/integrationtests/test-merchant-instances.ts b/packages/taler-harness/src/integrationtests/test-merchant-instances.ts
index 78626ea3d..f7d89c543 100644
--- a/packages/taler-harness/src/integrationtests/test-merchant-instances.ts
+++ b/packages/taler-harness/src/integrationtests/test-merchant-instances.ts
@@ -18,8 +18,6 @@
* Imports.
*/
import { URL } from "@gnu-taler/taler-util";
-import axiosImp from "axios";
-const axios = axiosImp.default;
import {
ExchangeService,
GlobalTestState,
@@ -27,7 +25,9 @@ import {
MerchantService,
setupDb,
getPayto,
+ harnessHttpLib,
} from "../harness/harness.js";
+import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
/**
* Do basic checks on instance management and authentication.
@@ -61,15 +61,19 @@ export async function runMerchantInstancesTest(t: GlobalTestState) {
const baseUrl = merchant.makeInstanceBaseUrl();
{
- const r = await axios.get(new URL("config", baseUrl).href);
- console.log(r.data);
- t.assertDeepEqual(r.data.currency, "TESTKUDOS");
+ const r = await harnessHttpLib.fetch(new URL("config", baseUrl).href);
+ const data = await r.json();
+ console.log(data);
+ t.assertDeepEqual(data.currency, "TESTKUDOS");
}
// Instances should initially be empty
{
- const r = await axios.get(new URL("management/instances", baseUrl).href);
- t.assertDeepEqual(r.data.instances, []);
+ const r = await harnessHttpLib.fetch(
+ new URL("management/instances", baseUrl).href,
+ );
+ const data = await r.json();
+ t.assertDeepEqual(data.instances, []);
}
// Add an instance, no auth!
@@ -104,11 +108,14 @@ export async function runMerchantInstancesTest(t: GlobalTestState) {
// Check that a "malformed" bearer Authorization header gets ignored
{
const url = merchant.makeInstanceBaseUrl();
- const resp = await axios.get(new URL("management/instances", url).href, {
- headers: {
- Authorization: "foo bar-baz",
+ const resp = await harnessHttpLib.fetch(
+ new URL("management/instances", url).href,
+ {
+ headers: {
+ Authorization: "foo bar-baz",
+ },
},
- });
+ );
t.assertDeepEqual(resp.status, 200);
}
@@ -130,9 +137,7 @@ export async function runMerchantInstancesTest(t: GlobalTestState) {
});
console.log(exc);
-
- t.assertAxiosError(exc);
- t.assertTrue(exc.response?.status === 401);
+ t.assertTrue(exc.errorDetail.httpStatusCode === 401);
merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl(), {
method: "token",
@@ -145,12 +150,15 @@ export async function runMerchantInstancesTest(t: GlobalTestState) {
// Now, try some variations.
{
const url = merchant.makeInstanceBaseUrl();
- const resp = await axios.get(new URL("management/instances", url).href, {
- headers: {
- // Note the spaces
- Authorization: "Bearer secret-token:foobar",
+ const resp = await harnessHttpLib.fetch(
+ new URL("management/instances", url).href,
+ {
+ headers: {
+ // Note the spaces
+ Authorization: "Bearer secret-token:foobar",
+ },
},
- });
+ );
t.assertDeepEqual(resp.status, 200);
}
@@ -176,7 +184,7 @@ export async function runMerchantInstancesTest(t: GlobalTestState) {
await unauthMerchantClient.deleteInstance("myinst");
});
console.log(exc);
- t.assertAxiosError(exc);
+ t.assertTrue(exc.errorDetail.httpStatusCode === 401);
t.assertDeepEqual(exc.response?.status, 401);
}
}
diff --git a/packages/taler-harness/src/integrationtests/test-merchant-longpolling.ts b/packages/taler-harness/src/integrationtests/test-merchant-longpolling.ts
index 59f23fe5d..8d271c5d1 100644
--- a/packages/taler-harness/src/integrationtests/test-merchant-longpolling.ts
+++ b/packages/taler-harness/src/integrationtests/test-merchant-longpolling.ts
@@ -24,20 +24,18 @@ import {
codecForMerchantOrderStatusUnpaid,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import axiosImp from "axios";
-import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js";
+import { GlobalTestState, MerchantPrivateApi, harnessHttpLib } from "../harness/harness.js";
import {
createSimpleTestkudosEnvironmentV2,
- withdrawViaBankV2
+ withdrawViaBankV2,
} from "../harness/helpers.js";
-const axios = axiosImp.default;
+import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
/**
* Run test for basic, bank-integrated withdrawal.
*/
export async function runMerchantLongpollingTest(t: GlobalTestState) {
// Set up test environment
-
const { walletClient, bank, exchange, merchant } =
await createSimpleTestkudosEnvironmentV2(t);
@@ -83,9 +81,7 @@ export async function runMerchantLongpollingTest(t: GlobalTestState) {
// First, request order status without longpolling
{
console.log("requesting", publicOrderStatusUrl.href);
- let publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, {
- validateStatus: () => true,
- });
+ let publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl.href);
if (publicOrderStatusResp.status != 402) {
throw Error(
@@ -98,9 +94,7 @@ export async function runMerchantLongpollingTest(t: GlobalTestState) {
publicOrderStatusUrl.searchParams.set("timeout_ms", "500");
console.log("requesting", publicOrderStatusUrl.href);
- let publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, {
- validateStatus: () => true,
- });
+ let publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl.href);
if (publicOrderStatusResp.status != 402) {
throw Error(
@@ -109,7 +103,7 @@ export async function runMerchantLongpollingTest(t: GlobalTestState) {
}
let pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
- publicOrderStatusResp.data,
+ await publicOrderStatusResp.json(),
);
console.log(pubUnpaidStatus);
@@ -135,9 +129,7 @@ export async function runMerchantLongpollingTest(t: GlobalTestState) {
preparePayResp.contractTermsHash,
);
- let publicOrderStatusPromise = axios.get(publicOrderStatusUrl.href, {
- validateStatus: () => true,
- });
+ let publicOrderStatusPromise = harnessHttpLib.fetch(publicOrderStatusUrl.href);
t.assertTrue(preparePayResp.status === PreparePayResultType.PaymentPossible);
@@ -152,15 +144,12 @@ export async function runMerchantLongpollingTest(t: GlobalTestState) {
}
pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
- publicOrderStatusResp.data,
+ await publicOrderStatusResp.json(),
);
- const confirmPayRes = await walletClient.call(
- WalletApiOperation.ConfirmPay,
- {
- proposalId: proposalId,
- },
- );
+ const confirmPayRes = await walletClient.call(WalletApiOperation.ConfirmPay, {
+ proposalId: proposalId,
+ });
t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done);
}
diff --git a/packages/taler-harness/src/integrationtests/test-merchant-refund-api.ts b/packages/taler-harness/src/integrationtests/test-merchant-refund-api.ts
index 5d9b23fa7..8efac1fc1 100644
--- a/packages/taler-harness/src/integrationtests/test-merchant-refund-api.ts
+++ b/packages/taler-harness/src/integrationtests/test-merchant-refund-api.ts
@@ -17,12 +17,14 @@
/**
* Imports.
*/
+import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
import {
GlobalTestState,
MerchantPrivateApi,
MerchantServiceInterface,
WalletCli,
ExchangeServiceInterface,
+ harnessHttpLib,
} from "../harness/harness.js";
import {
createSimpleTestkudosEnvironment,
@@ -34,8 +36,6 @@ import {
PreparePayResultType,
Duration,
} from "@gnu-taler/taler-util";
-import axiosImp from "axios";
-const axios = axiosImp.default;
import {
WalletApiOperation,
BankServiceHandle,
@@ -136,23 +136,19 @@ async function testRefundApiWithFulfillmentUrl(
preparePayResult.contractTermsHash,
);
- let publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, {
- validateStatus: () => true,
- });
- console.log(publicOrderStatusResp.data);
+ let publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl.href);
+ const respData = await publicOrderStatusResp.json();
t.assertTrue(publicOrderStatusResp.status === 200);
- t.assertAmountEquals(publicOrderStatusResp.data.refund_amount, "TESTKUDOS:5");
+ t.assertAmountEquals(respData.refund_amount, "TESTKUDOS:5");
publicOrderStatusUrl = new URL(
`orders/${orderId}`,
merchant.makeInstanceBaseUrl(),
);
console.log(`requesting order status via '${publicOrderStatusUrl.href}'`);
- publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, {
- validateStatus: () => true,
- });
+ publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl.href);
console.log(publicOrderStatusResp.status);
- console.log(publicOrderStatusResp.data);
+ console.log(await publicOrderStatusResp.json());
// We didn't give any authentication, so we should get a fulfillment URL back
t.assertTrue(publicOrderStatusResp.status === 403);
}
@@ -252,22 +248,20 @@ async function testRefundApiWithFulfillmentMessage(
preparePayResult.contractTermsHash,
);
- let publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, {
- validateStatus: () => true,
- });
- console.log(publicOrderStatusResp.data);
+ let publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl.href);
+ let respData = await publicOrderStatusResp.json();
+ console.log(respData);
t.assertTrue(publicOrderStatusResp.status === 200);
- t.assertAmountEquals(publicOrderStatusResp.data.refund_amount, "TESTKUDOS:5");
+ t.assertAmountEquals(respData.refund_amount, "TESTKUDOS:5");
publicOrderStatusUrl = new URL(
`orders/${orderId}`,
merchant.makeInstanceBaseUrl(),
);
- publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, {
- validateStatus: () => true,
- });
- console.log(publicOrderStatusResp.data);
+ publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl.href);
+ respData = await publicOrderStatusResp.json();
+ console.log(respData);
// We didn't give any authentication, so we should get a fulfillment URL back
t.assertTrue(publicOrderStatusResp.status === 403);
}
diff --git a/packages/taler-harness/src/integrationtests/test-merchant-spec-public-orders.ts b/packages/taler-harness/src/integrationtests/test-merchant-spec-public-orders.ts
index 975ba707b..e959e813b 100644
--- a/packages/taler-harness/src/integrationtests/test-merchant-spec-public-orders.ts
+++ b/packages/taler-harness/src/integrationtests/test-merchant-spec-public-orders.ts
@@ -24,7 +24,6 @@ import {
encodeCrock,
getRandomBytes,
} from "@gnu-taler/taler-util";
-import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import {
BankService,
@@ -33,17 +32,13 @@ import {
MerchantPrivateApi,
MerchantService,
WalletCli,
+ harnessHttpLib,
} from "../harness/harness.js";
import {
createSimpleTestkudosEnvironment,
withdrawViaBank,
} from "../harness/helpers.js";
-const httpLib = createPlatformHttpLib({
- allowHttp: true,
- enableThrottling: false,
-});
-
interface Context {
merchant: MerchantService;
merchantBaseUrl: string;
@@ -51,6 +46,8 @@ interface Context {
exchange: ExchangeService;
}
+const httpLib = harnessHttpLib;
+
async function testWithClaimToken(
t: GlobalTestState,
c: Context,
@@ -75,7 +72,7 @@ async function testWithClaimToken(
let talerPayUri: string;
{
- const httpResp = await httpLib.get(
+ const httpResp = await httpLib.fetch(
new URL(`orders/${orderId}`, merchantBaseUrl).href,
);
const r = await httpResp.json();
@@ -86,7 +83,7 @@ async function testWithClaimToken(
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
url.searchParams.set("token", claimToken);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
t.assertDeepEqual(httpResp.status, 402);
console.log(r);
@@ -97,7 +94,7 @@ async function testWithClaimToken(
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
url.searchParams.set("token", claimToken);
- const httpResp = await httpLib.get(url.href, {
+ const httpResp = await httpLib.fetch(url.href, {
headers: {
Accept: "text/html",
},
@@ -123,7 +120,7 @@ async function testWithClaimToken(
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
const hcWrong = encodeCrock(getRandomBytes(64));
url.searchParams.set("h_contract", hcWrong);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 403);
@@ -134,7 +131,7 @@ async function testWithClaimToken(
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
const ctWrong = encodeCrock(getRandomBytes(16));
url.searchParams.set("token", ctWrong);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 403);
@@ -144,7 +141,7 @@ async function testWithClaimToken(
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
url.searchParams.set("token", claimToken);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 402);
@@ -154,7 +151,7 @@ async function testWithClaimToken(
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
url.searchParams.set("h_contract", contractTermsHash);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 402);
@@ -163,7 +160,7 @@ async function testWithClaimToken(
// claimed, unpaid, access without credentials
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 202);
@@ -181,7 +178,7 @@ async function testWithClaimToken(
// paid, access without credentials
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 202);
@@ -192,7 +189,7 @@ async function testWithClaimToken(
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
const hcWrong = encodeCrock(getRandomBytes(64));
url.searchParams.set("h_contract", hcWrong);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 403);
@@ -203,7 +200,7 @@ async function testWithClaimToken(
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
const ctWrong = encodeCrock(getRandomBytes(16));
url.searchParams.set("token", ctWrong);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 403);
@@ -213,7 +210,7 @@ async function testWithClaimToken(
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
url.searchParams.set("h_contract", contractTermsHash);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 200);
@@ -223,7 +220,7 @@ async function testWithClaimToken(
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
url.searchParams.set("token", claimToken);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 200);
@@ -235,7 +232,7 @@ async function testWithClaimToken(
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
url.searchParams.set("token", claimToken);
- const httpResp = await httpLib.get(url.href, {
+ const httpResp = await httpLib.fetch(url.href, {
headers: { Accept: "text/html" },
});
t.assertDeepEqual(httpResp.status, 200);
@@ -272,7 +269,7 @@ async function testWithClaimToken(
{
const url = new URL(`orders/${apOrderId}`, merchantBaseUrl);
url.searchParams.set("token", apToken);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 402);
@@ -283,7 +280,7 @@ async function testWithClaimToken(
const url = new URL(`orders/${apOrderId}`, merchantBaseUrl);
url.searchParams.set("token", apToken);
url.searchParams.set("session_id", sessionId);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 402);
@@ -296,7 +293,7 @@ async function testWithClaimToken(
const url = new URL(`orders/${apOrderId}`, merchantBaseUrl);
url.searchParams.set("token", apToken);
url.searchParams.set("session_id", sessionId);
- const httpResp = await httpLib.get(url.href, {
+ const httpResp = await httpLib.fetch(url.href, {
headers: { Accept: "text/html" },
});
t.assertDeepEqual(httpResp.status, 302);
@@ -329,7 +326,7 @@ async function testWithoutClaimToken(
let talerPayUri: string;
{
- const httpResp = await httpLib.get(
+ const httpResp = await httpLib.fetch(
new URL(`orders/${orderId}`, merchantBaseUrl).href,
);
const r = await httpResp.json();
@@ -339,7 +336,7 @@ async function testWithoutClaimToken(
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
t.assertDeepEqual(httpResp.status, 402);
console.log(r);
@@ -349,7 +346,7 @@ async function testWithoutClaimToken(
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
- const httpResp = await httpLib.get(url.href, {
+ const httpResp = await httpLib.fetch(url.href, {
headers: {
Accept: "text/html",
},
@@ -377,7 +374,7 @@ async function testWithoutClaimToken(
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
const hcWrong = encodeCrock(getRandomBytes(64));
url.searchParams.set("h_contract", hcWrong);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 403);
@@ -388,7 +385,7 @@ async function testWithoutClaimToken(
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
const ctWrong = encodeCrock(getRandomBytes(16));
url.searchParams.set("token", ctWrong);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 403);
@@ -397,7 +394,7 @@ async function testWithoutClaimToken(
// claimed, unpaid, no claim token
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 402);
@@ -407,7 +404,7 @@ async function testWithoutClaimToken(
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
url.searchParams.set("h_contract", contractTermsHash);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 402);
@@ -416,7 +413,7 @@ async function testWithoutClaimToken(
// claimed, unpaid, access without credentials
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
// No credentials, but the order doesn't require a claim token.
@@ -437,7 +434,7 @@ async function testWithoutClaimToken(
// paid, access without credentials
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 200);
@@ -448,7 +445,7 @@ async function testWithoutClaimToken(
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
const hcWrong = encodeCrock(getRandomBytes(64));
url.searchParams.set("h_contract", hcWrong);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 403);
@@ -459,7 +456,7 @@ async function testWithoutClaimToken(
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
const ctWrong = encodeCrock(getRandomBytes(16));
url.searchParams.set("token", ctWrong);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 403);
@@ -469,7 +466,7 @@ async function testWithoutClaimToken(
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
url.searchParams.set("h_contract", contractTermsHash);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 200);
@@ -478,7 +475,7 @@ async function testWithoutClaimToken(
// paid, JSON
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 200);
@@ -489,7 +486,7 @@ async function testWithoutClaimToken(
// paid, HTML
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
- const httpResp = await httpLib.get(url.href, {
+ const httpResp = await httpLib.fetch(url.href, {
headers: { Accept: "text/html" },
});
t.assertDeepEqual(httpResp.status, 200);
@@ -526,7 +523,7 @@ async function testWithoutClaimToken(
{
const url = new URL(`orders/${apOrderId}`, merchantBaseUrl);
url.searchParams.set("token", apToken);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 402);
@@ -537,7 +534,7 @@ async function testWithoutClaimToken(
const url = new URL(`orders/${apOrderId}`, merchantBaseUrl);
url.searchParams.set("token", apToken);
url.searchParams.set("session_id", sessionId);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 402);
@@ -550,7 +547,7 @@ async function testWithoutClaimToken(
const url = new URL(`orders/${apOrderId}`, merchantBaseUrl);
url.searchParams.set("token", apToken);
url.searchParams.set("session_id", sessionId);
- const httpResp = await httpLib.get(url.href, {
+ const httpResp = await httpLib.fetch(url.href, {
headers: { Accept: "text/html" },
});
t.assertDeepEqual(httpResp.status, 302);
@@ -575,14 +572,14 @@ export async function runMerchantSpecPublicOrdersTest(t: GlobalTestState) {
const merchantBaseUrl = merchant.makeInstanceBaseUrl();
{
- const httpResp = await httpLib.get(new URL("config", merchantBaseUrl).href);
+ const httpResp = await httpLib.fetch(new URL("config", merchantBaseUrl).href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(r.currency, "TESTKUDOS");
}
{
- const httpResp = await httpLib.get(
+ const httpResp = await httpLib.fetch(
new URL("orders/foo", merchantBaseUrl).href,
);
const r = await httpResp.json();
@@ -592,7 +589,7 @@ export async function runMerchantSpecPublicOrdersTest(t: GlobalTestState) {
}
{
- const httpResp = await httpLib.get(
+ const httpResp = await httpLib.fetch(
new URL("orders/foo", merchantBaseUrl).href,
{
headers: {
diff --git a/packages/taler-harness/src/integrationtests/test-pay-paid.ts b/packages/taler-harness/src/integrationtests/test-pay-paid.ts
index 2ef91e4a8..a377b7237 100644
--- a/packages/taler-harness/src/integrationtests/test-pay-paid.ts
+++ b/packages/taler-harness/src/integrationtests/test-pay-paid.ts
@@ -17,7 +17,7 @@
/**
* Imports.
*/
-import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js";
+import { GlobalTestState, MerchantPrivateApi, harnessHttpLib } from "../harness/harness.js";
import {
withdrawViaBank,
createFaultInjectedMerchantTestkudosEnvironment,
@@ -28,8 +28,6 @@ import {
ConfirmPayResultType,
URL,
} from "@gnu-taler/taler-util";
-import axiosImp from "axios";
-const axios = axiosImp.default;
import { FaultInjectionRequestContext } from "../harness/faultInjection.js";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
@@ -86,9 +84,7 @@ export async function runPayPaidTest(t: GlobalTestState) {
t.assertTrue(orderStatus.already_paid_order_id === undefined);
let publicOrderStatusUrl = orderStatus.order_status_url;
- let publicOrderStatusResp = await axios.get(publicOrderStatusUrl, {
- validateStatus: () => true,
- });
+ let publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl);
if (publicOrderStatusResp.status != 402) {
throw Error(
@@ -97,7 +93,7 @@ export async function runPayPaidTest(t: GlobalTestState) {
}
let pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
- publicOrderStatusResp.data,
+ publicOrderStatusResp.json(),
);
console.log(pubUnpaidStatus);
@@ -113,9 +109,7 @@ export async function runPayPaidTest(t: GlobalTestState) {
const proposalId = preparePayResp.proposalId;
- publicOrderStatusResp = await axios.get(publicOrderStatusUrl, {
- validateStatus: () => true,
- });
+ publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl);
if (publicOrderStatusResp.status != 402) {
throw Error(
@@ -124,7 +118,7 @@ export async function runPayPaidTest(t: GlobalTestState) {
}
pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
- publicOrderStatusResp.data,
+ publicOrderStatusResp.json(),
);
const confirmPayRes = await wallet.client.call(
@@ -136,14 +130,12 @@ export async function runPayPaidTest(t: GlobalTestState) {
t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done);
- publicOrderStatusResp = await axios.get(publicOrderStatusUrl, {
- validateStatus: () => true,
- });
+ publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl);
- console.log(publicOrderStatusResp.data);
+ console.log(publicOrderStatusResp.json());
if (publicOrderStatusResp.status != 200) {
- console.log(publicOrderStatusResp.data);
+ console.log(publicOrderStatusResp.json());
throw Error(
`expected status 200 (after paying), but got ${publicOrderStatusResp.status}`,
);
diff --git a/packages/taler-harness/src/integrationtests/test-payment-abort.ts b/packages/taler-harness/src/integrationtests/test-payment-abort.ts
index 40438c583..05ca7a543 100644
--- a/packages/taler-harness/src/integrationtests/test-payment-abort.ts
+++ b/packages/taler-harness/src/integrationtests/test-payment-abort.ts
@@ -17,14 +17,12 @@
/**
* Imports.
*/
-import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js";
+import { GlobalTestState, MerchantPrivateApi, harnessHttpLib } from "../harness/harness.js";
import {
withdrawViaBank,
createFaultInjectedMerchantTestkudosEnvironment,
} from "../harness/helpers.js";
-import {
- FaultInjectionRequestContext,
-} from "../harness/faultInjection.js";
+import { FaultInjectionRequestContext } from "../harness/faultInjection.js";
import {
codecForMerchantOrderStatusUnpaid,
ConfirmPayResultType,
@@ -35,9 +33,6 @@ import {
URL,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import axiosImp from "axios";
-
-const axios = axiosImp.default;
export async function runPaymentAbortTest(t: GlobalTestState) {
// Set up test environment
@@ -75,9 +70,7 @@ export async function runPaymentAbortTest(t: GlobalTestState) {
t.assertTrue(orderStatus.already_paid_order_id === undefined);
let publicOrderStatusUrl = orderStatus.order_status_url;
- let publicOrderStatusResp = await axios.get(publicOrderStatusUrl, {
- validateStatus: () => true,
- });
+ let publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl);
if (publicOrderStatusResp.status != 402) {
throw Error(
@@ -86,7 +79,7 @@ export async function runPaymentAbortTest(t: GlobalTestState) {
}
let pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
- publicOrderStatusResp.data,
+ publicOrderStatusResp.json(),
);
console.log(pubUnpaidStatus);
@@ -102,9 +95,7 @@ export async function runPaymentAbortTest(t: GlobalTestState) {
const proposalId = preparePayResp.proposalId;
- publicOrderStatusResp = await axios.get(publicOrderStatusUrl, {
- validateStatus: () => true,
- });
+ publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl);
if (publicOrderStatusResp.status != 402) {
throw Error(
@@ -113,7 +104,7 @@ export async function runPaymentAbortTest(t: GlobalTestState) {
}
pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
- publicOrderStatusResp.data,
+ publicOrderStatusResp.json(),
);
faultyMerchant.faultProxy.addFault({
diff --git a/packages/taler-harness/src/integrationtests/test-payment-claim.ts b/packages/taler-harness/src/integrationtests/test-payment-claim.ts
index eb219c1e7..3e52cb5dd 100644
--- a/packages/taler-harness/src/integrationtests/test-payment-claim.ts
+++ b/packages/taler-harness/src/integrationtests/test-payment-claim.ts
@@ -42,13 +42,15 @@ export async function runPaymentClaimTest(t: GlobalTestState) {
// Withdraw digital cash into the wallet.
- await withdrawViaBankV2(t, {
+ const wres = await withdrawViaBankV2(t, {
walletClient,
bank,
exchange,
amount: "TESTKUDOS:20",
});
+ await wres.withdrawalFinishedCond;
+
// Set up order.
const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
diff --git a/packages/taler-harness/src/integrationtests/test-payment-idempotency.ts b/packages/taler-harness/src/integrationtests/test-payment-idempotency.ts
index e16cf9dd1..6373c2393 100644
--- a/packages/taler-harness/src/integrationtests/test-payment-idempotency.ts
+++ b/packages/taler-harness/src/integrationtests/test-payment-idempotency.ts
@@ -37,13 +37,15 @@ export async function runPaymentIdempotencyTest(t: GlobalTestState) {
// Withdraw digital cash into the wallet.
- await withdrawViaBankV2(t, {
+ const wres = await withdrawViaBankV2(t, {
walletClient,
bank,
exchange,
amount: "TESTKUDOS:20",
});
+ await wres.withdrawalFinishedCond;
+
// Set up order.
const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
diff --git a/packages/taler-harness/src/integrationtests/test-payment-template.ts b/packages/taler-harness/src/integrationtests/test-payment-template.ts
index 172791648..707be52e1 100644
--- a/packages/taler-harness/src/integrationtests/test-payment-template.ts
+++ b/packages/taler-harness/src/integrationtests/test-payment-template.ts
@@ -50,7 +50,8 @@ export async function runPaymentTemplateTest(t: GlobalTestState) {
// Withdraw digital cash into the wallet.
- await withdrawViaBankV2(t, { walletClient, bank, exchange, amount: "TESTKUDOS:20" });
+ const wres = await withdrawViaBankV2(t, { walletClient, bank, exchange, amount: "TESTKUDOS:20" });
+ await wres.withdrawalFinishedCond;
// Request a template payment
diff --git a/packages/taler-harness/src/integrationtests/test-payment-transient.ts b/packages/taler-harness/src/integrationtests/test-payment-transient.ts
index 33a9716d5..c2a8e37c5 100644
--- a/packages/taler-harness/src/integrationtests/test-payment-transient.ts
+++ b/packages/taler-harness/src/integrationtests/test-payment-transient.ts
@@ -17,23 +17,22 @@
/**
* Imports.
*/
-import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js";
import {
- withdrawViaBank,
- createFaultInjectedMerchantTestkudosEnvironment,
-} from "../harness/helpers.js";
-import { FaultInjectionResponseContext } from "../harness/faultInjection.js";
-import {
- codecForMerchantOrderStatusUnpaid,
ConfirmPayResultType,
PreparePayResultType,
TalerErrorCode,
TalerErrorDetail,
URL,
+ codecForMerchantOrderStatusUnpaid,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import axiosImp from "axios";
-const axios = axiosImp.default;
+import { FaultInjectionResponseContext } from "../harness/faultInjection.js";
+import { GlobalTestState, MerchantPrivateApi, harnessHttpLib } from "../harness/harness.js";
+import {
+ createFaultInjectedMerchantTestkudosEnvironment,
+ withdrawViaBank,
+} from "../harness/helpers.js";
+
/**
* Run test for a payment where the merchant has a transient
@@ -75,9 +74,7 @@ export async function runPaymentTransientTest(t: GlobalTestState) {
t.assertTrue(orderStatus.already_paid_order_id === undefined);
let publicOrderStatusUrl = orderStatus.order_status_url;
- let publicOrderStatusResp = await axios.get(publicOrderStatusUrl, {
- validateStatus: () => true,
- });
+ let publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl);
if (publicOrderStatusResp.status != 402) {
throw Error(
@@ -86,7 +83,7 @@ export async function runPaymentTransientTest(t: GlobalTestState) {
}
let pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
- publicOrderStatusResp.data,
+ publicOrderStatusResp.json(),
);
console.log(pubUnpaidStatus);
@@ -102,9 +99,7 @@ export async function runPaymentTransientTest(t: GlobalTestState) {
const proposalId = preparePayResp.proposalId;
- publicOrderStatusResp = await axios.get(publicOrderStatusUrl, {
- validateStatus: () => true,
- });
+ publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl);
if (publicOrderStatusResp.status != 402) {
throw Error(
@@ -113,7 +108,7 @@ export async function runPaymentTransientTest(t: GlobalTestState) {
}
pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
- publicOrderStatusResp.data,
+ publicOrderStatusResp.json(),
);
let faultInjected = false;
@@ -165,14 +160,12 @@ export async function runPaymentTransientTest(t: GlobalTestState) {
// Now ask the merchant if paid
console.log("requesting", publicOrderStatusUrl);
- publicOrderStatusResp = await axios.get(publicOrderStatusUrl, {
- validateStatus: () => true,
- });
+ publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl);
- console.log(publicOrderStatusResp.data);
+ console.log(publicOrderStatusResp.json());
if (publicOrderStatusResp.status != 200) {
- console.log(publicOrderStatusResp.data);
+ console.log(publicOrderStatusResp.json());
throw Error(
`expected status 200 (after paying), but got ${publicOrderStatusResp.status}`,
);
diff --git a/packages/taler-harness/src/integrationtests/test-paywall-flow.ts b/packages/taler-harness/src/integrationtests/test-paywall-flow.ts
index b0477a049..5f63d4fac 100644
--- a/packages/taler-harness/src/integrationtests/test-paywall-flow.ts
+++ b/packages/taler-harness/src/integrationtests/test-paywall-flow.ts
@@ -17,7 +17,7 @@
/**
* Imports.
*/
-import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js";
+import { GlobalTestState, MerchantPrivateApi, harnessHttpLib } from "../harness/harness.js";
import {
PreparePayResultType,
codecForMerchantOrderStatusUnpaid,
@@ -29,12 +29,6 @@ import {
createSimpleTestkudosEnvironmentV2,
withdrawViaBankV2,
} from "../harness/helpers.js";
-import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
-
-const httpLib = createPlatformHttpLib({
- allowHttp: true,
- enableThrottling: false,
-});
/**
* Run test for basic, bank-integrated withdrawal.
@@ -47,13 +41,15 @@ export async function runPaywallFlowTest(t: GlobalTestState) {
// Withdraw digital cash into the wallet.
- await withdrawViaBankV2(t, {
+ const wres = await withdrawViaBankV2(t, {
walletClient,
bank,
exchange,
amount: "TESTKUDOS:20",
});
+ await wres.withdrawalFinishedCond;
+
/**
* =========================================================================
* Create an order and let the wallet pay under a session ID
@@ -86,7 +82,7 @@ export async function runPaywallFlowTest(t: GlobalTestState) {
t.assertTrue(orderStatus.already_paid_order_id === undefined);
let publicOrderStatusUrl = new URL(orderStatus.order_status_url);
- let publicOrderStatusResp = await httpLib.fetch(publicOrderStatusUrl.href);
+ let publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl.href);
if (publicOrderStatusResp.status != 402) {
throw Error(
@@ -112,7 +108,7 @@ export async function runPaywallFlowTest(t: GlobalTestState) {
const proposalId = preparePayResp.proposalId;
console.log("requesting", publicOrderStatusUrl.href);
- publicOrderStatusResp = await httpLib.fetch(publicOrderStatusUrl.href);
+ publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl.href);
console.log("response body", publicOrderStatusResp.json());
if (publicOrderStatusResp.status != 402) {
throw Error(
@@ -129,7 +125,7 @@ export async function runPaywallFlowTest(t: GlobalTestState) {
});
t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done);
- publicOrderStatusResp = await httpLib.fetch(publicOrderStatusUrl.href);
+ publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl.href);
console.log(publicOrderStatusResp.json());
@@ -231,7 +227,7 @@ export async function runPaywallFlowTest(t: GlobalTestState) {
console.log("requesting public status", publicOrderStatusUrl);
// Ask the order status of the claimed-but-unpaid order
- publicOrderStatusResp = await httpLib.fetch(publicOrderStatusUrl.href);
+ publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl.href);
if (publicOrderStatusResp.status != 402) {
throw Error(`expected status 402, but got ${publicOrderStatusResp.status}`);
diff --git a/packages/taler-harness/src/integrationtests/test-refund-auto.ts b/packages/taler-harness/src/integrationtests/test-refund-auto.ts
index 607080e68..5648835d5 100644
--- a/packages/taler-harness/src/integrationtests/test-refund-auto.ts
+++ b/packages/taler-harness/src/integrationtests/test-refund-auto.ts
@@ -36,13 +36,15 @@ export async function runRefundAutoTest(t: GlobalTestState) {
// Withdraw digital cash into the wallet.
- await withdrawViaBankV2(t, {
+ const wres = await withdrawViaBankV2(t, {
walletClient,
bank,
exchange,
amount: "TESTKUDOS:20",
});
+ await wres.withdrawalFinishedCond;
+
// Set up order.
const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
order: {
diff --git a/packages/taler-harness/src/integrationtests/test-refund-incremental.ts b/packages/taler-harness/src/integrationtests/test-refund-incremental.ts
index 8e7e38b71..8ac0948f2 100644
--- a/packages/taler-harness/src/integrationtests/test-refund-incremental.ts
+++ b/packages/taler-harness/src/integrationtests/test-refund-incremental.ts
@@ -45,13 +45,15 @@ export async function runRefundIncrementalTest(t: GlobalTestState) {
// Withdraw digital cash into the wallet.
- await withdrawViaBankV2(t, {
+ const wres = await withdrawViaBankV2(t, {
walletClient,
bank,
exchange,
amount: "TESTKUDOS:20",
});
+ await wres.withdrawalFinishedCond;
+
// Set up order.
const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-balance.ts b/packages/taler-harness/src/integrationtests/test-wallet-balance.ts
index 0f75bd96e..15b0fd427 100644
--- a/packages/taler-harness/src/integrationtests/test-wallet-balance.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-balance.ts
@@ -17,22 +17,11 @@
/**
* Imports.
*/
-import { Amounts, Duration, PreparePayResultType } from "@gnu-taler/taler-util";
+import { Amounts, PreparePayResultType } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
-import {
- ExchangeService,
- FakebankService,
- getRandomIban,
- GlobalTestState,
- MerchantPrivateApi,
- MerchantService,
- setupDb,
- WalletCli,
-} from "../harness/harness.js";
+import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js";
import {
createSimpleTestkudosEnvironmentV2,
- withdrawViaBank,
withdrawViaBankV2,
} from "../harness/helpers.js";
@@ -50,13 +39,15 @@ export async function runWalletBalanceTest(t: GlobalTestState) {
// Withdraw digital cash into the wallet.
- await withdrawViaBankV2(t, {
+ const wres = await withdrawViaBankV2(t, {
walletClient,
bank,
exchange,
amount: "TESTKUDOS:20",
});
+ await wres.withdrawalFinishedCond;
+
const order = {
summary: "Buy me!",
amount: "TESTKUDOS:5",
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-dbless.ts b/packages/taler-harness/src/integrationtests/test-wallet-dbless.ts
index 58f564f34..153ae93d8 100644
--- a/packages/taler-harness/src/integrationtests/test-wallet-dbless.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-dbless.ts
@@ -23,7 +23,6 @@ import {
j2s,
TalerError,
} from "@gnu-taler/taler-util";
-import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
import {
checkReserve,
CryptoDispatcher,
@@ -36,7 +35,7 @@ import {
Wallet,
withdrawCoin,
} from "@gnu-taler/taler-wallet-core";
-import { GlobalTestState } from "../harness/harness.js";
+import { GlobalTestState, harnessHttpLib } from "../harness/harness.js";
import { createSimpleTestkudosEnvironmentV2 } from "../harness/helpers.js";
/**
@@ -47,10 +46,7 @@ export async function runWalletDblessTest(t: GlobalTestState) {
const { bank, exchange } = await createSimpleTestkudosEnvironmentV2(t);
- const http = createPlatformHttpLib({
- allowHttp: true,
- enableThrottling: false,
- });
+ const http = harnessHttpLib;
const cryptiDisp = new CryptoDispatcher(
new SynchronousCryptoWorkerFactoryPlain(),
);
diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts
index cbdca04b9..501af98a4 100644
--- a/packages/taler-harness/src/integrationtests/testrunner.ts
+++ b/packages/taler-harness/src/integrationtests/testrunner.ts
@@ -14,7 +14,12 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { CancellationToken, Logger, minimatch, setGlobalLogLevelFromString } from "@gnu-taler/taler-util";
+import {
+ CancellationToken,
+ Logger,
+ minimatch,
+ setGlobalLogLevelFromString,
+} from "@gnu-taler/taler-util";
import * as child_process from "child_process";
import * as fs from "fs";
import * as os from "os";
@@ -22,10 +27,12 @@ import * as path from "path";
import url from "url";
import {
GlobalTestState,
+ runCommand,
runTestWithState,
shouldLingerInTest,
TestRunResult,
} from "../harness/harness.js";
+import { spawnSync } from "child_process";
import { runAgeRestrictionsMerchantTest } from "./test-age-restrictions-merchant.js";
import { runBankApiTest } from "./test-bank-api.js";
import { runClaimLoopTest } from "./test-claim-loop.js";
@@ -105,6 +112,8 @@ import { runPeerRepairTest } from "./test-peer-repair.js";
import { runPaymentShareTest } from "./test-payment-share.js";
import { runSimplePaymentTest } from "./test-simple-payment.js";
import { runTermOfServiceFormatTest } from "./test-tos-format.js";
+import { runExchangePurseTest } from "./test-exchange-purse.js";
+import { getSharedTestDir } from "../harness/helpers.js";
/**
* Test runner.
@@ -137,6 +146,7 @@ const allTests: TestMainFunction[] = [
runFeeRegressionTest,
runForcedSelectionTest,
runKycTest,
+ runExchangePurseTest,
runExchangeDepositTest,
runLibeufinAnastasisFacadeTest,
runLibeufinApiBankaccountTest,
@@ -257,7 +267,37 @@ interface RunTestChildInstruction {
testRootDir: string;
}
+function purgeSharedTestEnvironment() {
+ const rmRes = spawnSync("rm", ["-rf", `${getSharedTestDir()}`]);
+ if (rmRes.status != 0) {
+ logger.warn("can't delete shared test directory");
+ }
+ const psqlRes = spawnSync("psql", ["-Aqtl"], {
+ encoding: "utf-8",
+ });
+ if (psqlRes.status != 0) {
+ logger.warn("could not list available postgres databases");
+ return;
+ }
+ if (psqlRes.output[1]!!.indexOf("taler-integrationtest-shared") >= 0) {
+ const dropRes = spawnSync("dropdb", ["taler-integrationtest-shared"], {
+ encoding: "utf-8",
+ });
+ if (dropRes.status != 0) {
+ logger.warn("could not drop taler-integrationtest-shared database");
+ return;
+ }
+ }
+}
+
export async function runTests(spec: TestRunSpec) {
+ if (!process.env.TALER_HARNESS_KEEP) {
+ logger.info("purging shared test environment");
+ purgeSharedTestEnvironment();
+ } else {
+ logger.info("keeping shared test environment");
+ }
+
const testRootDir = fs.mkdtempSync(
path.join(os.tmpdir(), "taler-integrationtests-"),
);
diff --git a/packages/taler-harness/src/lint.ts b/packages/taler-harness/src/lint.ts
index f13049710..6d8e679db 100644
--- a/packages/taler-harness/src/lint.ts
+++ b/packages/taler-harness/src/lint.ts
@@ -407,7 +407,7 @@ export async function checkExchangeHttpd(
{
const mgmtUrl = new URL("management/keys", baseUrl);
- const resp = await httpLib.get(mgmtUrl.href);
+ const resp = await httpLib.fetch(mgmtUrl.href);
const futureKeys = await readSuccessResponseJsonOrThrow(
resp,
@@ -431,7 +431,7 @@ export async function checkExchangeHttpd(
{
const keysUrl = new URL("keys", baseUrl);
- const resp = await Promise.race([httpLib.get(keysUrl.href), delayMs(2000)]);
+ const resp = await Promise.race([httpLib.fetch(keysUrl.href), delayMs(2000)]);
if (!resp) {
context.numErr++;
@@ -467,7 +467,7 @@ export async function checkExchangeHttpd(
{
const keysUrl = new URL("wire", baseUrl);
- const resp = await Promise.race([httpLib.get(keysUrl.href), delayMs(2000)]);
+ const resp = await Promise.race([httpLib.fetch(keysUrl.href), delayMs(2000)]);
if (!resp) {
context.numErr++;
diff --git a/packages/taler-util/package.json b/packages/taler-util/package.json
index 504b8259f..6ac9a2689 100644
--- a/packages/taler-util/package.json
+++ b/packages/taler-util/package.json
@@ -57,7 +57,7 @@
}
},
"scripts": {
- "compile": "tsc --build",
+ "compile": "tsc",
"test": "tsc && ava",
"clean": "rimraf dist lib tsconfig.tsbuildinfo",
"pretty": "prettier --write src"
diff --git a/packages/taler-util/src/http-common.ts b/packages/taler-util/src/http-common.ts
index 4f6aaaf44..93cf9bba0 100644
--- a/packages/taler-util/src/http-common.ts
+++ b/packages/taler-util/src/http-common.ts
@@ -19,7 +19,12 @@
import { CancellationToken } from "./CancellationToken.js";
import { Codec } from "./codec.js";
import { j2s } from "./helpers.js";
-import { TalerError, makeErrorDetail } from "./index.js";
+import {
+ TalerError,
+ base64FromArrayBuffer,
+ makeErrorDetail,
+ stringToBytes,
+} from "./index.js";
import { Logger } from "./logging.js";
import { TalerErrorCode } from "./taler-error-codes.js";
import { Duration, AbsoluteTime } from "./time.js";
@@ -306,6 +311,16 @@ export async function readSuccessResponseJsonOrThrow<T>(
throwUnexpectedRequestError(httpResponse, r.talerErrorResponse);
}
+export async function expectSuccessResponseOrThrow<T>(
+ httpResponse: HttpResponse,
+): Promise<void> {
+ if (httpResponse.status >= 200 && httpResponse.status <= 299) {
+ return;
+ }
+ const errResp = await readTalerErrorResponse(httpResponse);
+ throwUnexpectedRequestError(httpResponse, errResp);
+}
+
export async function readSuccessResponseTextOrErrorCode<T>(
httpResponse: HttpResponse,
): Promise<ResponseOrError<string>> {
@@ -452,3 +467,15 @@ export function getDefaultHeaders(method: string): Record<string, string> {
return headers;
}
+
+/**
+ * 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}`;
+}
diff --git a/packages/taler-util/src/payto.ts b/packages/taler-util/src/payto.ts
index dd35b44be..2b0af4cc2 100644
--- a/packages/taler-util/src/payto.ts
+++ b/packages/taler-util/src/payto.ts
@@ -239,3 +239,25 @@ export function parsePaytoUri(s: string): PaytoUri | undefined {
isKnown: false,
};
}
+
+export function talerPaytoFromExchangeReserve(
+ exchangeBaseUrl: string,
+ reservePub: string,
+): string {
+ const url = new URL(exchangeBaseUrl);
+ let proto: string;
+ if (url.protocol === "http:") {
+ proto = "taler-reserve-http";
+ } else if (url.protocol === "https:") {
+ proto = "taler-reserve";
+ } else {
+ throw Error(`unsupported exchange base URL protocol (${url.protocol})`);
+ }
+
+ let path = url.pathname;
+ if (!path.endsWith("/")) {
+ path = path + "/";
+ }
+
+ return `payto://${proto}/${url.host}${url.pathname}${reservePub}`;
+}
diff --git a/packages/taler-util/src/taler-crypto.ts b/packages/taler-util/src/taler-crypto.ts
index 396ac89e1..cc9c706ba 100644
--- a/packages/taler-util/src/taler-crypto.ts
+++ b/packages/taler-util/src/taler-crypto.ts
@@ -1004,7 +1004,7 @@ export enum TalerSignaturePurpose {
SYNC_BACKUP_UPLOAD = 1450,
}
-export const enum WalletAccountMergeFlags {
+export enum WalletAccountMergeFlags {
/**
* Not a legal mode!
*/
@@ -1260,7 +1260,8 @@ export namespace AgeRestriction {
}
const PublishedAgeRestrictionBaseKey: Edx25519PublicKey = decodeCrock(
- "CH0VKFDZ2GWRWHQBBGEK9MWV5YDQVJ0RXEE0KYT3NMB69F0R96TG");
+ "CH0VKFDZ2GWRWHQBBGEK9MWV5YDQVJ0RXEE0KYT3NMB69F0R96TG",
+ );
export async function restrictionCommitSeeded(
ageMask: number,
diff --git a/packages/taler-util/src/taler-types.ts b/packages/taler-util/src/taler-types.ts
index 178da87da..17900129c 100644
--- a/packages/taler-util/src/taler-types.ts
+++ b/packages/taler-util/src/taler-types.ts
@@ -25,7 +25,7 @@
* Imports.
*/
-import { codecForAmountString } from "./amounts.js";
+import { Amounts, codecForAmountString } from "./amounts.js";
import {
buildCodecForObject,
buildCodecForUnion,
@@ -719,16 +719,12 @@ export class ExchangeSignKeyJson {
* Structure that the exchange gives us in /keys.
*/
export class ExchangeKeysJson {
-
/**
* Canonical, public base URL of the exchange.
*/
base_url: string;
- /**
- * List of offered denominations.
- */
- denoms: ExchangeDenomination[];
+ currency: string;
/**
* The exchange's master public key.
@@ -764,6 +760,111 @@ export class ExchangeKeysJson {
reserve_closing_delay: TalerProtocolDuration;
global_fees: GlobalFees[];
+
+ accounts: AccountInfo[];
+
+ wire_fees: { [methodName: string]: WireFeesJson[] };
+
+ denominations: DenomGroup[];
+}
+
+export type DenomGroup =
+ | DenomGroupRsa
+ | DenomGroupCs
+ | DenomGroupRsaAgeRestricted
+ | DenomGroupCsAgeRestricted;
+
+export interface DenomGroupCommon {
+ // How much are coins of this denomination worth?
+ value: AmountString;
+
+ // Fee charged by the exchange for withdrawing a coin of this denomination.
+ fee_withdraw: AmountString;
+
+ // Fee charged by the exchange for depositing a coin of this denomination.
+ fee_deposit: AmountString;
+
+ // Fee charged by the exchange for refreshing a coin of this denomination.
+ fee_refresh: AmountString;
+
+ // Fee charged by the exchange for refunding a coin of this denomination.
+ fee_refund: AmountString;
+
+ // XOR of all the SHA-512 hash values of the denominations' public keys
+ // in this group. Note that for hashing, the binary format of the
+ // public keys is used, and not their base32 encoding.
+ hash: HashCodeString;
+}
+
+export interface DenomCommon {
+ // Signature of TALER_DenominationKeyValidityPS.
+ master_sig: EddsaSignatureString;
+
+ // When does the denomination key become valid?
+ stamp_start: TalerProtocolTimestamp;
+
+ // When is it no longer possible to deposit coins
+ // of this denomination?
+ stamp_expire_withdraw: TalerProtocolTimestamp;
+
+ // Timestamp indicating by when legal disputes relating to these coins must
+ // be settled, as the exchange will afterwards destroy its evidence relating to
+ // transactions involving this coin.
+ stamp_expire_legal: TalerProtocolTimestamp;
+
+ stamp_expire_deposit: TalerProtocolTimestamp;
+
+ // Set to 'true' if the exchange somehow "lost"
+ // the private key. The denomination was not
+ // necessarily revoked, but still cannot be used
+ // to withdraw coins at this time (theoretically,
+ // the private key could be recovered in the
+ // future; coins signed with the private key
+ // remain valid).
+ lost?: boolean;
+}
+
+export type RsaPublicKeySring = string;
+export type AgeMask = number;
+
+/**
+ * 32-byte value representing a point on Curve25519.
+ */
+export type Cs25519Point = string;
+
+export interface DenomGroupRsa extends DenomGroupCommon {
+ cipher: "RSA";
+
+ denoms: ({
+ rsa_pub: RsaPublicKeySring;
+ } & DenomCommon)[];
+}
+
+export interface DenomGroupRsaAgeRestricted extends DenomGroupCommon {
+ cipher: "RSA+age_restricted";
+ age_mask: AgeMask;
+
+ denoms: ({
+ rsa_pub: RsaPublicKeySring;
+ } & DenomCommon)[];
+}
+
+export interface DenomGroupCs extends DenomGroupCommon {
+ cipher: "CS";
+ age_mask: AgeMask;
+
+ denoms: ({
+ cs_pub: Cs25519Point;
+ } & DenomCommon)[];
+}
+
+export interface DenomGroupCsAgeRestricted extends DenomGroupCommon {
+ cipher: "CS+age_restricted";
+ age_mask: AgeMask;
+
+ denoms: ({
+ cs_pub: Cs25519Point;
+ } & DenomCommon)[];
}
export interface GlobalFees {
@@ -847,10 +948,10 @@ export interface AccountInfo {
debit_restrictions?: any;
}
-export interface ExchangeWireJson {
- accounts: AccountInfo[];
- fees: { [methodName: string]: WireFeesJson[] };
-}
+/**
+ * @deprecated
+ */
+export interface ExchangeWireJson {}
/**
* Proposal returned from the contract URL.
@@ -1404,10 +1505,13 @@ export const codecForGlobalFees = (): Codec<GlobalFees> =>
.property("master_sig", codecForString())
.build("GlobalFees");
+// FIXME: Validate properly!
+export const codecForNgDenominations: Codec<DenomGroup> = codecForAny();
+
export const codecForExchangeKeysJson = (): Codec<ExchangeKeysJson> =>
buildCodecForObject<ExchangeKeysJson>()
- .property("denoms", codecForList(codecForDenomination()))
.property("base_url", codecForString())
+ .property("currency", codecForString())
.property("master_public_key", codecForString())
.property("auditors", codecForList(codecForAuditor()))
.property("list_issue_date", codecForTimestamp)
@@ -1416,6 +1520,9 @@ export const codecForExchangeKeysJson = (): Codec<ExchangeKeysJson> =>
.property("version", codecForString())
.property("reserve_closing_delay", codecForDuration)
.property("global_fees", codecForList(codecForGlobalFees()))
+ .property("accounts", codecForList(codecForAccountInfo()))
+ .property("wire_fees", codecForMap(codecForList(codecForWireFeesJson())))
+ .property("denominations", codecForList(codecForNgDenominations))
.build("ExchangeKeysJson");
export const codecForWireFeesJson = (): Codec<WireFeesJson> =>
@@ -1436,12 +1543,6 @@ export const codecForAccountInfo = (): Codec<AccountInfo> =>
.property("debit_restrictions", codecForAny())
.build("AccountInfo");
-export const codecForExchangeWireJson = (): Codec<ExchangeWireJson> =>
- buildCodecForObject<ExchangeWireJson>()
- .property("accounts", codecForList(codecForAccountInfo()))
- .property("fees", codecForMap(codecForList(codecForWireFeesJson())))
- .build("ExchangeWireJson");
-
export const codecForProposal = (): Codec<Proposal> =>
buildCodecForObject<Proposal>()
.property("contract_terms", codecForAny())
diff --git a/packages/taler-wallet-cli/package.json b/packages/taler-wallet-cli/package.json
index 36201759e..06df1da76 100644
--- a/packages/taler-wallet-cli/package.json
+++ b/packages/taler-wallet-cli/package.json
@@ -16,7 +16,7 @@
},
"type": "module",
"scripts": {
- "compile": "tsc --build && ./build-node.mjs",
+ "compile": "tsc && ./build-node.mjs",
"test": "tsc",
"clean": "rimraf lib dist tsconfig.tsbuildinfo",
"pretty": "prettier --write src"
@@ -41,4 +41,4 @@
"@gnu-taler/taler-wallet-core": "workspace:*",
"tslib": "^2.5.3"
}
-} \ No newline at end of file
+}
diff --git a/packages/taler-wallet-core/package.json b/packages/taler-wallet-core/package.json
index 25e7c2eb2..9cd6f6466 100644
--- a/packages/taler-wallet-core/package.json
+++ b/packages/taler-wallet-core/package.json
@@ -12,7 +12,7 @@
"author": "Florian Dold",
"license": "GPL-3.0",
"scripts": {
- "compile": "tsc --build",
+ "compile": "tsc",
"pretty": "prettier --write src",
"test": "tsc && ava",
"coverage": "tsc && c8 --src src --all ava",
@@ -72,7 +72,6 @@
"@gnu-taler/idb-bridge": "workspace:*",
"@gnu-taler/taler-util": "workspace:*",
"@types/node": "^18.11.17",
- "axios": "^0.27.2",
"big-integer": "^1.6.51",
"fflate": "^0.7.4",
"tslib": "^2.5.3"
diff --git a/packages/taler-wallet-core/src/bank-api-client.ts b/packages/taler-wallet-core/src/bank-api-client.ts
index 8e351cb48..3174667f1 100644
--- a/packages/taler-wallet-core/src/bank-api-client.ts
+++ b/packages/taler-wallet-core/src/bank-api-client.ts
@@ -224,7 +224,7 @@ export namespace BankAccessApi {
`accounts/${bankUser.username}`,
bank.bankAccessApiBaseUrl,
);
- const resp = await bank.http.get(url.href, {
+ const resp = await bank.http.fetch(url.href, {
headers: {
Authorization: makeBasicAuthHeader(
bankUser.username,
diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
index 01d2677dc..c1a761fb6 100644
--- a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
+++ b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
@@ -723,7 +723,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
maybeAcp = await AgeRestriction.restrictionCommitSeeded(
denomPub.age_mask,
age,
- stringToBytes(req.secretSeed)
+ stringToBytes(req.secretSeed),
);
maybeAgeCommitmentHash = AgeRestriction.hashCommitment(
maybeAcp.commitment,
@@ -1127,6 +1127,8 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
// All zeros.
hAgeCommitment = new Uint8Array(32);
}
+ // FIXME: Actually allow passing user data here!
+ const walletDataHash = new Uint8Array(64);
let d: Uint8Array;
if (depositInfo.denomKeyType === DenomKeyType.Rsa) {
d = buildSigPS(TalerSignaturePurpose.WALLET_COIN_DEPOSIT)
@@ -1140,6 +1142,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
.put(amountToBuffer(depositInfo.spendAmount))
.put(amountToBuffer(depositInfo.feeDeposit))
.put(decodeCrock(depositInfo.merchantPub))
+ .put(walletDataHash)
.build();
} else {
throw Error("unsupported exchange protocol version");
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index 34beb6412..2da1e7913 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -352,6 +352,7 @@ export interface DenomFees {
export interface DenominationRecord {
currency: string;
+ // FIXME: Use binary encoding of amount instead?
amountVal: number;
amountFrac: number;
diff --git a/packages/taler-wallet-core/src/dbless.ts b/packages/taler-wallet-core/src/dbless.ts
index 4dfdff3f7..5532345ae 100644
--- a/packages/taler-wallet-core/src/dbless.ts
+++ b/packages/taler-wallet-core/src/dbless.ts
@@ -137,7 +137,7 @@ export async function topupReserveWithDemobank(
throw Error("no suggested exchange");
}
const plainPaytoUris =
- exchangeInfo.wire.accounts.map((x) => x.payto_uri) ?? [];
+ exchangeInfo.keys.accounts.map((x) => x.payto_uri) ?? [];
if (plainPaytoUris.length <= 0) {
throw new Error();
}
@@ -338,7 +338,10 @@ export async function refreshCoin(req: {
logger.info("requesting melt done");
- const meltHttpResp = await http.postJson(meltReqUrl.href, meltReqBody);
+ const meltHttpResp = await http.fetch(meltReqUrl.href, {
+ method: "POST",
+ body: meltReqBody,
+ });
const meltResponse = await readSuccessResponseJsonOrThrow(
meltHttpResp,
@@ -386,7 +389,7 @@ export async function createFakebankReserve(args: {
exchangeInfo: ExchangeInfo;
}): Promise<void> {
const { http, fakebankBaseUrl, amount, reservePub } = args;
- const paytoUri = args.exchangeInfo.wire.accounts[0].payto_uri;
+ const paytoUri = args.exchangeInfo.keys.accounts[0].payto_uri;
const pt = parsePaytoUri(paytoUri);
if (!pt) {
throw Error("failed to parse payto URI");
diff --git a/packages/taler-wallet-core/src/dev-experiments.ts b/packages/taler-wallet-core/src/dev-experiments.ts
index 113e9bede..176ed09d9 100644
--- a/packages/taler-wallet-core/src/dev-experiments.ts
+++ b/packages/taler-wallet-core/src/dev-experiments.ts
@@ -70,7 +70,7 @@ export class DevExperimentHttpLib implements HttpRequestLibrary {
opt?: HttpRequestOptions | undefined,
): Promise<HttpResponse> {
logger.trace(`devexperiment httplib ${url}`);
- return this.underlyingLib.get(url, opt);
+ return this.underlyingLib.fetch(url, opt);
}
postJson(
diff --git a/packages/taler-wallet-core/src/index.ts b/packages/taler-wallet-core/src/index.ts
index 8dd06fe2b..d64f7d5e6 100644
--- a/packages/taler-wallet-core/src/index.ts
+++ b/packages/taler-wallet-core/src/index.ts
@@ -51,12 +51,8 @@ export * from "./operations/refresh.js";
export * from "./dbless.js";
-export {
- nativeCryptoR,
- nativeCrypto,
- nullCrypto,
- TalerCryptoInterface,
-} from "./crypto/cryptoImplementation.js";
+export * from "./crypto/cryptoTypes.js";
+export * from "./crypto/cryptoImplementation.js";
export * from "./util/timer.js";
export * from "./util/denominations.js";
diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts b/packages/taler-wallet-core/src/operations/backup/index.ts
index 236ef1e0f..e35765165 100644
--- a/packages/taler-wallet-core/src/operations/backup/index.ts
+++ b/packages/taler-wallet-core/src/operations/backup/index.ts
@@ -661,7 +661,7 @@ export async function addBackupProvider(
}
});
const termsUrl = new URL("config", canonUrl);
- const resp = await ws.http.get(termsUrl.href);
+ const resp = await ws.http.fetch(termsUrl.href);
const terms = await readSuccessResponseJsonOrThrow(
resp,
codecForSyncTermsOfServiceResponse(),
diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts
index 8bf70fa27..c6b46e360 100644
--- a/packages/taler-wallet-core/src/operations/exchanges.ts
+++ b/packages/taler-wallet-core/src/operations/exchanges.ts
@@ -19,12 +19,14 @@
*/
import {
AbsoluteTime,
+ AccountInfo,
Amounts,
CancellationToken,
canonicalizeBaseUrl,
codecForExchangeKeysJson,
- codecForExchangeWireJson,
+ DenomGroup,
DenominationPubKey,
+ DenomKeyType,
Duration,
durationFromSpec,
encodeCrock,
@@ -51,6 +53,7 @@ import {
URL,
WireFee,
WireFeeMap,
+ WireFeesJson,
WireInfo,
} from "@gnu-taler/taler-util";
import {
@@ -84,43 +87,6 @@ import {
const logger = new Logger("exchanges.ts");
-function denominationRecordFromKeys(
- exchangeBaseUrl: string,
- exchangeMasterPub: string,
- listIssueDate: TalerProtocolTimestamp,
- denomIn: ExchangeDenomination,
-): DenominationRecord {
- let denomPub: DenominationPubKey;
- denomPub = denomIn.denom_pub;
- const denomPubHash = encodeCrock(hashDenomPub(denomPub));
- const value = Amounts.parseOrThrow(denomIn.value);
- const d: DenominationRecord = {
- denomPub,
- denomPubHash,
- exchangeBaseUrl,
- exchangeMasterPub,
- fees: {
- feeDeposit: Amounts.stringify(denomIn.fee_deposit),
- feeRefresh: Amounts.stringify(denomIn.fee_refresh),
- feeRefund: Amounts.stringify(denomIn.fee_refund),
- feeWithdraw: Amounts.stringify(denomIn.fee_withdraw),
- },
- isOffered: true,
- isRevoked: false,
- masterSig: denomIn.master_sig,
- stampExpireDeposit: denomIn.stamp_expire_deposit,
- stampExpireLegal: denomIn.stamp_expire_legal,
- stampExpireWithdraw: denomIn.stamp_expire_withdraw,
- stampStart: denomIn.stamp_start,
- verificationStatus: DenominationVerificationStatus.Unverified,
- amountFrac: value.fraction,
- amountVal: value.value,
- currency: value.currency,
- listIssueDate,
- };
- return d;
-}
-
export function getExchangeRequestTimeout(): Duration {
return Duration.fromSpec({
seconds: 5,
@@ -145,7 +111,7 @@ export async function downloadExchangeWithTermsOfService(
Accept: contentType,
};
- const resp = await http.get(reqUrl.href, {
+ const resp = await http.fetch(reqUrl.href, {
headers,
timeout,
});
@@ -241,7 +207,7 @@ export async function acceptExchangeTermsOfService(
async function validateWireInfo(
ws: InternalWalletState,
versionCurrent: number,
- wireInfo: ExchangeWireJson,
+ wireInfo: ExchangeKeysDownloadResult,
masterPublicKey: string,
): Promise<WireInfo> {
for (const a of wireInfo.accounts) {
@@ -267,9 +233,9 @@ async function validateWireInfo(
}
logger.trace("account validation done");
const feesForType: WireFeeMap = {};
- for (const wireMethod of Object.keys(wireInfo.fees)) {
+ for (const wireMethod of Object.keys(wireInfo.wireFees)) {
const feeList: WireFee[] = [];
- for (const x of wireInfo.fees[wireMethod]) {
+ for (const x of wireInfo.wireFees[wireMethod]) {
const startStamp = x.start_date;
const endStamp = x.end_date;
const fee: WireFee = {
@@ -343,7 +309,6 @@ async function validateGlobalFees(
}
export interface ExchangeInfo {
- wire: ExchangeWireJson;
keys: ExchangeKeysDownloadResult;
}
@@ -351,11 +316,6 @@ export async function downloadExchangeInfo(
exchangeBaseUrl: string,
http: HttpRequestLibrary,
): Promise<ExchangeInfo> {
- const wireInfo = await downloadExchangeWireInfo(
- exchangeBaseUrl,
- http,
- Duration.getForever(),
- );
const keysInfo = await downloadExchangeKeysInfo(
exchangeBaseUrl,
http,
@@ -363,33 +323,9 @@ export async function downloadExchangeInfo(
);
return {
keys: keysInfo,
- wire: wireInfo,
};
}
-/**
- * Fetch wire information for an exchange.
- *
- * @param exchangeBaseUrl Exchange base URL, assumed to be already normalized.
- */
-async function downloadExchangeWireInfo(
- exchangeBaseUrl: string,
- http: HttpRequestLibrary,
- timeout: Duration,
-): Promise<ExchangeWireJson> {
- const reqUrl = new URL("wire", exchangeBaseUrl);
-
- const resp = await http.get(reqUrl.href, {
- timeout,
- });
- const wireInfo = await readSuccessResponseJsonOrThrow(
- resp,
- codecForExchangeWireJson(),
- );
-
- return wireInfo;
-}
-
export async function provideExchangeRecordInTx(
ws: InternalWalletState,
tx: GetReadWriteAccess<{
@@ -434,6 +370,8 @@ interface ExchangeKeysDownloadResult {
recoup: Recoup[];
listIssueDate: TalerProtocolTimestamp;
globalFees: GlobalFees[];
+ accounts: AccountInfo[];
+ wireFees: { [methodName: string]: WireFeesJson[] };
}
/**
@@ -446,7 +384,7 @@ async function downloadExchangeKeysInfo(
): Promise<ExchangeKeysDownloadResult> {
const keysUrl = new URL("keys", baseUrl);
- const resp = await http.get(keysUrl.href, {
+ const resp = await http.fetch(keysUrl.href, {
timeout,
});
const exchangeKeysJsonUnchecked = await readSuccessResponseJsonOrThrow(
@@ -454,7 +392,7 @@ async function downloadExchangeKeysInfo(
codecForExchangeKeysJson(),
);
- if (exchangeKeysJsonUnchecked.denoms.length === 0) {
+ if (exchangeKeysJsonUnchecked.denominations.length === 0) {
throw TalerError.fromDetail(
TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT,
{
@@ -481,23 +419,72 @@ async function downloadExchangeKeysInfo(
);
}
- const currency = Amounts.parseOrThrow(
- exchangeKeysJsonUnchecked.denoms[0].value,
- ).currency.toUpperCase();
+ const currency = exchangeKeysJsonUnchecked.currency;
+
+ const currentDenominations: DenominationRecord[] = [];
+
+ for (const denomGroup of exchangeKeysJsonUnchecked.denominations) {
+ switch (denomGroup.cipher) {
+ case "RSA":
+ case "RSA+age_restricted": {
+ let ageMask = 0;
+ if (denomGroup.cipher === "RSA+age_restricted") {
+ ageMask = denomGroup.age_mask;
+ }
+ for (const denomIn of denomGroup.denoms) {
+ const denomPub: DenominationPubKey = {
+ age_mask: ageMask,
+ cipher: DenomKeyType.Rsa,
+ rsa_public_key: denomIn.rsa_pub,
+ };
+ const denomPubHash = encodeCrock(hashDenomPub(denomPub));
+ const value = Amounts.parseOrThrow(denomGroup.value);
+ const rec: DenominationRecord = {
+ denomPub,
+ denomPubHash,
+ exchangeBaseUrl: baseUrl,
+ exchangeMasterPub: exchangeKeysJsonUnchecked.master_public_key,
+ isOffered: true,
+ isRevoked: false,
+ amountFrac: value.fraction,
+ amountVal: value.value,
+ currency: value.currency,
+ stampExpireDeposit: denomIn.stamp_expire_deposit,
+ stampExpireLegal: denomIn.stamp_expire_legal,
+ stampExpireWithdraw: denomIn.stamp_expire_withdraw,
+ stampStart: denomIn.stamp_start,
+ verificationStatus: DenominationVerificationStatus.Unverified,
+ masterSig: denomIn.master_sig,
+ listIssueDate: exchangeKeysJsonUnchecked.list_issue_date,
+ fees: {
+ feeDeposit: Amounts.stringify(denomGroup.fee_deposit),
+ feeRefresh: Amounts.stringify(denomGroup.fee_refresh),
+ feeRefund: Amounts.stringify(denomGroup.fee_refund),
+ feeWithdraw: Amounts.stringify(denomGroup.fee_withdraw),
+ },
+ };
+ currentDenominations.push(rec);
+ }
+ break;
+ }
+ case "CS+age_restricted":
+ case "CS":
+ logger.warn("Clause-Schnorr denominations not supported");
+ continue;
+ default:
+ logger.warn(
+ `denomination type ${(denomGroup as any).cipher} not supported`,
+ );
+ continue;
+ }
+ }
return {
masterPublicKey: exchangeKeysJsonUnchecked.master_public_key,
currency,
baseUrl: exchangeKeysJsonUnchecked.base_url,
auditors: exchangeKeysJsonUnchecked.auditors,
- currentDenominations: exchangeKeysJsonUnchecked.denoms.map((d) =>
- denominationRecordFromKeys(
- baseUrl,
- exchangeKeysJsonUnchecked.master_public_key,
- exchangeKeysJsonUnchecked.list_issue_date,
- d,
- ),
- ),
+ currentDenominations,
protocolVersion: exchangeKeysJsonUnchecked.version,
signingKeys: exchangeKeysJsonUnchecked.signkeys,
reserveClosingDelay: exchangeKeysJsonUnchecked.reserve_closing_delay,
@@ -509,6 +496,8 @@ async function downloadExchangeKeysInfo(
recoup: exchangeKeysJsonUnchecked.recoup ?? [],
listIssueDate: exchangeKeysJsonUnchecked.list_issue_date,
globalFees: exchangeKeysJsonUnchecked.global_fees,
+ accounts: exchangeKeysJsonUnchecked.accounts,
+ wireFees: exchangeKeysJsonUnchecked.wire_fees,
};
}
@@ -654,14 +643,7 @@ export async function updateExchangeFromUrlHandler(
}
}
- logger.trace("updating exchange /wire info");
- const wireInfoDownload = await downloadExchangeWireInfo(
- exchangeBaseUrl,
- ws.http,
- timeout,
- );
-
- logger.trace("validating exchange /wire info");
+ logger.trace("validating exchange wire info");
const version = LibtoolVersion.parseVersion(keysInfo.protocolVersion);
if (!version) {
@@ -672,7 +654,7 @@ export async function updateExchangeFromUrlHandler(
const wireInfo = await validateWireInfo(
ws,
version.current,
- wireInfoDownload,
+ keysInfo,
keysInfo.masterPublicKey,
);
diff --git a/packages/taler-wallet-core/src/operations/merchants.ts b/packages/taler-wallet-core/src/operations/merchants.ts
index c47ec4a0a..a148953f0 100644
--- a/packages/taler-wallet-core/src/operations/merchants.ts
+++ b/packages/taler-wallet-core/src/operations/merchants.ts
@@ -41,7 +41,7 @@ export async function getMerchantInfo(
}
const configUrl = new URL("config", canonBaseUrl);
- const resp = await ws.http.get(configUrl.href);
+ const resp = await ws.http.fetch(configUrl.href);
const configResp = await readSuccessResponseJsonOrThrow(
resp,
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-common.ts b/packages/taler-wallet-core/src/operations/pay-peer-common.ts
index 1bc2e8d49..49f255eb9 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-common.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-common.ts
@@ -43,8 +43,6 @@ import {
import { SpendCoinDetails } from "../crypto/cryptoImplementation.js";
import {
DenominationRecord,
- KycPendingInfo,
- KycUserType,
PeerPushPaymentCoinSelection,
ReserveRecord,
} from "../db.js";
@@ -52,68 +50,13 @@ import { InternalWalletState } from "../internal-wallet-state.js";
import { checkDbInvariant } from "../util/invariants.js";
import { getPeerPaymentBalanceDetailsInTx } from "./balance.js";
import { getTotalRefreshCost } from "./refresh.js";
+import type { PeerCoinInfo, PeerCoinSelectionRequest, SelectPeerCoinsResult, SelectedPeerCoin } from "../util/coinSelection.js";
const logger = new Logger("operations/peer-to-peer.ts");
-interface SelectedPeerCoin {
- coinPub: string;
- coinPriv: string;
- contribution: AmountString;
- denomPubHash: string;
- denomSig: UnblindedSignature;
- ageCommitmentProof: AgeCommitmentProof | undefined;
-}
-
-interface PeerCoinSelectionDetails {
- exchangeBaseUrl: string;
-
- /**
- * Info of Coins that were selected.
- */
- coins: SelectedPeerCoin[];
-
- /**
- * How much of the deposit fees is the customer paying?
- */
- depositFees: AmountJson;
-}
-
-/**
- * Information about a selected coin for peer to peer payments.
- */
-interface CoinInfo {
- /**
- * Public key of the coin.
- */
- coinPub: string;
-
- coinPriv: string;
-
- /**
- * Deposit fee for the coin.
- */
- feeDeposit: AmountJson;
-
- value: AmountJson;
-
- denomPubHash: string;
-
- denomSig: UnblindedSignature;
-
- maxAge: number;
-
- ageCommitmentProof?: AgeCommitmentProof;
-}
-
-export type SelectPeerCoinsResult =
- | { type: "success"; result: PeerCoinSelectionDetails }
- | {
- type: "failure";
- insufficientBalanceDetails: PayPeerInsufficientBalanceDetails;
- };
-
/**
* Get information about the coin selected for signatures
+ *
* @param ws
* @param csel
* @returns
@@ -153,211 +96,7 @@ export async function queryCoinInfosForSelection(
return infos;
}
-export interface PeerCoinRepair {
- exchangeBaseUrl: string;
- coinPubs: CoinPublicKeyString[];
- contribs: AmountJson[];
-}
-
-export interface PeerCoinSelectionRequest {
- instructedAmount: AmountJson;
- /**
- * Instruct the coin selection to repair this coin
- * selection instead of selecting completely new coins.
- */
- repair?: PeerCoinRepair;
-}
-
-export async function selectPeerCoins(
- ws: InternalWalletState,
- req: PeerCoinSelectionRequest,
-): Promise<SelectPeerCoinsResult> {
- const instructedAmount = req.instructedAmount;
- if (Amounts.isZero(instructedAmount)) {
- // Other parts of the code assume that we have at least
- // one coin to spend.
- throw new Error("amount of zero not allowed");
- }
- return await ws.db
- .mktx((x) => [
- x.exchanges,
- x.contractTerms,
- x.coins,
- x.coinAvailability,
- x.denominations,
- x.refreshGroups,
- x.peerPushPaymentInitiations,
- ])
- .runReadWrite(async (tx) => {
- const exchanges = await tx.exchanges.iter().toArray();
- const exchangeFeeGap: { [url: string]: AmountJson } = {};
- const currency = Amounts.currencyOf(instructedAmount);
- for (const exch of exchanges) {
- if (exch.detailsPointer?.currency !== currency) {
- continue;
- }
- // FIXME: Can't we do this faster by using coinAvailability?
- const coins = (
- await tx.coins.indexes.byBaseUrl.getAll(exch.baseUrl)
- ).filter((x) => x.status === CoinStatus.Fresh);
- const coinInfos: CoinInfo[] = [];
- for (const coin of coins) {
- const denom = await ws.getDenomInfo(
- ws,
- tx,
- coin.exchangeBaseUrl,
- coin.denomPubHash,
- );
- if (!denom) {
- throw Error("denom not found");
- }
- coinInfos.push({
- coinPub: coin.coinPub,
- feeDeposit: Amounts.parseOrThrow(denom.feeDeposit),
- value: Amounts.parseOrThrow(denom.value),
- denomPubHash: denom.denomPubHash,
- coinPriv: coin.coinPriv,
- denomSig: coin.denomSig,
- maxAge: coin.maxAge,
- ageCommitmentProof: coin.ageCommitmentProof,
- });
- }
- if (coinInfos.length === 0) {
- continue;
- }
- coinInfos.sort(
- (o1, o2) =>
- -Amounts.cmp(o1.value, o2.value) ||
- strcmp(o1.denomPubHash, o2.denomPubHash),
- );
- let amountAcc = Amounts.zeroOfCurrency(currency);
- let depositFeesAcc = Amounts.zeroOfCurrency(currency);
- const resCoins: {
- coinPub: string;
- coinPriv: string;
- contribution: AmountString;
- denomPubHash: string;
- denomSig: UnblindedSignature;
- ageCommitmentProof: AgeCommitmentProof | undefined;
- }[] = [];
- let lastDepositFee = Amounts.zeroOfCurrency(currency);
-
- if (req.repair) {
- for (let i = 0; i < req.repair.coinPubs.length; i++) {
- const contrib = req.repair.contribs[i];
- const coin = await tx.coins.get(req.repair.coinPubs[i]);
- if (!coin) {
- throw Error("repair not possible, coin not found");
- }
- const denom = await ws.getDenomInfo(
- ws,
- tx,
- coin.exchangeBaseUrl,
- coin.denomPubHash,
- );
- checkDbInvariant(!!denom);
- resCoins.push({
- coinPriv: coin.coinPriv,
- coinPub: coin.coinPub,
- contribution: Amounts.stringify(contrib),
- denomPubHash: coin.denomPubHash,
- denomSig: coin.denomSig,
- ageCommitmentProof: coin.ageCommitmentProof,
- });
- const depositFee = Amounts.parseOrThrow(denom.feeDeposit);
- lastDepositFee = depositFee;
- amountAcc = Amounts.add(
- amountAcc,
- Amounts.sub(contrib, depositFee).amount,
- ).amount;
- depositFeesAcc = Amounts.add(depositFeesAcc, depositFee).amount;
- }
- }
-
- for (const coin of coinInfos) {
- if (Amounts.cmp(amountAcc, instructedAmount) >= 0) {
- break;
- }
- const gap = Amounts.add(
- coin.feeDeposit,
- Amounts.sub(instructedAmount, amountAcc).amount,
- ).amount;
- const contrib = Amounts.min(gap, coin.value);
- amountAcc = Amounts.add(
- amountAcc,
- Amounts.sub(contrib, coin.feeDeposit).amount,
- ).amount;
- depositFeesAcc = Amounts.add(depositFeesAcc, coin.feeDeposit).amount;
- resCoins.push({
- coinPriv: coin.coinPriv,
- coinPub: coin.coinPub,
- contribution: Amounts.stringify(contrib),
- denomPubHash: coin.denomPubHash,
- denomSig: coin.denomSig,
- ageCommitmentProof: coin.ageCommitmentProof,
- });
- lastDepositFee = coin.feeDeposit;
- }
- if (Amounts.cmp(amountAcc, instructedAmount) >= 0) {
- const res: PeerCoinSelectionDetails = {
- exchangeBaseUrl: exch.baseUrl,
- coins: resCoins,
- depositFees: depositFeesAcc,
- };
- return { type: "success", result: res };
- }
- const diff = Amounts.sub(instructedAmount, amountAcc).amount;
- exchangeFeeGap[exch.baseUrl] = Amounts.add(lastDepositFee, diff).amount;
-
- continue;
- }
-
- // We were unable to select coins.
- // Now we need to produce error details.
-
- const infoGeneral = await getPeerPaymentBalanceDetailsInTx(ws, tx, {
- currency,
- });
-
- const perExchange: PayPeerInsufficientBalanceDetails["perExchange"] = {};
-
- let maxFeeGapEstimate = Amounts.zeroOfCurrency(currency);
-
- for (const exch of exchanges) {
- if (exch.detailsPointer?.currency !== currency) {
- continue;
- }
- const infoExchange = await getPeerPaymentBalanceDetailsInTx(ws, tx, {
- currency,
- restrictExchangeTo: exch.baseUrl,
- });
- let gap =
- exchangeFeeGap[exch.baseUrl] ?? Amounts.zeroOfCurrency(currency);
- if (Amounts.cmp(infoExchange.balanceMaterial, instructedAmount) < 0) {
- // Show fee gap only if we should've been able to pay with the material amount
- gap = Amounts.zeroOfCurrency(currency);
- }
- perExchange[exch.baseUrl] = {
- balanceAvailable: Amounts.stringify(infoExchange.balanceAvailable),
- balanceMaterial: Amounts.stringify(infoExchange.balanceMaterial),
- feeGapEstimate: Amounts.stringify(gap),
- };
-
- maxFeeGapEstimate = Amounts.max(maxFeeGapEstimate, gap);
- }
-
- const errDetails: PayPeerInsufficientBalanceDetails = {
- amountRequested: Amounts.stringify(instructedAmount),
- balanceAvailable: Amounts.stringify(infoGeneral.balanceAvailable),
- balanceMaterial: Amounts.stringify(infoGeneral.balanceMaterial),
- feeGapEstimate: Amounts.stringify(maxFeeGapEstimate),
- perExchange,
- };
-
- return { type: "failure", insufficientBalanceDetails: errDetails };
- });
-}
export async function getTotalPeerPaymentCost(
ws: InternalWalletState,
@@ -420,28 +159,6 @@ export const codecForExchangePurseStatus = (): Codec<ExchangePurseStatus> =>
.property("merge_timestamp", codecOptional(codecForTimestamp))
.build("ExchangePurseStatus");
-export function talerPaytoFromExchangeReserve(
- exchangeBaseUrl: string,
- reservePub: string,
-): string {
- const url = new URL(exchangeBaseUrl);
- let proto: string;
- if (url.protocol === "http:") {
- proto = "taler-reserve-http";
- } else if (url.protocol === "https:") {
- proto = "taler-reserve";
- } else {
- throw Error(`unsupported exchange base URL protocol (${url.protocol})`);
- }
-
- let path = url.pathname;
- if (!path.endsWith("/")) {
- path = path + "/";
- }
-
- return `payto://${proto}/${url.host}${url.pathname}${reservePub}`;
-}
-
export async function getMergeReserveInfo(
ws: InternalWalletState,
req: {
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts
index 88b441cdd..954300264 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts
@@ -45,6 +45,7 @@ import {
j2s,
makeErrorDetail,
stringifyTalerUri,
+ talerPaytoFromExchangeReserve,
} from "@gnu-taler/taler-util";
import {
readSuccessResponseJsonOrErrorCode,
@@ -74,7 +75,6 @@ import {
import {
codecForExchangePurseStatus,
getMergeReserveInfo,
- talerPaytoFromExchangeReserve,
} from "./pay-peer-common.js";
import {
constructTransactionIdentifier,
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts
index eca3bc91b..0de91bf97 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts
@@ -68,11 +68,9 @@ import {
spendCoins,
} from "./common.js";
import {
- PeerCoinRepair,
codecForExchangePurseStatus,
getTotalPeerPaymentCost,
queryCoinInfosForSelection,
- selectPeerCoins,
} from "./pay-peer-common.js";
import {
constructTransactionIdentifier,
@@ -80,6 +78,7 @@ import {
parseTransactionIdentifier,
stopLongpolling,
} from "./transactions.js";
+import { PeerCoinRepair, selectPeerCoins } from "../util/coinSelection.js";
const logger = new Logger("pay-peer-pull-debit.ts");
@@ -530,7 +529,7 @@ export async function preparePeerPullDebit(
const getContractUrl = new URL(`contracts/${contractPub}`, exchangeBaseUrl);
- const contractHttpResp = await ws.http.get(getContractUrl.href);
+ const contractHttpResp = await ws.http.fetch(getContractUrl.href);
const contractResp = await readSuccessResponseJsonOrThrow(
contractHttpResp,
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts
index e76b934fa..47e9eaddd 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts
@@ -47,6 +47,7 @@ import {
j2s,
makeErrorDetail,
parsePayPushUri,
+ talerPaytoFromExchangeReserve,
} from "@gnu-taler/taler-util";
import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
import {
@@ -71,7 +72,6 @@ import { updateExchangeFromUrl } from "./exchanges.js";
import {
codecForExchangePurseStatus,
getMergeReserveInfo,
- talerPaytoFromExchangeReserve,
} from "./pay-peer-common.js";
import {
TransitionInfo,
@@ -165,7 +165,7 @@ export async function preparePeerPushCredit(
const getPurseUrl = new URL(`purses/${pursePub}/deposit`, exchangeBaseUrl);
- const purseHttpResp = await ws.http.get(getPurseUrl.href);
+ const purseHttpResp = await ws.http.fetch(getPurseUrl.href);
const purseStatus = await readSuccessResponseJsonOrThrow(
purseHttpResp,
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts
index c853bc0ef..2349e5c4a 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts
@@ -68,17 +68,16 @@ import {
spendCoins,
} from "./common.js";
import {
- PeerCoinRepair,
codecForExchangePurseStatus,
getTotalPeerPaymentCost,
queryCoinInfosForSelection,
- selectPeerCoins,
} from "./pay-peer-common.js";
import {
constructTransactionIdentifier,
notifyTransition,
stopLongpolling,
} from "./transactions.js";
+import { PeerCoinRepair, selectPeerCoins } from "../util/coinSelection.js";
const logger = new Logger("pay-peer-push-debit.ts");
diff --git a/packages/taler-wallet-core/src/operations/recoup.ts b/packages/taler-wallet-core/src/operations/recoup.ts
index abeca1119..6a18e5de6 100644
--- a/packages/taler-wallet-core/src/operations/recoup.ts
+++ b/packages/taler-wallet-core/src/operations/recoup.ts
@@ -358,7 +358,7 @@ export async function processRecoupGroup(
);
logger.info(`querying reserve status for recoup via ${reserveUrl}`);
- const resp = await ws.http.get(reserveUrl.href);
+ const resp = await ws.http.fetch(reserveUrl.href);
const result = await readSuccessResponseJsonOrThrow(
resp,
diff --git a/packages/taler-wallet-core/src/operations/reward.ts b/packages/taler-wallet-core/src/operations/reward.ts
index 47956f15f..69c888d7a 100644
--- a/packages/taler-wallet-core/src/operations/reward.ts
+++ b/packages/taler-wallet-core/src/operations/reward.ts
@@ -161,7 +161,7 @@ export async function prepareTip(
res.merchantBaseUrl,
);
logger.trace("checking tip status from", tipStatusUrl.href);
- const merchantResp = await ws.http.get(tipStatusUrl.href);
+ const merchantResp = await ws.http.fetch(tipStatusUrl.href);
const tipPickupStatus = await readSuccessResponseJsonOrThrow(
merchantResp,
codecForTipPickupGetResponse(),
diff --git a/packages/taler-wallet-core/src/operations/testing.ts b/packages/taler-wallet-core/src/operations/testing.ts
index 3090549d5..aff92622a 100644
--- a/packages/taler-wallet-core/src/operations/testing.ts
+++ b/packages/taler-wallet-core/src/operations/testing.ts
@@ -293,7 +293,7 @@ async function checkPayment(
): Promise<CheckPaymentResponse> {
const reqUrl = new URL(`private/orders/${orderId}`, merchantBackend.baseUrl);
reqUrl.searchParams.set("order_id", orderId);
- const resp = await http.get(reqUrl.href, {
+ const resp = await http.fetch(reqUrl.href, {
headers: getMerchantAuthHeader(merchantBackend),
});
return readSuccessResponseJsonOrThrow(resp, codecForCheckPaymentResponse());
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts
index 9d0cded37..44817b389 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -558,7 +558,7 @@ export async function getBankWithdrawalInfo(
const configReqUrl = new URL("config", uriResult.bankIntegrationApiBaseUrl);
- const configResp = await http.get(configReqUrl.href);
+ const configResp = await http.fetch(configReqUrl.href);
const config = await readSuccessResponseJsonOrThrow(
configResp,
codecForTalerConfigResponse(),
@@ -586,7 +586,7 @@ export async function getBankWithdrawalInfo(
logger.info(`bank withdrawal status URL: ${reqUrl.href}}`);
- const resp = await http.get(reqUrl.href);
+ const resp = await http.fetch(reqUrl.href);
const status = await readSuccessResponseJsonOrThrow(
resp,
codecForWithdrawOperationStatusResponse(),
@@ -2103,7 +2103,7 @@ async function processReserveBankStatus(
const bankStatusUrl = getBankStatusUrl(bankInfo.talerWithdrawUri);
- const statusResp = await ws.http.get(bankStatusUrl, {
+ const statusResp = await ws.http.fetch(bankStatusUrl, {
timeout: getReserveRequestTimeout(withdrawalGroup),
});
const status = await readSuccessResponseJsonOrThrow(
diff --git a/packages/taler-wallet-core/src/util/coinSelection.test.ts b/packages/taler-wallet-core/src/util/coinSelection.test.ts
index fddd217ea..b907eb160 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.test.ts
+++ b/packages/taler-wallet-core/src/util/coinSelection.test.ts
@@ -22,746 +22,4 @@ import {
TransactionAmountMode,
} from "@gnu-taler/taler-util";
import test, { ExecutionContext } from "ava";
-import {
- CoinInfo,
- convertDepositAmountForAvailableCoins,
- convertWithdrawalAmountFromAvailableCoins,
- getMaxDepositAmountForAvailableCoins,
-} from "./coinSelection.js";
-
-function makeCurrencyHelper(currency: string) {
- return (sx: TemplateStringsArray, ...vx: any[]) => {
- const s = String.raw({ raw: sx }, ...vx);
- return Amounts.parseOrThrow(`${currency}:${s}`);
- };
-}
-
-const kudos = makeCurrencyHelper("kudos");
-
-function defaultFeeConfig(value: AmountJson, totalAvailable: number): CoinInfo {
- return {
- id: Amounts.stringify(value),
- denomDeposit: kudos`0.01`,
- denomRefresh: kudos`0.01`,
- denomWithdraw: kudos`0.01`,
- exchangeBaseUrl: "1",
- duration: Duration.getForever(),
- exchangePurse: undefined,
- exchangeWire: undefined,
- maxAge: AgeRestriction.AGE_UNRESTRICTED,
- totalAvailable,
- value,
- };
-}
-type Coin = [AmountJson, number];
-
-/**
- * Making a deposit with effective amount
- *
- */
-
-test("deposit effective 2", (t) => {
- const coinList: Coin[] = [
- [kudos`2`, 5],
- [kudos`5`, 5],
- ];
- const result = convertDepositAmountForAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {},
- },
- kudos`2`,
- TransactionAmountMode.Effective,
- );
- t.is(Amounts.stringifyValue(result.effective), "2");
- t.is(Amounts.stringifyValue(result.raw), "1.99");
-});
-
-test("deposit effective 10", (t) => {
- const coinList: Coin[] = [
- [kudos`2`, 5],
- [kudos`5`, 5],
- ];
- const result = convertDepositAmountForAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {},
- },
- kudos`10`,
- TransactionAmountMode.Effective,
- );
- t.is(Amounts.stringifyValue(result.effective), "10");
- t.is(Amounts.stringifyValue(result.raw), "9.98");
-});
-
-test("deposit effective 24", (t) => {
- const coinList: Coin[] = [
- [kudos`2`, 5],
- [kudos`5`, 5],
- ];
- const result = convertDepositAmountForAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {},
- },
- kudos`24`,
- TransactionAmountMode.Effective,
- );
- t.is(Amounts.stringifyValue(result.effective), "24");
- t.is(Amounts.stringifyValue(result.raw), "23.94");
-});
-
-test("deposit effective 40", (t) => {
- const coinList: Coin[] = [
- [kudos`2`, 5],
- [kudos`5`, 5],
- ];
- const result = convertDepositAmountForAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {},
- },
- kudos`40`,
- TransactionAmountMode.Effective,
- );
- t.is(Amounts.stringifyValue(result.effective), "35");
- t.is(Amounts.stringifyValue(result.raw), "34.9");
-});
-
-test("deposit with wire fee effective 2", (t) => {
- const coinList: Coin[] = [
- [kudos`2`, 5],
- [kudos`5`, 5],
- ];
- const result = convertDepositAmountForAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {
- one: {
- wireFee: kudos`0.1`,
- purseFee: kudos`0.00`,
- creditDeadline: AbsoluteTime.never(),
- debitDeadline: AbsoluteTime.never(),
- },
- },
- },
- kudos`2`,
- TransactionAmountMode.Effective,
- );
- t.is(Amounts.stringifyValue(result.effective), "2");
- t.is(Amounts.stringifyValue(result.raw), "1.89");
-});
-
-/**
- * Making a deposit with raw amount, using the result from effective
- *
- */
-
-test("deposit raw 1.99 (effective 2)", (t) => {
- const coinList: Coin[] = [
- [kudos`2`, 5],
- [kudos`5`, 5],
- ];
- const result = convertDepositAmountForAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {},
- },
- kudos`1.99`,
- TransactionAmountMode.Raw,
- );
- t.is(Amounts.stringifyValue(result.effective), "2");
- t.is(Amounts.stringifyValue(result.raw), "1.99");
-});
-
-test("deposit raw 9.98 (effective 10)", (t) => {
- const coinList: Coin[] = [
- [kudos`2`, 5],
- [kudos`5`, 5],
- ];
- const result = convertDepositAmountForAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {},
- },
- kudos`9.98`,
- TransactionAmountMode.Raw,
- );
- t.is(Amounts.stringifyValue(result.effective), "10");
- t.is(Amounts.stringifyValue(result.raw), "9.98");
-});
-
-test("deposit raw 23.94 (effective 24)", (t) => {
- const coinList: Coin[] = [
- [kudos`2`, 5],
- [kudos`5`, 5],
- ];
- const result = convertDepositAmountForAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {},
- },
- kudos`23.94`,
- TransactionAmountMode.Raw,
- );
- t.is(Amounts.stringifyValue(result.effective), "24");
- t.is(Amounts.stringifyValue(result.raw), "23.94");
-});
-
-test("deposit raw 34.9 (effective 40)", (t) => {
- const coinList: Coin[] = [
- [kudos`2`, 5],
- [kudos`5`, 5],
- ];
- const result = convertDepositAmountForAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {},
- },
- kudos`34.9`,
- TransactionAmountMode.Raw,
- );
- t.is(Amounts.stringifyValue(result.effective), "35");
- t.is(Amounts.stringifyValue(result.raw), "34.9");
-});
-
-test("deposit with wire fee raw 2", (t) => {
- const coinList: Coin[] = [
- [kudos`2`, 5],
- [kudos`5`, 5],
- ];
- const result = convertDepositAmountForAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {
- one: {
- wireFee: kudos`0.1`,
- purseFee: kudos`0.00`,
- creditDeadline: AbsoluteTime.never(),
- debitDeadline: AbsoluteTime.never(),
- },
- },
- },
- kudos`2`,
- TransactionAmountMode.Effective,
- );
- t.is(Amounts.stringifyValue(result.effective), "2");
- t.is(Amounts.stringifyValue(result.raw), "1.89");
-});
-
-/**
- * Calculating the max amount possible to deposit
- *
- */
-
-test("deposit max 35", (t) => {
- const coinList: Coin[] = [
- [kudos`2`, 5],
- [kudos`5`, 5],
- ];
- const result = getMaxDepositAmountForAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {
- "2": {
- wireFee: kudos`0.00`,
- purseFee: kudos`0.00`,
- creditDeadline: AbsoluteTime.never(),
- debitDeadline: AbsoluteTime.never(),
- },
- },
- },
- "KUDOS",
- );
- t.is(Amounts.stringifyValue(result.raw), "34.9");
- t.is(Amounts.stringifyValue(result.effective), "35");
-});
-
-test("deposit max 35 with wirefee", (t) => {
- const coinList: Coin[] = [
- [kudos`2`, 5],
- [kudos`5`, 5],
- ];
- const result = getMaxDepositAmountForAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {
- "2": {
- wireFee: kudos`1`,
- purseFee: kudos`0.00`,
- creditDeadline: AbsoluteTime.never(),
- debitDeadline: AbsoluteTime.never(),
- },
- },
- },
- "KUDOS",
- );
- t.is(Amounts.stringifyValue(result.raw), "33.9");
- t.is(Amounts.stringifyValue(result.effective), "35");
-});
-
-test("deposit max repeated denom", (t) => {
- const coinList: Coin[] = [
- [kudos`2`, 1],
- [kudos`2`, 1],
- [kudos`5`, 1],
- ];
- const result = getMaxDepositAmountForAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {
- "2": {
- wireFee: kudos`0.00`,
- purseFee: kudos`0.00`,
- creditDeadline: AbsoluteTime.never(),
- debitDeadline: AbsoluteTime.never(),
- },
- },
- },
- "KUDOS",
- );
- t.is(Amounts.stringifyValue(result.raw), "8.97");
- t.is(Amounts.stringifyValue(result.effective), "9");
-});
-
-/**
- * Making a withdrawal with effective amount
- *
- */
-
-test("withdraw effective 2", (t) => {
- const coinList: Coin[] = [
- [kudos`2`, 5],
- [kudos`5`, 5],
- ];
- const result = convertWithdrawalAmountFromAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {},
- },
- kudos`2`,
- TransactionAmountMode.Effective,
- );
- t.is(Amounts.stringifyValue(result.effective), "2");
- t.is(Amounts.stringifyValue(result.raw), "2.01");
-});
-
-test("withdraw effective 10", (t) => {
- const coinList: Coin[] = [
- [kudos`2`, 5],
- [kudos`5`, 5],
- ];
- const result = convertWithdrawalAmountFromAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {},
- },
- kudos`10`,
- TransactionAmountMode.Effective,
- );
- t.is(Amounts.stringifyValue(result.effective), "10");
- t.is(Amounts.stringifyValue(result.raw), "10.02");
-});
-
-test("withdraw effective 24", (t) => {
- const coinList: Coin[] = [
- [kudos`2`, 5],
- [kudos`5`, 5],
- ];
- const result = convertWithdrawalAmountFromAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {},
- },
- kudos`24`,
- TransactionAmountMode.Effective,
- );
- t.is(Amounts.stringifyValue(result.effective), "24");
- t.is(Amounts.stringifyValue(result.raw), "24.06");
-});
-
-test("withdraw effective 40", (t) => {
- const coinList: Coin[] = [
- [kudos`2`, 5],
- [kudos`5`, 5],
- ];
- const result = convertWithdrawalAmountFromAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {},
- },
- kudos`40`,
- TransactionAmountMode.Effective,
- );
- t.is(Amounts.stringifyValue(result.effective), "40");
- t.is(Amounts.stringifyValue(result.raw), "40.08");
-});
-
-/**
- * Making a deposit with raw amount, using the result from effective
- *
- */
-
-test("withdraw raw 2.01 (effective 2)", (t) => {
- const coinList: Coin[] = [
- [kudos`2`, 5],
- [kudos`5`, 5],
- ];
- const result = convertWithdrawalAmountFromAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {},
- },
- kudos`2.01`,
- TransactionAmountMode.Raw,
- );
- t.is(Amounts.stringifyValue(result.effective), "2");
- t.is(Amounts.stringifyValue(result.raw), "2.01");
-});
-
-test("withdraw raw 10.02 (effective 10)", (t) => {
- const coinList: Coin[] = [
- [kudos`2`, 5],
- [kudos`5`, 5],
- ];
- const result = convertWithdrawalAmountFromAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {},
- },
- kudos`10.02`,
- TransactionAmountMode.Raw,
- );
- t.is(Amounts.stringifyValue(result.effective), "10");
- t.is(Amounts.stringifyValue(result.raw), "10.02");
-});
-
-test("withdraw raw 24.06 (effective 24)", (t) => {
- const coinList: Coin[] = [
- [kudos`2`, 5],
- [kudos`5`, 5],
- ];
- const result = convertWithdrawalAmountFromAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {},
- },
- kudos`24.06`,
- TransactionAmountMode.Raw,
- );
- t.is(Amounts.stringifyValue(result.effective), "24");
- t.is(Amounts.stringifyValue(result.raw), "24.06");
-});
-
-test("withdraw raw 40.08 (effective 40)", (t) => {
- const coinList: Coin[] = [
- [kudos`2`, 5],
- [kudos`5`, 5],
- ];
- const result = convertWithdrawalAmountFromAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {},
- },
- kudos`40.08`,
- TransactionAmountMode.Raw,
- );
- t.is(Amounts.stringifyValue(result.effective), "40");
- t.is(Amounts.stringifyValue(result.raw), "40.08");
-});
-
-test("withdraw raw 25", (t) => {
- const coinList: Coin[] = [
- [kudos`0.1`, 0],
- [kudos`1`, 0],
- [kudos`2`, 0],
- [kudos`5`, 0],
- ];
- const result = convertWithdrawalAmountFromAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {},
- },
- kudos`25`,
- TransactionAmountMode.Raw,
- );
- t.is(Amounts.stringifyValue(result.effective), "24.8");
- t.is(Amounts.stringifyValue(result.raw), "24.94");
-});
-
-test("withdraw effective 24.8 (raw 25)", (t) => {
- const coinList: Coin[] = [
- [kudos`0.1`, 0],
- [kudos`1`, 0],
- [kudos`2`, 0],
- [kudos`5`, 0],
- ];
- const result = convertWithdrawalAmountFromAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {},
- },
- kudos`24.8`,
- TransactionAmountMode.Effective,
- );
- t.is(Amounts.stringifyValue(result.effective), "24.8");
- t.is(Amounts.stringifyValue(result.raw), "24.94");
-});
-
-/**
- * Making a deposit with refresh
- *
- */
-
-test("deposit with refresh: effective 3", (t) => {
- const coinList: Coin[] = [
- [kudos`0.1`, 0],
- [kudos`1`, 0],
- [kudos`2`, 5],
- [kudos`5`, 5],
- ];
- const result = convertDepositAmountForAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {},
- },
- kudos`3`,
- TransactionAmountMode.Effective,
- );
- t.is(Amounts.stringifyValue(result.effective), "3.1");
- t.is(Amounts.stringifyValue(result.raw), "2.98");
- expectDefined(t, result.refresh);
- //FEES
- //deposit 2 x 0.01
- //refresh 1 x 0.01
- //withdraw 9 x 0.01
- //-----------------
- //op 0.12
-
- //coins sent 2 x 2.0
- //coins recv 9 x 0.1
- //-------------------
- //effective 3.10
- //raw 2.98
- t.is(Amounts.stringifyValue(result.refresh.selected.id), "2");
- t.deepEqual(asCoinList(result.refresh.coins), [[kudos`0.1`, 9]]);
-});
-
-test("deposit with refresh: raw 2.98 (effective 3)", (t) => {
- const coinList: Coin[] = [
- [kudos`0.1`, 0],
- [kudos`1`, 0],
- [kudos`2`, 5],
- [kudos`5`, 5],
- ];
- const result = convertDepositAmountForAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {},
- },
- kudos`2.98`,
- TransactionAmountMode.Raw,
- );
- t.is(Amounts.stringifyValue(result.effective), "3.2");
- t.is(Amounts.stringifyValue(result.raw), "3.09");
- expectDefined(t, result.refresh);
- //FEES
- //deposit 1 x 0.01
- //refresh 1 x 0.01
- //withdraw 8 x 0.01
- //-----------------
- //op 0.10
-
- //coins sent 1 x 2.0
- //coins recv 8 x 0.1
- //-------------------
- //effective 3.20
- //raw 3.09
- t.is(Amounts.stringifyValue(result.refresh.selected.id), "2");
- t.deepEqual(asCoinList(result.refresh.coins), [[kudos`0.1`, 8]]);
-});
-
-test("deposit with refresh: effective 3.2 (raw 2.98)", (t) => {
- const coinList: Coin[] = [
- [kudos`0.1`, 0],
- [kudos`1`, 0],
- [kudos`2`, 5],
- [kudos`5`, 5],
- ];
- const result = convertDepositAmountForAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {},
- },
- kudos`3.2`,
- TransactionAmountMode.Effective,
- );
- t.is(Amounts.stringifyValue(result.effective), "3.3");
- t.is(Amounts.stringifyValue(result.raw), "3.2");
- expectDefined(t, result.refresh);
- //FEES
- //deposit 2 x 0.01
- //refresh 1 x 0.01
- //withdraw 7 x 0.01
- //-----------------
- //op 0.10
-
- //coins sent 2 x 2.0
- //coins recv 7 x 0.1
- //-------------------
- //effective 3.30
- //raw 3.20
- t.is(Amounts.stringifyValue(result.refresh.selected.id), "2");
- t.deepEqual(asCoinList(result.refresh.coins), [[kudos`0.1`, 7]]);
-});
-
-function expectDefined<T>(
- t: ExecutionContext,
- v: T | undefined,
-): asserts v is T {
- t.assert(v !== undefined);
-}
-
-function asCoinList(v: { info: CoinInfo; size: number }[]): any {
- return v.map((c) => {
- return [c.info.value, c.size];
- });
-}
-
-/**
- * regression tests
- */
-
-test("demo: withdraw raw 25", (t) => {
- const coinList: Coin[] = [
- [kudos`0.1`, 0],
- [kudos`1`, 0],
- [kudos`2`, 0],
- [kudos`5`, 0],
- [kudos`10`, 0],
- ];
- const result = convertWithdrawalAmountFromAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {},
- },
- kudos`25`,
- TransactionAmountMode.Raw,
- );
- t.is(Amounts.stringifyValue(result.effective), "24.8");
- t.is(Amounts.stringifyValue(result.raw), "24.92");
- // coins received
- // 8 x 0.1
- // 2 x 0.2
- // 2 x 10.0
- // total effective 24.8
- // fee 12 x 0.01 = 0.12
- // total raw 24.92
- // left in reserve 25 - 24.92 == 0.08
-
- //current wallet impl: hides the left in reserve fee
- //shows fee = 0.2
-});
-
-test("demo: deposit max after withdraw raw 25", (t) => {
- const coinList: Coin[] = [
- [kudos`0.1`, 8],
- [kudos`1`, 0],
- [kudos`2`, 2],
- [kudos`5`, 0],
- [kudos`10`, 2],
- ];
- const result = getMaxDepositAmountForAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {
- one: {
- wireFee: kudos`0.01`,
- purseFee: kudos`0.00`,
- creditDeadline: AbsoluteTime.never(),
- debitDeadline: AbsoluteTime.never(),
- },
- },
- },
- "KUDOS",
- );
- t.is(Amounts.stringifyValue(result.effective), "24.8");
- t.is(Amounts.stringifyValue(result.raw), "24.67");
-
- // 8 x 0.1
- // 2 x 0.2
- // 2 x 10.0
- // total effective 24.8
- // deposit fee 12 x 0.01 = 0.12
- // wire fee 0.01
- // total raw: 24.8 - 0.13 = 24.67
-
- // current wallet impl fee 0.14
-});
-
-test("demo: withdraw raw 13", (t) => {
- const coinList: Coin[] = [
- [kudos`0.1`, 0],
- [kudos`1`, 0],
- [kudos`2`, 0],
- [kudos`5`, 0],
- [kudos`10`, 0],
- ];
- const result = convertWithdrawalAmountFromAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {},
- },
- kudos`13`,
- TransactionAmountMode.Raw,
- );
- t.is(Amounts.stringifyValue(result.effective), "12.8");
- t.is(Amounts.stringifyValue(result.raw), "12.9");
- // coins received
- // 8 x 0.1
- // 1 x 0.2
- // 1 x 10.0
- // total effective 12.8
- // fee 10 x 0.01 = 0.10
- // total raw 12.9
- // left in reserve 13 - 12.9 == 0.1
-
- //current wallet impl: hides the left in reserve fee
- //shows fee = 0.2
-});
-
-test("demo: deposit max after withdraw raw 13", (t) => {
- const coinList: Coin[] = [
- [kudos`0.1`, 8],
- [kudos`1`, 0],
- [kudos`2`, 1],
- [kudos`5`, 0],
- [kudos`10`, 1],
- ];
- const result = getMaxDepositAmountForAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {
- one: {
- wireFee: kudos`0.01`,
- purseFee: kudos`0.00`,
- creditDeadline: AbsoluteTime.never(),
- debitDeadline: AbsoluteTime.never(),
- },
- },
- },
- "KUDOS",
- );
- t.is(Amounts.stringifyValue(result.effective), "12.8");
- t.is(Amounts.stringifyValue(result.raw), "12.69");
-
- // 8 x 0.1
- // 1 x 0.2
- // 1 x 10.0
- // total effective 12.8
- // deposit fee 10 x 0.01 = 0.10
- // wire fee 0.01
- // total raw: 12.8 - 0.11 = 12.69
- // current wallet impl fee 0.14
-});
diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts b/packages/taler-wallet-core/src/util/coinSelection.ts
index d3c6ffc67..bb901fd75 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.ts
+++ b/packages/taler-wallet-core/src/util/coinSelection.ts
@@ -31,6 +31,8 @@ import {
AmountJson,
AmountResponse,
Amounts,
+ AmountString,
+ CoinPublicKeyString,
CoinStatus,
ConvertAmountRequest,
DenominationInfo,
@@ -40,28 +42,28 @@ import {
ForcedCoinSel,
ForcedDenomSel,
GetAmountRequest,
- GetPlanForOperationRequest,
j2s,
Logger,
parsePaytoUri,
PayCoinSelection,
PayMerchantInsufficientBalanceDetails,
+ PayPeerInsufficientBalanceDetails,
strcmp,
TransactionAmountMode,
TransactionType,
+ UnblindedSignature,
} from "@gnu-taler/taler-util";
import {
AllowedAuditorInfo,
AllowedExchangeInfo,
DenominationRecord,
} from "../db.js";
-import {
- CoinAvailabilityRecord,
- getExchangeDetails,
- isWithdrawableDenom,
-} from "../index.js";
+import { getExchangeDetails, isWithdrawableDenom } from "../index.js";
import { InternalWalletState } from "../internal-wallet-state.js";
-import { getMerchantPaymentBalanceDetails } from "../operations/balance.js";
+import {
+ getMerchantPaymentBalanceDetails,
+ getPeerPaymentBalanceDetailsInTx,
+} from "../operations/balance.js";
import { checkDbInvariant, checkLogicInvariant } from "./invariants.js";
const logger = new Logger("coinSelection.ts");
@@ -255,7 +257,7 @@ export async function selectPayCoinsNew(
wireFeeAmortization,
} = req;
- const [candidateDenoms, wireFeesPerExchange] = await selectCandidates(
+ const [candidateDenoms, wireFeesPerExchange] = await selectPayMerchantCandidates(
ws,
req,
);
@@ -549,7 +551,7 @@ export type AvailableDenom = DenominationInfo & {
numAvailable: number;
};
-async function selectCandidates(
+async function selectPayMerchantCandidates(
ws: InternalWalletState,
req: SelectPayCoinRequestNg,
): Promise<[AvailableDenom[], Record<string, AmountJson>]> {
@@ -797,76 +799,6 @@ export function selectForcedWithdrawalDenominations(
};
}
-function getCoinsFilter(req: GetPlanForOperationRequest): CoinsFilter {
- switch (req.type) {
- case TransactionType.Withdrawal: {
- return {
- exchanges:
- req.exchangeUrl === undefined ? undefined : [req.exchangeUrl],
- };
- }
- case TransactionType.Deposit: {
- const payto = parsePaytoUri(req.account);
- if (!payto) {
- throw Error(`wrong payto ${req.account}`);
- }
- return {
- wireMethod: payto.targetType,
- };
- }
- }
-}
-
-/**
- * If the operation going to be plan subtracts
- * or adds amount in the wallet db
- */
-export enum OperationType {
- Credit = "credit",
- Debit = "debit",
-}
-
-function getOperationType(txType: TransactionType): OperationType {
- const operationType =
- txType === TransactionType.Withdrawal
- ? OperationType.Credit
- : txType === TransactionType.Deposit
- ? OperationType.Debit
- : undefined;
- if (!operationType) {
- throw Error(`operation type ${txType} not yet supported`);
- }
- return operationType;
-}
-
-interface RefreshChoice {
- /**
- * Amount that need to be covered
- */
- gap: AmountJson;
- totalFee: AmountJson;
- selected: CoinInfo;
- totalChangeValue: AmountJson;
- refreshEffective: AmountJson;
- coins: { info: CoinInfo; size: number }[];
-
- // totalValue: AmountJson;
- // totalDepositFee: AmountJson;
- // totalRefreshFee: AmountJson;
- // totalChangeContribution: AmountJson;
- // totalChangeWithdrawalFee: AmountJson;
-}
-
-interface AvailableCoins {
- list: CoinInfo[];
- exchanges: Record<string, ExchangeInfo>;
-}
-interface SelectedCoins {
- totalValue: AmountJson;
- coins: { info: CoinInfo; size: number }[];
- refresh?: RefreshChoice;
-}
-
export interface CoinInfo {
id: string;
value: AmountJson;
@@ -880,739 +812,267 @@ export interface CoinInfo {
exchangeBaseUrl: string;
maxAge: number;
}
-interface ExchangeInfo {
- wireFee: AmountJson | undefined;
- purseFee: AmountJson | undefined;
- creditDeadline: AbsoluteTime;
- debitDeadline: AbsoluteTime;
-}
-
-interface CoinsFilter {
- shouldCalculatePurseFee?: boolean;
- exchanges?: string[];
- wireMethod?: string;
- ageRestricted?: number;
-}
-/**
- * Get all the denoms that can be used for a operation that is limited
- * by the following restrictions.
- * This function is costly (by the database access) but with high chances
- * of being cached
- */
-async function getAvailableDenoms(
- ws: InternalWalletState,
- op: TransactionType,
- currency: string,
- filters: CoinsFilter = {},
-): Promise<AvailableCoins> {
- const operationType = getOperationType(TransactionType.Deposit);
-
- return await ws.db
- .mktx((x) => [
- x.exchanges,
- x.exchangeDetails,
- x.denominations,
- x.coinAvailability,
- ])
- .runReadOnly(async (tx) => {
- const list: CoinInfo[] = [];
- const exchanges: Record<string, ExchangeInfo> = {};
-
- const databaseExchanges = await tx.exchanges.iter().toArray();
- const filteredExchanges =
- filters.exchanges ?? databaseExchanges.map((e) => e.baseUrl);
-
- for (const exchangeBaseUrl of filteredExchanges) {
- const exchangeDetails = await getExchangeDetails(tx, exchangeBaseUrl);
- // 1.- exchange has same currency
- if (exchangeDetails?.currency !== currency) {
- continue;
- }
-
- let deadline = AbsoluteTime.never();
- // 2.- exchange supports wire method
- let wireFee: AmountJson | undefined;
- if (filters.wireMethod) {
- const wireMethodWithDates =
- exchangeDetails.wireInfo.feesForType[filters.wireMethod];
-
- if (!wireMethodWithDates) {
- throw Error(
- `exchange ${exchangeBaseUrl} doesn't have wire method ${filters.wireMethod}`,
- );
- }
- const wireMethodFee = wireMethodWithDates.find((x) => {
- return AbsoluteTime.isBetween(
- AbsoluteTime.now(),
- AbsoluteTime.fromProtocolTimestamp(x.startStamp),
- AbsoluteTime.fromProtocolTimestamp(x.endStamp),
- );
- });
-
- if (!wireMethodFee) {
- throw Error(
- `exchange ${exchangeBaseUrl} doesn't have wire fee defined for this period`,
- );
- }
- wireFee = Amounts.parseOrThrow(wireMethodFee.wireFee);
- deadline = AbsoluteTime.min(
- deadline,
- AbsoluteTime.fromProtocolTimestamp(wireMethodFee.endStamp),
- );
- }
- // exchanges[exchangeBaseUrl].wireFee = wireMethodFee;
-
- // 3.- exchange supports wire method
- let purseFee: AmountJson | undefined;
- if (filters.shouldCalculatePurseFee) {
- const purseFeeFound = exchangeDetails.globalFees.find((x) => {
- return AbsoluteTime.isBetween(
- AbsoluteTime.now(),
- AbsoluteTime.fromProtocolTimestamp(x.startDate),
- AbsoluteTime.fromProtocolTimestamp(x.endDate),
- );
- });
- if (!purseFeeFound) {
- throw Error(
- `exchange ${exchangeBaseUrl} doesn't have purse fee defined for this period`,
- );
- }
- purseFee = Amounts.parseOrThrow(purseFeeFound.purseFee);
- deadline = AbsoluteTime.min(
- deadline,
- AbsoluteTime.fromProtocolTimestamp(purseFeeFound.endDate),
- );
- }
-
- let creditDeadline = AbsoluteTime.never();
- let debitDeadline = AbsoluteTime.never();
- //4.- filter coins restricted by age
- if (operationType === OperationType.Credit) {
- const ds = await tx.denominations.indexes.byExchangeBaseUrl.getAll(
- exchangeBaseUrl,
- );
- for (const denom of ds) {
- const expiresWithdraw = AbsoluteTime.fromProtocolTimestamp(
- denom.stampExpireWithdraw,
- );
- const expiresDeposit = AbsoluteTime.fromProtocolTimestamp(
- denom.stampExpireDeposit,
- );
- creditDeadline = AbsoluteTime.min(deadline, expiresWithdraw);
- debitDeadline = AbsoluteTime.min(deadline, expiresDeposit);
- list.push(
- buildCoinInfoFromDenom(
- denom,
- purseFee,
- wireFee,
- AgeRestriction.AGE_UNRESTRICTED,
- Number.MAX_SAFE_INTEGER, // Max withdrawable from single denom
- ),
- );
- }
- } else {
- const ageLower = filters.ageRestricted ?? 0;
- const ageUpper = AgeRestriction.AGE_UNRESTRICTED;
-
- const myExchangeCoins =
- await tx.coinAvailability.indexes.byExchangeAgeAvailability.getAll(
- GlobalIDB.KeyRange.bound(
- [exchangeDetails.exchangeBaseUrl, ageLower, 1],
- [
- exchangeDetails.exchangeBaseUrl,
- ageUpper,
- Number.MAX_SAFE_INTEGER,
- ],
- ),
- );
- //5.- save denoms with how many coins are available
- // FIXME: Check that the individual denomination is audited!
- // FIXME: Should we exclude denominations that are
- // not spendable anymore?
- for (const coinAvail of myExchangeCoins) {
- const denom = await tx.denominations.get([
- coinAvail.exchangeBaseUrl,
- coinAvail.denomPubHash,
- ]);
- checkDbInvariant(!!denom);
- if (denom.isRevoked || !denom.isOffered) {
- continue;
- }
- const expiresWithdraw = AbsoluteTime.fromProtocolTimestamp(
- denom.stampExpireWithdraw,
- );
- const expiresDeposit = AbsoluteTime.fromProtocolTimestamp(
- denom.stampExpireDeposit,
- );
- creditDeadline = AbsoluteTime.min(deadline, expiresWithdraw);
- debitDeadline = AbsoluteTime.min(deadline, expiresDeposit);
- list.push(
- buildCoinInfoFromDenom(
- denom,
- purseFee,
- wireFee,
- coinAvail.maxAge,
- coinAvail.freshCoinCount,
- ),
- );
- }
- }
-
- exchanges[exchangeBaseUrl] = {
- purseFee,
- wireFee,
- debitDeadline,
- creditDeadline,
- };
- }
-
- return { list, exchanges };
- });
-}
-function buildCoinInfoFromDenom(
- denom: DenominationRecord,
- purseFee: AmountJson | undefined,
- wireFee: AmountJson | undefined,
- maxAge: number,
- total: number,
-): CoinInfo {
- return {
- id: denom.denomPubHash,
- denomWithdraw: Amounts.parseOrThrow(denom.fees.feeWithdraw),
- denomDeposit: Amounts.parseOrThrow(denom.fees.feeDeposit),
- denomRefresh: Amounts.parseOrThrow(denom.fees.feeRefresh),
- exchangePurse: purseFee,
- exchangeWire: wireFee,
- exchangeBaseUrl: denom.exchangeBaseUrl,
- duration: AbsoluteTime.difference(
- AbsoluteTime.now(),
- AbsoluteTime.fromProtocolTimestamp(denom.stampExpireDeposit),
- ),
- totalAvailable: total,
- value: DenominationRecord.getValue(denom),
- maxAge,
- };
-}
-
-export async function convertDepositAmount(
- ws: InternalWalletState,
- req: ConvertAmountRequest,
-): Promise<AmountResponse> {
- const amount = Amounts.parseOrThrow(req.amount);
- // const filter = getCoinsFilter(req);
-
- const denoms = await getAvailableDenoms(
- ws,
- TransactionType.Deposit,
- amount.currency,
- {},
- );
- const result = convertDepositAmountForAvailableCoins(
- denoms,
- amount,
- req.type,
- );
- return {
- effectiveAmount: Amounts.stringify(result.effective),
- rawAmount: Amounts.stringify(result.raw),
- };
+export interface SelectedPeerCoin {
+ coinPub: string;
+ coinPriv: string;
+ contribution: AmountString;
+ denomPubHash: string;
+ denomSig: UnblindedSignature;
+ ageCommitmentProof: AgeCommitmentProof | undefined;
}
-const LOG_REFRESH = false;
-const LOG_DEPOSIT = false;
-export function convertDepositAmountForAvailableCoins(
- denoms: AvailableCoins,
- amount: AmountJson,
- mode: TransactionAmountMode,
-): AmountAndRefresh {
- const zero = Amounts.zeroOfCurrency(amount.currency);
- if (!denoms.list.length) {
- // no coins in the database
- return { effective: zero, raw: zero };
- }
- const depositDenoms = rankDenominationForDeposit(denoms.list, mode);
-
- //FIXME: we are not taking into account
- // * exchanges with multiple accounts
- // * wallet with multiple exchanges
- const wireFee = Object.values(denoms.exchanges)[0]?.wireFee ?? zero;
- const adjustedAmount = Amounts.add(amount, wireFee).amount;
-
- const selected = selectGreedyCoins(depositDenoms, adjustedAmount);
-
- const gap = Amounts.sub(amount, selected.totalValue).amount;
-
- const result = getTotalEffectiveAndRawForDeposit(
- selected.coins,
- amount.currency,
- );
- result.raw = Amounts.sub(result.raw, wireFee).amount;
-
- if (Amounts.isZero(gap)) {
- // exact amount founds
- return result;
- }
-
- if (LOG_DEPOSIT) {
- const logInfo = selected.coins.map((c) => {
- return `${Amounts.stringifyValue(c.info.id)} x ${c.size}`;
- });
- console.log(
- "deposit used:",
- logInfo.join(", "),
- "gap:",
- Amounts.stringifyValue(gap),
- );
- }
+export interface PeerCoinSelectionDetails {
+ exchangeBaseUrl: string;
- const refreshDenoms = rankDenominationForRefresh(denoms.list);
/**
- * FIXME: looking for refresh AFTER selecting greedy is not optimal
+ * Info of Coins that were selected.
*/
- const refreshCoin = searchBestRefreshCoin(
- depositDenoms,
- refreshDenoms,
- gap,
- mode,
- );
-
- if (refreshCoin) {
- const fee = Amounts.sub(result.effective, result.raw).amount;
- const effective = Amounts.add(
- result.effective,
- refreshCoin.refreshEffective,
- ).amount;
- const raw = Amounts.sub(effective, fee, refreshCoin.totalFee).amount;
- //found with change
- return {
- effective,
- raw,
- refresh: refreshCoin,
- };
- }
+ coins: SelectedPeerCoin[];
- // there is a gap, but no refresh coin was found
- return result;
-}
-
-export async function getMaxDepositAmount(
- ws: InternalWalletState,
- req: GetAmountRequest,
-): Promise<AmountResponse> {
- // const filter = getCoinsFilter(req);
-
- const denoms = await getAvailableDenoms(
- ws,
- TransactionType.Deposit,
- req.currency,
- {},
- );
-
- const result = getMaxDepositAmountForAvailableCoins(denoms, req.currency);
- return {
- effectiveAmount: Amounts.stringify(result.effective),
- rawAmount: Amounts.stringify(result.raw),
- };
+ /**
+ * How much of the deposit fees is the customer paying?
+ */
+ depositFees: AmountJson;
}
-export function getMaxDepositAmountForAvailableCoins(
- denoms: AvailableCoins,
- currency: string,
-) {
- const zero = Amounts.zeroOfCurrency(currency);
- if (!denoms.list.length) {
- // no coins in the database
- return { effective: zero, raw: zero };
- }
-
- const result = getTotalEffectiveAndRawForDeposit(
- denoms.list.map((info) => {
- return { info, size: info.totalAvailable ?? 0 };
- }),
- currency,
- );
-
- const wireFee = Object.values(denoms.exchanges)[0]?.wireFee ?? zero;
- result.raw = Amounts.sub(result.raw, wireFee).amount;
-
- return result;
-}
+/**
+ * Information about a selected coin for peer to peer payments.
+ */
+export interface PeerCoinInfo {
+ /**
+ * Public key of the coin.
+ */
+ coinPub: string;
-export async function convertPeerPushAmount(
- ws: InternalWalletState,
- req: ConvertAmountRequest,
-): Promise<AmountResponse> {
- throw Error("to be implemented after 1.0");
-}
-export async function getMaxPeerPushAmount(
- ws: InternalWalletState,
- req: GetAmountRequest,
-): Promise<AmountResponse> {
- throw Error("to be implemented after 1.0");
-}
-export async function convertWithdrawalAmount(
- ws: InternalWalletState,
- req: ConvertAmountRequest,
-): Promise<AmountResponse> {
- const amount = Amounts.parseOrThrow(req.amount);
+ coinPriv: string;
- const denoms = await getAvailableDenoms(
- ws,
- TransactionType.Withdrawal,
- amount.currency,
- {},
- );
+ /**
+ * Deposit fee for the coin.
+ */
+ feeDeposit: AmountJson;
- const result = convertWithdrawalAmountFromAvailableCoins(
- denoms,
- amount,
- req.type,
- );
+ value: AmountJson;
- return {
- effectiveAmount: Amounts.stringify(result.effective),
- rawAmount: Amounts.stringify(result.raw),
- };
-}
+ denomPubHash: string;
-export function convertWithdrawalAmountFromAvailableCoins(
- denoms: AvailableCoins,
- amount: AmountJson,
- mode: TransactionAmountMode,
-) {
- const zero = Amounts.zeroOfCurrency(amount.currency);
- if (!denoms.list.length) {
- // no coins in the database
- return { effective: zero, raw: zero };
- }
- const withdrawDenoms = rankDenominationForWithdrawals(denoms.list, mode);
+ denomSig: UnblindedSignature;
- const selected = selectGreedyCoins(withdrawDenoms, amount);
+ maxAge: number;
- return getTotalEffectiveAndRawForWithdrawal(selected.coins, amount.currency);
+ ageCommitmentProof?: AgeCommitmentProof;
}
-/** *****************************************************
- * HELPERS
- * *****************************************************
- */
-
-/**
- *
- * @param depositDenoms
- * @param refreshDenoms
- * @param amount
- * @param mode
- * @returns
- */
-function searchBestRefreshCoin(
- depositDenoms: SelectableElement[],
- refreshDenoms: Record<string, SelectableElement[]>,
- amount: AmountJson,
- mode: TransactionAmountMode,
-): RefreshChoice | undefined {
- let choice: RefreshChoice | undefined = undefined;
- let refreshIdx = 0;
- refreshIteration: while (refreshIdx < depositDenoms.length) {
- const d = depositDenoms[refreshIdx];
-
- const denomContribution =
- mode === TransactionAmountMode.Effective
- ? d.value
- : Amounts.sub(d.value, d.info.denomRefresh, d.info.denomDeposit).amount;
-
- const changeAfterDeposit = Amounts.sub(denomContribution, amount).amount;
- if (Amounts.isZero(changeAfterDeposit)) {
- //this coin is not big enough to use for refresh
- //since the list is sorted, we can break here
- break refreshIteration;
- }
-
- const withdrawDenoms = refreshDenoms[d.info.exchangeBaseUrl];
- const change = selectGreedyCoins(withdrawDenoms, changeAfterDeposit);
-
- const zero = Amounts.zeroOfCurrency(amount.currency);
- const withdrawChangeFee = change.coins.reduce((cur, prev) => {
- return Amounts.add(
- cur,
- Amounts.mult(prev.info.denomWithdraw, prev.size).amount,
- ).amount;
- }, zero);
-
- const withdrawChangeValue = change.coins.reduce((cur, prev) => {
- return Amounts.add(cur, Amounts.mult(prev.info.value, prev.size).amount)
- .amount;
- }, zero);
-
- const totalFee = Amounts.add(
- d.info.denomDeposit,
- d.info.denomRefresh,
- withdrawChangeFee,
- ).amount;
+export type SelectPeerCoinsResult =
+ | { type: "success"; result: PeerCoinSelectionDetails }
+ | {
+ type: "failure";
+ insufficientBalanceDetails: PayPeerInsufficientBalanceDetails;
+ };
- if (!choice || Amounts.cmp(totalFee, choice.totalFee) === -1) {
- //found cheaper change
- choice = {
- gap: amount,
- totalFee: totalFee,
- totalChangeValue: change.totalValue, //change after refresh
- refreshEffective: Amounts.sub(d.info.value, withdrawChangeValue).amount, // what of the denom used is not recovered
- selected: d.info,
- coins: change.coins,
- };
- }
- refreshIdx++;
- }
- if (choice) {
- if (LOG_REFRESH) {
- const logInfo = choice.coins.map((c) => {
- return `${Amounts.stringifyValue(c.info.id)} x ${c.size}`;
- });
- console.log(
- "refresh used:",
- Amounts.stringifyValue(choice.selected.value),
- "change:",
- logInfo.join(", "),
- "fee:",
- Amounts.stringifyValue(choice.totalFee),
- "refreshEffective:",
- Amounts.stringifyValue(choice.refreshEffective),
- "totalChangeValue:",
- Amounts.stringifyValue(choice.totalChangeValue),
- );
- }
- }
- return choice;
+export interface PeerCoinRepair {
+ exchangeBaseUrl: string;
+ coinPubs: CoinPublicKeyString[];
+ contribs: AmountJson[];
}
-/**
- * Returns a copy of the list sorted for the best denom to withdraw first
- *
- * @param denoms
- * @returns
- */
-function rankDenominationForWithdrawals(
- denoms: CoinInfo[],
- mode: TransactionAmountMode,
-): SelectableElement[] {
- const copyList = [...denoms];
- /**
- * Rank coins
- */
- copyList.sort((d1, d2) => {
- // the best coin to use is
- // 1.- the one that contrib more and pay less fee
- // 2.- it takes more time before expires
-
- //different exchanges may have different wireFee
- //ranking should take the relative contribution in the exchange
- //which is (value - denomFee / fixedFee)
- const rate1 = Amounts.divmod(d1.value, d1.denomWithdraw).quotient;
- const rate2 = Amounts.divmod(d2.value, d2.denomWithdraw).quotient;
- const contribCmp = rate1 === rate2 ? 0 : rate1 < rate2 ? 1 : -1;
- return (
- contribCmp ||
- Duration.cmp(d1.duration, d2.duration) ||
- strcmp(d1.id, d2.id)
- );
- });
-
- return copyList.map((info) => {
- switch (mode) {
- case TransactionAmountMode.Effective: {
- //if the user instructed "effective" then we need to selected
- //greedy total coin value
- return {
- info,
- value: info.value,
- total: Number.MAX_SAFE_INTEGER,
- };
- }
- case TransactionAmountMode.Raw: {
- //if the user instructed "raw" then we need to selected
- //greedy total coin raw amount (without fee)
- return {
- info,
- value: Amounts.add(info.value, info.denomWithdraw).amount,
- total: Number.MAX_SAFE_INTEGER,
- };
- }
- }
- });
-}
+export interface PeerCoinSelectionRequest {
+ instructedAmount: AmountJson;
-/**
- * Returns a copy of the list sorted for the best denom to deposit first
- *
- * @param denoms
- * @returns
- */
-function rankDenominationForDeposit(
- denoms: CoinInfo[],
- mode: TransactionAmountMode,
-): SelectableElement[] {
- const copyList = [...denoms];
/**
- * Rank coins
+ * Instruct the coin selection to repair this coin
+ * selection instead of selecting completely new coins.
*/
- copyList.sort((d1, d2) => {
- // the best coin to use is
- // 1.- the one that contrib more and pay less fee
- // 2.- it takes more time before expires
-
- //different exchanges may have different wireFee
- //ranking should take the relative contribution in the exchange
- //which is (value - denomFee / fixedFee)
- const rate1 = Amounts.divmod(d1.value, d1.denomDeposit).quotient;
- const rate2 = Amounts.divmod(d2.value, d2.denomDeposit).quotient;
- const contribCmp = rate1 === rate2 ? 0 : rate1 < rate2 ? 1 : -1;
- return (
- contribCmp ||
- Duration.cmp(d1.duration, d2.duration) ||
- strcmp(d1.id, d2.id)
- );
- });
-
- return copyList.map((info) => {
- switch (mode) {
- case TransactionAmountMode.Effective: {
- //if the user instructed "effective" then we need to selected
- //greedy total coin value
- return {
- info,
- value: info.value,
- total: info.totalAvailable ?? 0,
- };
- }
- case TransactionAmountMode.Raw: {
- //if the user instructed "raw" then we need to selected
- //greedy total coin raw amount (without fee)
- return {
- info,
- value: Amounts.sub(info.value, info.denomDeposit).amount,
- total: info.totalAvailable ?? 0,
- };
- }
- }
- });
+ repair?: PeerCoinRepair;
}
-/**
- * Returns a copy of the list sorted for the best denom to withdraw first
- *
- * @param denoms
- * @returns
- */
-function rankDenominationForRefresh(
- denoms: CoinInfo[],
-): Record<string, SelectableElement[]> {
- const groupByExchange: Record<string, CoinInfo[]> = {};
- for (const d of denoms) {
- if (!groupByExchange[d.exchangeBaseUrl]) {
- groupByExchange[d.exchangeBaseUrl] = [];
- }
- groupByExchange[d.exchangeBaseUrl].push(d);
- }
-
- const result: Record<string, SelectableElement[]> = {};
- for (const d of denoms) {
- result[d.exchangeBaseUrl] = rankDenominationForWithdrawals(
- groupByExchange[d.exchangeBaseUrl],
- TransactionAmountMode.Raw,
- );
+export async function selectPeerCoins(
+ ws: InternalWalletState,
+ req: PeerCoinSelectionRequest,
+): Promise<SelectPeerCoinsResult> {
+ const instructedAmount = req.instructedAmount;
+ if (Amounts.isZero(instructedAmount)) {
+ // Other parts of the code assume that we have at least
+ // one coin to spend.
+ throw new Error("amount of zero not allowed");
}
- return result;
-}
-
-interface SelectableElement {
- total: number;
- value: AmountJson;
- info: CoinInfo;
-}
-
-function selectGreedyCoins(
- coins: SelectableElement[],
- limit: AmountJson,
-): SelectedCoins {
- const result: SelectedCoins = {
- totalValue: Amounts.zeroOfCurrency(limit.currency),
- coins: [],
- };
- if (!coins.length) return result;
-
- let denomIdx = 0;
- iterateDenoms: while (denomIdx < coins.length) {
- const denom = coins[denomIdx];
- // let total = denom.total;
- const left = Amounts.sub(limit, result.totalValue).amount;
+ return await ws.db
+ .mktx((x) => [
+ x.exchanges,
+ x.contractTerms,
+ x.coins,
+ x.coinAvailability,
+ x.denominations,
+ x.refreshGroups,
+ x.peerPushPaymentInitiations,
+ ])
+ .runReadWrite(async (tx) => {
+ const exchanges = await tx.exchanges.iter().toArray();
+ const exchangeFeeGap: { [url: string]: AmountJson } = {};
+ const currency = Amounts.currencyOf(instructedAmount);
+ for (const exch of exchanges) {
+ if (exch.detailsPointer?.currency !== currency) {
+ continue;
+ }
+ // FIXME: Can't we do this faster by using coinAvailability?
+ const coins = (
+ await tx.coins.indexes.byBaseUrl.getAll(exch.baseUrl)
+ ).filter((x) => x.status === CoinStatus.Fresh);
+ const coinInfos: PeerCoinInfo[] = [];
+ for (const coin of coins) {
+ const denom = await ws.getDenomInfo(
+ ws,
+ tx,
+ coin.exchangeBaseUrl,
+ coin.denomPubHash,
+ );
+ if (!denom) {
+ throw Error("denom not found");
+ }
+ coinInfos.push({
+ coinPub: coin.coinPub,
+ feeDeposit: Amounts.parseOrThrow(denom.feeDeposit),
+ value: Amounts.parseOrThrow(denom.value),
+ denomPubHash: denom.denomPubHash,
+ coinPriv: coin.coinPriv,
+ denomSig: coin.denomSig,
+ maxAge: coin.maxAge,
+ ageCommitmentProof: coin.ageCommitmentProof,
+ });
+ }
+ if (coinInfos.length === 0) {
+ continue;
+ }
+ coinInfos.sort(
+ (o1, o2) =>
+ -Amounts.cmp(o1.value, o2.value) ||
+ strcmp(o1.denomPubHash, o2.denomPubHash),
+ );
+ let amountAcc = Amounts.zeroOfCurrency(currency);
+ let depositFeesAcc = Amounts.zeroOfCurrency(currency);
+ const resCoins: {
+ coinPub: string;
+ coinPriv: string;
+ contribution: AmountString;
+ denomPubHash: string;
+ denomSig: UnblindedSignature;
+ ageCommitmentProof: AgeCommitmentProof | undefined;
+ }[] = [];
+ let lastDepositFee = Amounts.zeroOfCurrency(currency);
+
+ if (req.repair) {
+ for (let i = 0; i < req.repair.coinPubs.length; i++) {
+ const contrib = req.repair.contribs[i];
+ const coin = await tx.coins.get(req.repair.coinPubs[i]);
+ if (!coin) {
+ throw Error("repair not possible, coin not found");
+ }
+ const denom = await ws.getDenomInfo(
+ ws,
+ tx,
+ coin.exchangeBaseUrl,
+ coin.denomPubHash,
+ );
+ checkDbInvariant(!!denom);
+ resCoins.push({
+ coinPriv: coin.coinPriv,
+ coinPub: coin.coinPub,
+ contribution: Amounts.stringify(contrib),
+ denomPubHash: coin.denomPubHash,
+ denomSig: coin.denomSig,
+ ageCommitmentProof: coin.ageCommitmentProof,
+ });
+ const depositFee = Amounts.parseOrThrow(denom.feeDeposit);
+ lastDepositFee = depositFee;
+ amountAcc = Amounts.add(
+ amountAcc,
+ Amounts.sub(contrib, depositFee).amount,
+ ).amount;
+ depositFeesAcc = Amounts.add(depositFeesAcc, depositFee).amount;
+ }
+ }
- if (Amounts.isZero(denom.value)) {
- // 0 contribution denoms should be the last
- break iterateDenoms;
- }
+ for (const coin of coinInfos) {
+ if (Amounts.cmp(amountAcc, instructedAmount) >= 0) {
+ break;
+ }
+ const gap = Amounts.add(
+ coin.feeDeposit,
+ Amounts.sub(instructedAmount, amountAcc).amount,
+ ).amount;
+ const contrib = Amounts.min(gap, coin.value);
+ amountAcc = Amounts.add(
+ amountAcc,
+ Amounts.sub(contrib, coin.feeDeposit).amount,
+ ).amount;
+ depositFeesAcc = Amounts.add(depositFeesAcc, coin.feeDeposit).amount;
+ resCoins.push({
+ coinPriv: coin.coinPriv,
+ coinPub: coin.coinPub,
+ contribution: Amounts.stringify(contrib),
+ denomPubHash: coin.denomPubHash,
+ denomSig: coin.denomSig,
+ ageCommitmentProof: coin.ageCommitmentProof,
+ });
+ lastDepositFee = coin.feeDeposit;
+ }
+ if (Amounts.cmp(amountAcc, instructedAmount) >= 0) {
+ const res: PeerCoinSelectionDetails = {
+ exchangeBaseUrl: exch.baseUrl,
+ coins: resCoins,
+ depositFees: depositFeesAcc,
+ };
+ return { type: "success", result: res };
+ }
+ const diff = Amounts.sub(instructedAmount, amountAcc).amount;
+ exchangeFeeGap[exch.baseUrl] = Amounts.add(lastDepositFee, diff).amount;
- //use Amounts.divmod instead of iterate
- const div = Amounts.divmod(left, denom.value);
- const size = Math.min(div.quotient, denom.total);
- if (size > 0) {
- const mul = Amounts.mult(denom.value, size).amount;
- const progress = Amounts.add(result.totalValue, mul).amount;
+ continue;
+ }
- result.totalValue = progress;
- result.coins.push({ info: denom.info, size });
- denom.total = denom.total - size;
- }
+ // We were unable to select coins.
+ // Now we need to produce error details.
- //go next denom
- denomIdx++;
- }
+ const infoGeneral = await getPeerPaymentBalanceDetailsInTx(ws, tx, {
+ currency,
+ });
- return result;
-}
+ const perExchange: PayPeerInsufficientBalanceDetails["perExchange"] = {};
-type AmountWithFee = { raw: AmountJson; effective: AmountJson };
-type AmountAndRefresh = AmountWithFee & { refresh?: RefreshChoice };
+ let maxFeeGapEstimate = Amounts.zeroOfCurrency(currency);
-export function getTotalEffectiveAndRawForDeposit(
- list: { info: CoinInfo; size: number }[],
- currency: string,
-): AmountWithFee {
- const init = {
- raw: Amounts.zeroOfCurrency(currency),
- effective: Amounts.zeroOfCurrency(currency),
- };
- return list.reduce((prev, cur) => {
- const ef = Amounts.mult(cur.info.value, cur.size).amount;
- const rw = Amounts.mult(
- Amounts.sub(cur.info.value, cur.info.denomDeposit).amount,
- cur.size,
- ).amount;
+ for (const exch of exchanges) {
+ if (exch.detailsPointer?.currency !== currency) {
+ continue;
+ }
+ const infoExchange = await getPeerPaymentBalanceDetailsInTx(ws, tx, {
+ currency,
+ restrictExchangeTo: exch.baseUrl,
+ });
+ let gap =
+ exchangeFeeGap[exch.baseUrl] ?? Amounts.zeroOfCurrency(currency);
+ if (Amounts.cmp(infoExchange.balanceMaterial, instructedAmount) < 0) {
+ // Show fee gap only if we should've been able to pay with the material amount
+ gap = Amounts.zeroOfCurrency(currency);
+ }
+ perExchange[exch.baseUrl] = {
+ balanceAvailable: Amounts.stringify(infoExchange.balanceAvailable),
+ balanceMaterial: Amounts.stringify(infoExchange.balanceMaterial),
+ feeGapEstimate: Amounts.stringify(gap),
+ };
- prev.effective = Amounts.add(prev.effective, ef).amount;
- prev.raw = Amounts.add(prev.raw, rw).amount;
- return prev;
- }, init);
-}
+ maxFeeGapEstimate = Amounts.max(maxFeeGapEstimate, gap);
+ }
-function getTotalEffectiveAndRawForWithdrawal(
- list: { info: CoinInfo; size: number }[],
- currency: string,
-): AmountWithFee {
- const init = {
- raw: Amounts.zeroOfCurrency(currency),
- effective: Amounts.zeroOfCurrency(currency),
- };
- return list.reduce((prev, cur) => {
- const ef = Amounts.mult(cur.info.value, cur.size).amount;
- const rw = Amounts.mult(
- Amounts.add(cur.info.value, cur.info.denomWithdraw).amount,
- cur.size,
- ).amount;
+ const errDetails: PayPeerInsufficientBalanceDetails = {
+ amountRequested: Amounts.stringify(instructedAmount),
+ balanceAvailable: Amounts.stringify(infoGeneral.balanceAvailable),
+ balanceMaterial: Amounts.stringify(infoGeneral.balanceMaterial),
+ feeGapEstimate: Amounts.stringify(maxFeeGapEstimate),
+ perExchange,
+ };
- prev.effective = Amounts.add(prev.effective, ef).amount;
- prev.raw = Amounts.add(prev.raw, rw).amount;
- return prev;
- }, init);
+ return { type: "failure", insufficientBalanceDetails: errDetails };
+ });
}
diff --git a/packages/taler-wallet-core/src/util/instructedAmountConversion.test.ts b/packages/taler-wallet-core/src/util/instructedAmountConversion.test.ts
new file mode 100644
index 000000000..de8515d09
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/instructedAmountConversion.test.ts
@@ -0,0 +1,763 @@
+/*
+ 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 {
+ AbsoluteTime,
+ AgeRestriction,
+ AmountJson,
+ Amounts,
+ Duration,
+ TransactionAmountMode,
+} from "@gnu-taler/taler-util";
+import test, { ExecutionContext } from "ava";
+import { CoinInfo } from "./coinSelection.js";
+import { convertDepositAmountForAvailableCoins, getMaxDepositAmountForAvailableCoins, convertWithdrawalAmountFromAvailableCoins } from "./instructedAmountConversion.js";
+
+function makeCurrencyHelper(currency: string) {
+ return (sx: TemplateStringsArray, ...vx: any[]) => {
+ const s = String.raw({ raw: sx }, ...vx);
+ return Amounts.parseOrThrow(`${currency}:${s}`);
+ };
+}
+
+const kudos = makeCurrencyHelper("kudos");
+
+function defaultFeeConfig(value: AmountJson, totalAvailable: number): CoinInfo {
+ return {
+ id: Amounts.stringify(value),
+ denomDeposit: kudos`0.01`,
+ denomRefresh: kudos`0.01`,
+ denomWithdraw: kudos`0.01`,
+ exchangeBaseUrl: "1",
+ duration: Duration.getForever(),
+ exchangePurse: undefined,
+ exchangeWire: undefined,
+ maxAge: AgeRestriction.AGE_UNRESTRICTED,
+ totalAvailable,
+ value,
+ };
+}
+type Coin = [AmountJson, number];
+
+/**
+ * Making a deposit with effective amount
+ *
+ */
+
+test("deposit effective 2", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`2`,
+ TransactionAmountMode.Effective,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "2");
+ t.is(Amounts.stringifyValue(result.raw), "1.99");
+});
+
+test("deposit effective 10", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`10`,
+ TransactionAmountMode.Effective,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "10");
+ t.is(Amounts.stringifyValue(result.raw), "9.98");
+});
+
+test("deposit effective 24", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`24`,
+ TransactionAmountMode.Effective,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "24");
+ t.is(Amounts.stringifyValue(result.raw), "23.94");
+});
+
+test("deposit effective 40", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`40`,
+ TransactionAmountMode.Effective,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "35");
+ t.is(Amounts.stringifyValue(result.raw), "34.9");
+});
+
+test("deposit with wire fee effective 2", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {
+ one: {
+ wireFee: kudos`0.1`,
+ purseFee: kudos`0.00`,
+ creditDeadline: AbsoluteTime.never(),
+ debitDeadline: AbsoluteTime.never(),
+ },
+ },
+ },
+ kudos`2`,
+ TransactionAmountMode.Effective,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "2");
+ t.is(Amounts.stringifyValue(result.raw), "1.89");
+});
+
+/**
+ * Making a deposit with raw amount, using the result from effective
+ *
+ */
+
+test("deposit raw 1.99 (effective 2)", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`1.99`,
+ TransactionAmountMode.Raw,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "2");
+ t.is(Amounts.stringifyValue(result.raw), "1.99");
+});
+
+test("deposit raw 9.98 (effective 10)", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`9.98`,
+ TransactionAmountMode.Raw,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "10");
+ t.is(Amounts.stringifyValue(result.raw), "9.98");
+});
+
+test("deposit raw 23.94 (effective 24)", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`23.94`,
+ TransactionAmountMode.Raw,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "24");
+ t.is(Amounts.stringifyValue(result.raw), "23.94");
+});
+
+test("deposit raw 34.9 (effective 40)", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`34.9`,
+ TransactionAmountMode.Raw,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "35");
+ t.is(Amounts.stringifyValue(result.raw), "34.9");
+});
+
+test("deposit with wire fee raw 2", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {
+ one: {
+ wireFee: kudos`0.1`,
+ purseFee: kudos`0.00`,
+ creditDeadline: AbsoluteTime.never(),
+ debitDeadline: AbsoluteTime.never(),
+ },
+ },
+ },
+ kudos`2`,
+ TransactionAmountMode.Effective,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "2");
+ t.is(Amounts.stringifyValue(result.raw), "1.89");
+});
+
+/**
+ * Calculating the max amount possible to deposit
+ *
+ */
+
+test("deposit max 35", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = getMaxDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {
+ "2": {
+ wireFee: kudos`0.00`,
+ purseFee: kudos`0.00`,
+ creditDeadline: AbsoluteTime.never(),
+ debitDeadline: AbsoluteTime.never(),
+ },
+ },
+ },
+ "KUDOS",
+ );
+ t.is(Amounts.stringifyValue(result.raw), "34.9");
+ t.is(Amounts.stringifyValue(result.effective), "35");
+});
+
+test("deposit max 35 with wirefee", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = getMaxDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {
+ "2": {
+ wireFee: kudos`1`,
+ purseFee: kudos`0.00`,
+ creditDeadline: AbsoluteTime.never(),
+ debitDeadline: AbsoluteTime.never(),
+ },
+ },
+ },
+ "KUDOS",
+ );
+ t.is(Amounts.stringifyValue(result.raw), "33.9");
+ t.is(Amounts.stringifyValue(result.effective), "35");
+});
+
+test("deposit max repeated denom", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 1],
+ [kudos`2`, 1],
+ [kudos`5`, 1],
+ ];
+ const result = getMaxDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {
+ "2": {
+ wireFee: kudos`0.00`,
+ purseFee: kudos`0.00`,
+ creditDeadline: AbsoluteTime.never(),
+ debitDeadline: AbsoluteTime.never(),
+ },
+ },
+ },
+ "KUDOS",
+ );
+ t.is(Amounts.stringifyValue(result.raw), "8.97");
+ t.is(Amounts.stringifyValue(result.effective), "9");
+});
+
+/**
+ * Making a withdrawal with effective amount
+ *
+ */
+
+test("withdraw effective 2", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertWithdrawalAmountFromAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`2`,
+ TransactionAmountMode.Effective,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "2");
+ t.is(Amounts.stringifyValue(result.raw), "2.01");
+});
+
+test("withdraw effective 10", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertWithdrawalAmountFromAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`10`,
+ TransactionAmountMode.Effective,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "10");
+ t.is(Amounts.stringifyValue(result.raw), "10.02");
+});
+
+test("withdraw effective 24", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertWithdrawalAmountFromAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`24`,
+ TransactionAmountMode.Effective,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "24");
+ t.is(Amounts.stringifyValue(result.raw), "24.06");
+});
+
+test("withdraw effective 40", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertWithdrawalAmountFromAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`40`,
+ TransactionAmountMode.Effective,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "40");
+ t.is(Amounts.stringifyValue(result.raw), "40.08");
+});
+
+/**
+ * Making a deposit with raw amount, using the result from effective
+ *
+ */
+
+test("withdraw raw 2.01 (effective 2)", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertWithdrawalAmountFromAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`2.01`,
+ TransactionAmountMode.Raw,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "2");
+ t.is(Amounts.stringifyValue(result.raw), "2.01");
+});
+
+test("withdraw raw 10.02 (effective 10)", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertWithdrawalAmountFromAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`10.02`,
+ TransactionAmountMode.Raw,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "10");
+ t.is(Amounts.stringifyValue(result.raw), "10.02");
+});
+
+test("withdraw raw 24.06 (effective 24)", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertWithdrawalAmountFromAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`24.06`,
+ TransactionAmountMode.Raw,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "24");
+ t.is(Amounts.stringifyValue(result.raw), "24.06");
+});
+
+test("withdraw raw 40.08 (effective 40)", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertWithdrawalAmountFromAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`40.08`,
+ TransactionAmountMode.Raw,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "40");
+ t.is(Amounts.stringifyValue(result.raw), "40.08");
+});
+
+test("withdraw raw 25", (t) => {
+ const coinList: Coin[] = [
+ [kudos`0.1`, 0],
+ [kudos`1`, 0],
+ [kudos`2`, 0],
+ [kudos`5`, 0],
+ ];
+ const result = convertWithdrawalAmountFromAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`25`,
+ TransactionAmountMode.Raw,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "24.8");
+ t.is(Amounts.stringifyValue(result.raw), "24.94");
+});
+
+test("withdraw effective 24.8 (raw 25)", (t) => {
+ const coinList: Coin[] = [
+ [kudos`0.1`, 0],
+ [kudos`1`, 0],
+ [kudos`2`, 0],
+ [kudos`5`, 0],
+ ];
+ const result = convertWithdrawalAmountFromAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`24.8`,
+ TransactionAmountMode.Effective,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "24.8");
+ t.is(Amounts.stringifyValue(result.raw), "24.94");
+});
+
+/**
+ * Making a deposit with refresh
+ *
+ */
+
+test("deposit with refresh: effective 3", (t) => {
+ const coinList: Coin[] = [
+ [kudos`0.1`, 0],
+ [kudos`1`, 0],
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`3`,
+ TransactionAmountMode.Effective,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "3.1");
+ t.is(Amounts.stringifyValue(result.raw), "2.98");
+ expectDefined(t, result.refresh);
+ //FEES
+ //deposit 2 x 0.01
+ //refresh 1 x 0.01
+ //withdraw 9 x 0.01
+ //-----------------
+ //op 0.12
+
+ //coins sent 2 x 2.0
+ //coins recv 9 x 0.1
+ //-------------------
+ //effective 3.10
+ //raw 2.98
+ t.is(Amounts.stringifyValue(result.refresh.selected.id), "2");
+ t.deepEqual(asCoinList(result.refresh.coins), [[kudos`0.1`, 9]]);
+});
+
+test("deposit with refresh: raw 2.98 (effective 3)", (t) => {
+ const coinList: Coin[] = [
+ [kudos`0.1`, 0],
+ [kudos`1`, 0],
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`2.98`,
+ TransactionAmountMode.Raw,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "3.2");
+ t.is(Amounts.stringifyValue(result.raw), "3.09");
+ expectDefined(t, result.refresh);
+ //FEES
+ //deposit 1 x 0.01
+ //refresh 1 x 0.01
+ //withdraw 8 x 0.01
+ //-----------------
+ //op 0.10
+
+ //coins sent 1 x 2.0
+ //coins recv 8 x 0.1
+ //-------------------
+ //effective 3.20
+ //raw 3.09
+ t.is(Amounts.stringifyValue(result.refresh.selected.id), "2");
+ t.deepEqual(asCoinList(result.refresh.coins), [[kudos`0.1`, 8]]);
+});
+
+test("deposit with refresh: effective 3.2 (raw 2.98)", (t) => {
+ const coinList: Coin[] = [
+ [kudos`0.1`, 0],
+ [kudos`1`, 0],
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`3.2`,
+ TransactionAmountMode.Effective,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "3.3");
+ t.is(Amounts.stringifyValue(result.raw), "3.2");
+ expectDefined(t, result.refresh);
+ //FEES
+ //deposit 2 x 0.01
+ //refresh 1 x 0.01
+ //withdraw 7 x 0.01
+ //-----------------
+ //op 0.10
+
+ //coins sent 2 x 2.0
+ //coins recv 7 x 0.1
+ //-------------------
+ //effective 3.30
+ //raw 3.20
+ t.is(Amounts.stringifyValue(result.refresh.selected.id), "2");
+ t.deepEqual(asCoinList(result.refresh.coins), [[kudos`0.1`, 7]]);
+});
+
+function expectDefined<T>(
+ t: ExecutionContext,
+ v: T | undefined,
+): asserts v is T {
+ t.assert(v !== undefined);
+}
+
+function asCoinList(v: { info: CoinInfo; size: number }[]): any {
+ return v.map((c) => {
+ return [c.info.value, c.size];
+ });
+}
+
+/**
+ * regression tests
+ */
+
+test("demo: withdraw raw 25", (t) => {
+ const coinList: Coin[] = [
+ [kudos`0.1`, 0],
+ [kudos`1`, 0],
+ [kudos`2`, 0],
+ [kudos`5`, 0],
+ [kudos`10`, 0],
+ ];
+ const result = convertWithdrawalAmountFromAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`25`,
+ TransactionAmountMode.Raw,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "24.8");
+ t.is(Amounts.stringifyValue(result.raw), "24.92");
+ // coins received
+ // 8 x 0.1
+ // 2 x 0.2
+ // 2 x 10.0
+ // total effective 24.8
+ // fee 12 x 0.01 = 0.12
+ // total raw 24.92
+ // left in reserve 25 - 24.92 == 0.08
+
+ //current wallet impl: hides the left in reserve fee
+ //shows fee = 0.2
+});
+
+test("demo: deposit max after withdraw raw 25", (t) => {
+ const coinList: Coin[] = [
+ [kudos`0.1`, 8],
+ [kudos`1`, 0],
+ [kudos`2`, 2],
+ [kudos`5`, 0],
+ [kudos`10`, 2],
+ ];
+ const result = getMaxDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {
+ one: {
+ wireFee: kudos`0.01`,
+ purseFee: kudos`0.00`,
+ creditDeadline: AbsoluteTime.never(),
+ debitDeadline: AbsoluteTime.never(),
+ },
+ },
+ },
+ "KUDOS",
+ );
+ t.is(Amounts.stringifyValue(result.effective), "24.8");
+ t.is(Amounts.stringifyValue(result.raw), "24.67");
+
+ // 8 x 0.1
+ // 2 x 0.2
+ // 2 x 10.0
+ // total effective 24.8
+ // deposit fee 12 x 0.01 = 0.12
+ // wire fee 0.01
+ // total raw: 24.8 - 0.13 = 24.67
+
+ // current wallet impl fee 0.14
+});
+
+test("demo: withdraw raw 13", (t) => {
+ const coinList: Coin[] = [
+ [kudos`0.1`, 0],
+ [kudos`1`, 0],
+ [kudos`2`, 0],
+ [kudos`5`, 0],
+ [kudos`10`, 0],
+ ];
+ const result = convertWithdrawalAmountFromAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`13`,
+ TransactionAmountMode.Raw,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "12.8");
+ t.is(Amounts.stringifyValue(result.raw), "12.9");
+ // coins received
+ // 8 x 0.1
+ // 1 x 0.2
+ // 1 x 10.0
+ // total effective 12.8
+ // fee 10 x 0.01 = 0.10
+ // total raw 12.9
+ // left in reserve 13 - 12.9 == 0.1
+
+ //current wallet impl: hides the left in reserve fee
+ //shows fee = 0.2
+});
+
+test("demo: deposit max after withdraw raw 13", (t) => {
+ const coinList: Coin[] = [
+ [kudos`0.1`, 8],
+ [kudos`1`, 0],
+ [kudos`2`, 1],
+ [kudos`5`, 0],
+ [kudos`10`, 1],
+ ];
+ const result = getMaxDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {
+ one: {
+ wireFee: kudos`0.01`,
+ purseFee: kudos`0.00`,
+ creditDeadline: AbsoluteTime.never(),
+ debitDeadline: AbsoluteTime.never(),
+ },
+ },
+ },
+ "KUDOS",
+ );
+ t.is(Amounts.stringifyValue(result.effective), "12.8");
+ t.is(Amounts.stringifyValue(result.raw), "12.69");
+
+ // 8 x 0.1
+ // 1 x 0.2
+ // 1 x 10.0
+ // total effective 12.8
+ // deposit fee 10 x 0.01 = 0.10
+ // wire fee 0.01
+ // total raw: 12.8 - 0.11 = 12.69
+
+ // current wallet impl fee 0.14
+});
diff --git a/packages/taler-wallet-core/src/util/instructedAmountConversion.ts b/packages/taler-wallet-core/src/util/instructedAmountConversion.ts
new file mode 100644
index 000000000..bd02e7b22
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/instructedAmountConversion.ts
@@ -0,0 +1,849 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 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 {
+ AbsoluteTime,
+ AgeRestriction,
+ AmountJson,
+ AmountResponse,
+ Amounts,
+ ConvertAmountRequest,
+ Duration,
+ GetAmountRequest,
+ GetPlanForOperationRequest,
+ TransactionAmountMode,
+ TransactionType,
+ parsePaytoUri,
+ strcmp,
+} from "@gnu-taler/taler-util";
+import { checkDbInvariant } from "./invariants.js";
+import {
+ DenominationRecord,
+ InternalWalletState,
+ getExchangeDetails,
+} from "../index.js";
+import { CoinInfo } from "./coinSelection.js";
+import { GlobalIDB } from "@gnu-taler/idb-bridge";
+
+/**
+ * If the operation going to be plan subtracts
+ * or adds amount in the wallet db
+ */
+export enum OperationType {
+ Credit = "credit",
+ Debit = "debit",
+}
+
+// FIXME: Name conflict ...
+interface ExchangeInfo {
+ wireFee: AmountJson | undefined;
+ purseFee: AmountJson | undefined;
+ creditDeadline: AbsoluteTime;
+ debitDeadline: AbsoluteTime;
+}
+
+function getOperationType(txType: TransactionType): OperationType {
+ const operationType =
+ txType === TransactionType.Withdrawal
+ ? OperationType.Credit
+ : txType === TransactionType.Deposit
+ ? OperationType.Debit
+ : undefined;
+ if (!operationType) {
+ throw Error(`operation type ${txType} not yet supported`);
+ }
+ return operationType;
+}
+
+interface SelectedCoins {
+ totalValue: AmountJson;
+ coins: { info: CoinInfo; size: number }[];
+ refresh?: RefreshChoice;
+}
+
+function getCoinsFilter(req: GetPlanForOperationRequest): CoinsFilter {
+ switch (req.type) {
+ case TransactionType.Withdrawal: {
+ return {
+ exchanges:
+ req.exchangeUrl === undefined ? undefined : [req.exchangeUrl],
+ };
+ }
+ case TransactionType.Deposit: {
+ const payto = parsePaytoUri(req.account);
+ if (!payto) {
+ throw Error(`wrong payto ${req.account}`);
+ }
+ return {
+ wireMethod: payto.targetType,
+ };
+ }
+ }
+}
+
+interface RefreshChoice {
+ /**
+ * Amount that need to be covered
+ */
+ gap: AmountJson;
+ totalFee: AmountJson;
+ selected: CoinInfo;
+ totalChangeValue: AmountJson;
+ refreshEffective: AmountJson;
+ coins: { info: CoinInfo; size: number }[];
+
+ // totalValue: AmountJson;
+ // totalDepositFee: AmountJson;
+ // totalRefreshFee: AmountJson;
+ // totalChangeContribution: AmountJson;
+ // totalChangeWithdrawalFee: AmountJson;
+}
+
+interface CoinsFilter {
+ shouldCalculatePurseFee?: boolean;
+ exchanges?: string[];
+ wireMethod?: string;
+ ageRestricted?: number;
+}
+
+interface AvailableCoins {
+ list: CoinInfo[];
+ exchanges: Record<string, ExchangeInfo>;
+}
+
+/**
+ * Get all the denoms that can be used for a operation that is limited
+ * by the following restrictions.
+ * This function is costly (by the database access) but with high chances
+ * of being cached
+ */
+async function getAvailableDenoms(
+ ws: InternalWalletState,
+ op: TransactionType,
+ currency: string,
+ filters: CoinsFilter = {},
+): Promise<AvailableCoins> {
+ const operationType = getOperationType(TransactionType.Deposit);
+
+ return await ws.db
+ .mktx((x) => [
+ x.exchanges,
+ x.exchangeDetails,
+ x.denominations,
+ x.coinAvailability,
+ ])
+ .runReadOnly(async (tx) => {
+ const list: CoinInfo[] = [];
+ const exchanges: Record<string, ExchangeInfo> = {};
+
+ const databaseExchanges = await tx.exchanges.iter().toArray();
+ const filteredExchanges =
+ filters.exchanges ?? databaseExchanges.map((e) => e.baseUrl);
+
+ for (const exchangeBaseUrl of filteredExchanges) {
+ const exchangeDetails = await getExchangeDetails(tx, exchangeBaseUrl);
+ // 1.- exchange has same currency
+ if (exchangeDetails?.currency !== currency) {
+ continue;
+ }
+
+ let deadline = AbsoluteTime.never();
+ // 2.- exchange supports wire method
+ let wireFee: AmountJson | undefined;
+ if (filters.wireMethod) {
+ const wireMethodWithDates =
+ exchangeDetails.wireInfo.feesForType[filters.wireMethod];
+
+ if (!wireMethodWithDates) {
+ throw Error(
+ `exchange ${exchangeBaseUrl} doesn't have wire method ${filters.wireMethod}`,
+ );
+ }
+ const wireMethodFee = wireMethodWithDates.find((x) => {
+ return AbsoluteTime.isBetween(
+ AbsoluteTime.now(),
+ AbsoluteTime.fromProtocolTimestamp(x.startStamp),
+ AbsoluteTime.fromProtocolTimestamp(x.endStamp),
+ );
+ });
+
+ if (!wireMethodFee) {
+ throw Error(
+ `exchange ${exchangeBaseUrl} doesn't have wire fee defined for this period`,
+ );
+ }
+ wireFee = Amounts.parseOrThrow(wireMethodFee.wireFee);
+ deadline = AbsoluteTime.min(
+ deadline,
+ AbsoluteTime.fromProtocolTimestamp(wireMethodFee.endStamp),
+ );
+ }
+ // exchanges[exchangeBaseUrl].wireFee = wireMethodFee;
+
+ // 3.- exchange supports wire method
+ let purseFee: AmountJson | undefined;
+ if (filters.shouldCalculatePurseFee) {
+ const purseFeeFound = exchangeDetails.globalFees.find((x) => {
+ return AbsoluteTime.isBetween(
+ AbsoluteTime.now(),
+ AbsoluteTime.fromProtocolTimestamp(x.startDate),
+ AbsoluteTime.fromProtocolTimestamp(x.endDate),
+ );
+ });
+ if (!purseFeeFound) {
+ throw Error(
+ `exchange ${exchangeBaseUrl} doesn't have purse fee defined for this period`,
+ );
+ }
+ purseFee = Amounts.parseOrThrow(purseFeeFound.purseFee);
+ deadline = AbsoluteTime.min(
+ deadline,
+ AbsoluteTime.fromProtocolTimestamp(purseFeeFound.endDate),
+ );
+ }
+
+ let creditDeadline = AbsoluteTime.never();
+ let debitDeadline = AbsoluteTime.never();
+ //4.- filter coins restricted by age
+ if (operationType === OperationType.Credit) {
+ const ds = await tx.denominations.indexes.byExchangeBaseUrl.getAll(
+ exchangeBaseUrl,
+ );
+ for (const denom of ds) {
+ const expiresWithdraw = AbsoluteTime.fromProtocolTimestamp(
+ denom.stampExpireWithdraw,
+ );
+ const expiresDeposit = AbsoluteTime.fromProtocolTimestamp(
+ denom.stampExpireDeposit,
+ );
+ creditDeadline = AbsoluteTime.min(deadline, expiresWithdraw);
+ debitDeadline = AbsoluteTime.min(deadline, expiresDeposit);
+ list.push(
+ buildCoinInfoFromDenom(
+ denom,
+ purseFee,
+ wireFee,
+ AgeRestriction.AGE_UNRESTRICTED,
+ Number.MAX_SAFE_INTEGER, // Max withdrawable from single denom
+ ),
+ );
+ }
+ } else {
+ const ageLower = filters.ageRestricted ?? 0;
+ const ageUpper = AgeRestriction.AGE_UNRESTRICTED;
+
+ const myExchangeCoins =
+ await tx.coinAvailability.indexes.byExchangeAgeAvailability.getAll(
+ GlobalIDB.KeyRange.bound(
+ [exchangeDetails.exchangeBaseUrl, ageLower, 1],
+ [
+ exchangeDetails.exchangeBaseUrl,
+ ageUpper,
+ Number.MAX_SAFE_INTEGER,
+ ],
+ ),
+ );
+ //5.- save denoms with how many coins are available
+ // FIXME: Check that the individual denomination is audited!
+ // FIXME: Should we exclude denominations that are
+ // not spendable anymore?
+ for (const coinAvail of myExchangeCoins) {
+ const denom = await tx.denominations.get([
+ coinAvail.exchangeBaseUrl,
+ coinAvail.denomPubHash,
+ ]);
+ checkDbInvariant(!!denom);
+ if (denom.isRevoked || !denom.isOffered) {
+ continue;
+ }
+ const expiresWithdraw = AbsoluteTime.fromProtocolTimestamp(
+ denom.stampExpireWithdraw,
+ );
+ const expiresDeposit = AbsoluteTime.fromProtocolTimestamp(
+ denom.stampExpireDeposit,
+ );
+ creditDeadline = AbsoluteTime.min(deadline, expiresWithdraw);
+ debitDeadline = AbsoluteTime.min(deadline, expiresDeposit);
+ list.push(
+ buildCoinInfoFromDenom(
+ denom,
+ purseFee,
+ wireFee,
+ coinAvail.maxAge,
+ coinAvail.freshCoinCount,
+ ),
+ );
+ }
+ }
+
+ exchanges[exchangeBaseUrl] = {
+ purseFee,
+ wireFee,
+ debitDeadline,
+ creditDeadline,
+ };
+ }
+
+ return { list, exchanges };
+ });
+}
+
+function buildCoinInfoFromDenom(
+ denom: DenominationRecord,
+ purseFee: AmountJson | undefined,
+ wireFee: AmountJson | undefined,
+ maxAge: number,
+ total: number,
+): CoinInfo {
+ return {
+ id: denom.denomPubHash,
+ denomWithdraw: Amounts.parseOrThrow(denom.fees.feeWithdraw),
+ denomDeposit: Amounts.parseOrThrow(denom.fees.feeDeposit),
+ denomRefresh: Amounts.parseOrThrow(denom.fees.feeRefresh),
+ exchangePurse: purseFee,
+ exchangeWire: wireFee,
+ exchangeBaseUrl: denom.exchangeBaseUrl,
+ duration: AbsoluteTime.difference(
+ AbsoluteTime.now(),
+ AbsoluteTime.fromProtocolTimestamp(denom.stampExpireDeposit),
+ ),
+ totalAvailable: total,
+ value: DenominationRecord.getValue(denom),
+ maxAge,
+ };
+}
+
+export async function convertDepositAmount(
+ ws: InternalWalletState,
+ req: ConvertAmountRequest,
+): Promise<AmountResponse> {
+ const amount = Amounts.parseOrThrow(req.amount);
+ // const filter = getCoinsFilter(req);
+
+ const denoms = await getAvailableDenoms(
+ ws,
+ TransactionType.Deposit,
+ amount.currency,
+ {},
+ );
+ const result = convertDepositAmountForAvailableCoins(
+ denoms,
+ amount,
+ req.type,
+ );
+
+ return {
+ effectiveAmount: Amounts.stringify(result.effective),
+ rawAmount: Amounts.stringify(result.raw),
+ };
+}
+
+const LOG_REFRESH = false;
+const LOG_DEPOSIT = false;
+export function convertDepositAmountForAvailableCoins(
+ denoms: AvailableCoins,
+ amount: AmountJson,
+ mode: TransactionAmountMode,
+): AmountAndRefresh {
+ const zero = Amounts.zeroOfCurrency(amount.currency);
+ if (!denoms.list.length) {
+ // no coins in the database
+ return { effective: zero, raw: zero };
+ }
+ const depositDenoms = rankDenominationForDeposit(denoms.list, mode);
+
+ //FIXME: we are not taking into account
+ // * exchanges with multiple accounts
+ // * wallet with multiple exchanges
+ const wireFee = Object.values(denoms.exchanges)[0]?.wireFee ?? zero;
+ const adjustedAmount = Amounts.add(amount, wireFee).amount;
+
+ const selected = selectGreedyCoins(depositDenoms, adjustedAmount);
+
+ const gap = Amounts.sub(amount, selected.totalValue).amount;
+
+ const result = getTotalEffectiveAndRawForDeposit(
+ selected.coins,
+ amount.currency,
+ );
+ result.raw = Amounts.sub(result.raw, wireFee).amount;
+
+ if (Amounts.isZero(gap)) {
+ // exact amount founds
+ return result;
+ }
+
+ if (LOG_DEPOSIT) {
+ const logInfo = selected.coins.map((c) => {
+ return `${Amounts.stringifyValue(c.info.id)} x ${c.size}`;
+ });
+ console.log(
+ "deposit used:",
+ logInfo.join(", "),
+ "gap:",
+ Amounts.stringifyValue(gap),
+ );
+ }
+
+ const refreshDenoms = rankDenominationForRefresh(denoms.list);
+ /**
+ * FIXME: looking for refresh AFTER selecting greedy is not optimal
+ */
+ const refreshCoin = searchBestRefreshCoin(
+ depositDenoms,
+ refreshDenoms,
+ gap,
+ mode,
+ );
+
+ if (refreshCoin) {
+ const fee = Amounts.sub(result.effective, result.raw).amount;
+ const effective = Amounts.add(
+ result.effective,
+ refreshCoin.refreshEffective,
+ ).amount;
+ const raw = Amounts.sub(effective, fee, refreshCoin.totalFee).amount;
+ //found with change
+ return {
+ effective,
+ raw,
+ refresh: refreshCoin,
+ };
+ }
+
+ // there is a gap, but no refresh coin was found
+ return result;
+}
+
+export async function getMaxDepositAmount(
+ ws: InternalWalletState,
+ req: GetAmountRequest,
+): Promise<AmountResponse> {
+ // const filter = getCoinsFilter(req);
+
+ const denoms = await getAvailableDenoms(
+ ws,
+ TransactionType.Deposit,
+ req.currency,
+ {},
+ );
+
+ const result = getMaxDepositAmountForAvailableCoins(denoms, req.currency);
+ return {
+ effectiveAmount: Amounts.stringify(result.effective),
+ rawAmount: Amounts.stringify(result.raw),
+ };
+}
+
+export function getMaxDepositAmountForAvailableCoins(
+ denoms: AvailableCoins,
+ currency: string,
+) {
+ const zero = Amounts.zeroOfCurrency(currency);
+ if (!denoms.list.length) {
+ // no coins in the database
+ return { effective: zero, raw: zero };
+ }
+
+ const result = getTotalEffectiveAndRawForDeposit(
+ denoms.list.map((info) => {
+ return { info, size: info.totalAvailable ?? 0 };
+ }),
+ currency,
+ );
+
+ const wireFee = Object.values(denoms.exchanges)[0]?.wireFee ?? zero;
+ result.raw = Amounts.sub(result.raw, wireFee).amount;
+
+ return result;
+}
+
+export async function convertPeerPushAmount(
+ ws: InternalWalletState,
+ req: ConvertAmountRequest,
+): Promise<AmountResponse> {
+ throw Error("to be implemented after 1.0");
+}
+export async function getMaxPeerPushAmount(
+ ws: InternalWalletState,
+ req: GetAmountRequest,
+): Promise<AmountResponse> {
+ throw Error("to be implemented after 1.0");
+}
+export async function convertWithdrawalAmount(
+ ws: InternalWalletState,
+ req: ConvertAmountRequest,
+): Promise<AmountResponse> {
+ const amount = Amounts.parseOrThrow(req.amount);
+
+ const denoms = await getAvailableDenoms(
+ ws,
+ TransactionType.Withdrawal,
+ amount.currency,
+ {},
+ );
+
+ const result = convertWithdrawalAmountFromAvailableCoins(
+ denoms,
+ amount,
+ req.type,
+ );
+
+ return {
+ effectiveAmount: Amounts.stringify(result.effective),
+ rawAmount: Amounts.stringify(result.raw),
+ };
+}
+
+export function convertWithdrawalAmountFromAvailableCoins(
+ denoms: AvailableCoins,
+ amount: AmountJson,
+ mode: TransactionAmountMode,
+) {
+ const zero = Amounts.zeroOfCurrency(amount.currency);
+ if (!denoms.list.length) {
+ // no coins in the database
+ return { effective: zero, raw: zero };
+ }
+ const withdrawDenoms = rankDenominationForWithdrawals(denoms.list, mode);
+
+ const selected = selectGreedyCoins(withdrawDenoms, amount);
+
+ return getTotalEffectiveAndRawForWithdrawal(selected.coins, amount.currency);
+}
+
+/** *****************************************************
+ * HELPERS
+ * *****************************************************
+ */
+
+/**
+ *
+ * @param depositDenoms
+ * @param refreshDenoms
+ * @param amount
+ * @param mode
+ * @returns
+ */
+function searchBestRefreshCoin(
+ depositDenoms: SelectableElement[],
+ refreshDenoms: Record<string, SelectableElement[]>,
+ amount: AmountJson,
+ mode: TransactionAmountMode,
+): RefreshChoice | undefined {
+ let choice: RefreshChoice | undefined = undefined;
+ let refreshIdx = 0;
+ refreshIteration: while (refreshIdx < depositDenoms.length) {
+ const d = depositDenoms[refreshIdx];
+
+ const denomContribution =
+ mode === TransactionAmountMode.Effective
+ ? d.value
+ : Amounts.sub(d.value, d.info.denomRefresh, d.info.denomDeposit).amount;
+
+ const changeAfterDeposit = Amounts.sub(denomContribution, amount).amount;
+ if (Amounts.isZero(changeAfterDeposit)) {
+ //this coin is not big enough to use for refresh
+ //since the list is sorted, we can break here
+ break refreshIteration;
+ }
+
+ const withdrawDenoms = refreshDenoms[d.info.exchangeBaseUrl];
+ const change = selectGreedyCoins(withdrawDenoms, changeAfterDeposit);
+
+ const zero = Amounts.zeroOfCurrency(amount.currency);
+ const withdrawChangeFee = change.coins.reduce((cur, prev) => {
+ return Amounts.add(
+ cur,
+ Amounts.mult(prev.info.denomWithdraw, prev.size).amount,
+ ).amount;
+ }, zero);
+
+ const withdrawChangeValue = change.coins.reduce((cur, prev) => {
+ return Amounts.add(cur, Amounts.mult(prev.info.value, prev.size).amount)
+ .amount;
+ }, zero);
+
+ const totalFee = Amounts.add(
+ d.info.denomDeposit,
+ d.info.denomRefresh,
+ withdrawChangeFee,
+ ).amount;
+
+ if (!choice || Amounts.cmp(totalFee, choice.totalFee) === -1) {
+ //found cheaper change
+ choice = {
+ gap: amount,
+ totalFee: totalFee,
+ totalChangeValue: change.totalValue, //change after refresh
+ refreshEffective: Amounts.sub(d.info.value, withdrawChangeValue).amount, // what of the denom used is not recovered
+ selected: d.info,
+ coins: change.coins,
+ };
+ }
+ refreshIdx++;
+ }
+ if (choice) {
+ if (LOG_REFRESH) {
+ const logInfo = choice.coins.map((c) => {
+ return `${Amounts.stringifyValue(c.info.id)} x ${c.size}`;
+ });
+ console.log(
+ "refresh used:",
+ Amounts.stringifyValue(choice.selected.value),
+ "change:",
+ logInfo.join(", "),
+ "fee:",
+ Amounts.stringifyValue(choice.totalFee),
+ "refreshEffective:",
+ Amounts.stringifyValue(choice.refreshEffective),
+ "totalChangeValue:",
+ Amounts.stringifyValue(choice.totalChangeValue),
+ );
+ }
+ }
+ return choice;
+}
+
+/**
+ * Returns a copy of the list sorted for the best denom to withdraw first
+ *
+ * @param denoms
+ * @returns
+ */
+function rankDenominationForWithdrawals(
+ denoms: CoinInfo[],
+ mode: TransactionAmountMode,
+): SelectableElement[] {
+ const copyList = [...denoms];
+ /**
+ * Rank coins
+ */
+ copyList.sort((d1, d2) => {
+ // the best coin to use is
+ // 1.- the one that contrib more and pay less fee
+ // 2.- it takes more time before expires
+
+ //different exchanges may have different wireFee
+ //ranking should take the relative contribution in the exchange
+ //which is (value - denomFee / fixedFee)
+ const rate1 = Amounts.divmod(d1.value, d1.denomWithdraw).quotient;
+ const rate2 = Amounts.divmod(d2.value, d2.denomWithdraw).quotient;
+ const contribCmp = rate1 === rate2 ? 0 : rate1 < rate2 ? 1 : -1;
+ return (
+ contribCmp ||
+ Duration.cmp(d1.duration, d2.duration) ||
+ strcmp(d1.id, d2.id)
+ );
+ });
+
+ return copyList.map((info) => {
+ switch (mode) {
+ case TransactionAmountMode.Effective: {
+ //if the user instructed "effective" then we need to selected
+ //greedy total coin value
+ return {
+ info,
+ value: info.value,
+ total: Number.MAX_SAFE_INTEGER,
+ };
+ }
+ case TransactionAmountMode.Raw: {
+ //if the user instructed "raw" then we need to selected
+ //greedy total coin raw amount (without fee)
+ return {
+ info,
+ value: Amounts.add(info.value, info.denomWithdraw).amount,
+ total: Number.MAX_SAFE_INTEGER,
+ };
+ }
+ }
+ });
+}
+
+/**
+ * Returns a copy of the list sorted for the best denom to deposit first
+ *
+ * @param denoms
+ * @returns
+ */
+function rankDenominationForDeposit(
+ denoms: CoinInfo[],
+ mode: TransactionAmountMode,
+): SelectableElement[] {
+ const copyList = [...denoms];
+ /**
+ * Rank coins
+ */
+ copyList.sort((d1, d2) => {
+ // the best coin to use is
+ // 1.- the one that contrib more and pay less fee
+ // 2.- it takes more time before expires
+
+ //different exchanges may have different wireFee
+ //ranking should take the relative contribution in the exchange
+ //which is (value - denomFee / fixedFee)
+ const rate1 = Amounts.divmod(d1.value, d1.denomDeposit).quotient;
+ const rate2 = Amounts.divmod(d2.value, d2.denomDeposit).quotient;
+ const contribCmp = rate1 === rate2 ? 0 : rate1 < rate2 ? 1 : -1;
+ return (
+ contribCmp ||
+ Duration.cmp(d1.duration, d2.duration) ||
+ strcmp(d1.id, d2.id)
+ );
+ });
+
+ return copyList.map((info) => {
+ switch (mode) {
+ case TransactionAmountMode.Effective: {
+ //if the user instructed "effective" then we need to selected
+ //greedy total coin value
+ return {
+ info,
+ value: info.value,
+ total: info.totalAvailable ?? 0,
+ };
+ }
+ case TransactionAmountMode.Raw: {
+ //if the user instructed "raw" then we need to selected
+ //greedy total coin raw amount (without fee)
+ return {
+ info,
+ value: Amounts.sub(info.value, info.denomDeposit).amount,
+ total: info.totalAvailable ?? 0,
+ };
+ }
+ }
+ });
+}
+
+/**
+ * Returns a copy of the list sorted for the best denom to withdraw first
+ *
+ * @param denoms
+ * @returns
+ */
+function rankDenominationForRefresh(
+ denoms: CoinInfo[],
+): Record<string, SelectableElement[]> {
+ const groupByExchange: Record<string, CoinInfo[]> = {};
+ for (const d of denoms) {
+ if (!groupByExchange[d.exchangeBaseUrl]) {
+ groupByExchange[d.exchangeBaseUrl] = [];
+ }
+ groupByExchange[d.exchangeBaseUrl].push(d);
+ }
+
+ const result: Record<string, SelectableElement[]> = {};
+ for (const d of denoms) {
+ result[d.exchangeBaseUrl] = rankDenominationForWithdrawals(
+ groupByExchange[d.exchangeBaseUrl],
+ TransactionAmountMode.Raw,
+ );
+ }
+ return result;
+}
+
+interface SelectableElement {
+ total: number;
+ value: AmountJson;
+ info: CoinInfo;
+}
+
+function selectGreedyCoins(
+ coins: SelectableElement[],
+ limit: AmountJson,
+): SelectedCoins {
+ const result: SelectedCoins = {
+ totalValue: Amounts.zeroOfCurrency(limit.currency),
+ coins: [],
+ };
+ if (!coins.length) return result;
+
+ let denomIdx = 0;
+ iterateDenoms: while (denomIdx < coins.length) {
+ const denom = coins[denomIdx];
+ // let total = denom.total;
+ const left = Amounts.sub(limit, result.totalValue).amount;
+
+ if (Amounts.isZero(denom.value)) {
+ // 0 contribution denoms should be the last
+ break iterateDenoms;
+ }
+
+ //use Amounts.divmod instead of iterate
+ const div = Amounts.divmod(left, denom.value);
+ const size = Math.min(div.quotient, denom.total);
+ if (size > 0) {
+ const mul = Amounts.mult(denom.value, size).amount;
+ const progress = Amounts.add(result.totalValue, mul).amount;
+
+ result.totalValue = progress;
+ result.coins.push({ info: denom.info, size });
+ denom.total = denom.total - size;
+ }
+
+ //go next denom
+ denomIdx++;
+ }
+
+ return result;
+}
+
+type AmountWithFee = { raw: AmountJson; effective: AmountJson };
+type AmountAndRefresh = AmountWithFee & { refresh?: RefreshChoice };
+
+export function getTotalEffectiveAndRawForDeposit(
+ list: { info: CoinInfo; size: number }[],
+ currency: string,
+): AmountWithFee {
+ const init = {
+ raw: Amounts.zeroOfCurrency(currency),
+ effective: Amounts.zeroOfCurrency(currency),
+ };
+ return list.reduce((prev, cur) => {
+ const ef = Amounts.mult(cur.info.value, cur.size).amount;
+ const rw = Amounts.mult(
+ Amounts.sub(cur.info.value, cur.info.denomDeposit).amount,
+ cur.size,
+ ).amount;
+
+ prev.effective = Amounts.add(prev.effective, ef).amount;
+ prev.raw = Amounts.add(prev.raw, rw).amount;
+ return prev;
+ }, init);
+}
+
+function getTotalEffectiveAndRawForWithdrawal(
+ list: { info: CoinInfo; size: number }[],
+ currency: string,
+): AmountWithFee {
+ const init = {
+ raw: Amounts.zeroOfCurrency(currency),
+ effective: Amounts.zeroOfCurrency(currency),
+ };
+ return list.reduce((prev, cur) => {
+ const ef = Amounts.mult(cur.info.value, cur.size).amount;
+ const rw = Amounts.mult(
+ Amounts.add(cur.info.value, cur.info.denomWithdraw).amount,
+ cur.size,
+ ).amount;
+
+ prev.effective = Amounts.add(prev.effective, ef).amount;
+ prev.raw = Amounts.add(prev.raw, rw).amount;
+ return prev;
+ }, init);
+}
diff --git a/packages/taler-wallet-core/src/versions.ts b/packages/taler-wallet-core/src/versions.ts
index f0f747e22..8b9177bc3 100644
--- a/packages/taler-wallet-core/src/versions.ts
+++ b/packages/taler-wallet-core/src/versions.ts
@@ -19,7 +19,7 @@
*
* Uses libtool's current:revision:age versioning.
*/
-export const WALLET_EXCHANGE_PROTOCOL_VERSION = "15:0:2";
+export const WALLET_EXCHANGE_PROTOCOL_VERSION = "17:0:0";
/**
* Protocol version spoken with the merchant.
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index b967571d0..bff4442b6 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -277,13 +277,6 @@ import {
import { PendingTaskInfo, PendingTaskType } from "./pending-types.js";
import { assertUnreachable } from "./util/assertUnreachable.js";
import {
- convertDepositAmount,
- convertPeerPushAmount,
- convertWithdrawalAmount,
- getMaxDepositAmount,
- getMaxPeerPushAmount,
-} from "./util/coinSelection.js";
-import {
createTimeline,
selectBestForOverlappingDenominations,
selectMinimumFee,
@@ -313,6 +306,13 @@ import {
WalletCoreApiClient,
WalletCoreResponseType,
} from "./wallet-api-types.js";
+import {
+ convertDepositAmount,
+ getMaxDepositAmount,
+ convertPeerPushAmount,
+ getMaxPeerPushAmount,
+ convertWithdrawalAmount,
+} from "./util/instructedAmountConversion.js";
const logger = new Logger("wallet.ts");
diff --git a/packages/web-util/package.json b/packages/web-util/package.json
index 9661b3f87..6cfe19258 100644
--- a/packages/web-util/package.json
+++ b/packages/web-util/package.json
@@ -44,7 +44,6 @@
"@types/web": "^0.0.82",
"@types/ws": "^8.5.3",
"autoprefixer": "^10.4.14",
- "axios": "^1.2.2",
"chokidar": "^3.5.3",
"esbuild": "^0.17.7",
"express": "^4.18.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7fd6f7619..5b733353b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -579,9 +579,6 @@ importers:
'@gnu-taler/taler-wallet-core':
specifier: workspace:*
version: link:../taler-wallet-core
- axios:
- specifier: ^0.27.2
- version: 0.27.2
tslib:
specifier: ^2.5.3
version: 2.5.3
@@ -678,9 +675,6 @@ importers:
'@types/node':
specifier: ^18.11.17
version: 18.11.17
- axios:
- specifier: ^0.27.2
- version: 0.27.2
big-integer:
specifier: ^1.6.51
version: 1.6.51
@@ -925,9 +919,6 @@ importers:
autoprefixer:
specifier: ^10.4.14
version: 10.4.14(postcss@8.4.23)
- axios:
- specifier: ^1.2.2
- version: 1.2.2
chokidar:
specifier: ^3.5.3
version: 3.5.3
@@ -6804,6 +6795,7 @@ packages:
/asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+ dev: true
/at-least-node@1.0.0:
resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
@@ -6969,15 +6961,6 @@ packages:
- debug
dev: true
- /axios@0.27.2:
- resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==}
- dependencies:
- follow-redirects: 1.15.2
- form-data: 4.0.0
- transitivePeerDependencies:
- - debug
- dev: false
-
/axios@1.1.3:
resolution: {integrity: sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==}
dependencies:
@@ -6988,16 +6971,6 @@ packages:
- debug
dev: true
- /axios@1.2.2:
- resolution: {integrity: sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q==}
- dependencies:
- follow-redirects: 1.15.2
- form-data: 4.0.0
- proxy-from-env: 1.1.0
- transitivePeerDependencies:
- - debug
- dev: true
-
/axobject-query@2.2.0:
resolution: {integrity: sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==}
dev: true
@@ -8069,6 +8042,7 @@ packages:
engines: {node: '>= 0.8'}
dependencies:
delayed-stream: 1.0.0
+ dev: true
/commander@2.17.1:
resolution: {integrity: sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==}
@@ -8918,6 +8892,7 @@ packages:
/delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
+ dev: true
/depd@1.1.2:
resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==}
@@ -10528,6 +10503,7 @@ packages:
peerDependenciesMeta:
debug:
optional: true
+ dev: true
/for-each@0.3.3:
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
@@ -10604,6 +10580,7 @@ packages:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.35
+ dev: true
/forwarded@0.2.0:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
@@ -12669,12 +12646,14 @@ packages:
/mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
+ dev: true
/mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
dependencies:
mime-db: 1.52.0
+ dev: true
/mime@1.6.0:
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}