gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] branch master updated: harness,wallet-cli: notificat


From: gnunet
Subject: [taler-wallet-core] branch master updated: harness,wallet-cli: notification-based testing with RPC wallet
Date: Thu, 02 Feb 2023 20:21:07 +0100

This is an automated email from the git hooks/post-receive script.

dold pushed a commit to branch master
in repository wallet-core.

The following commit(s) were added to refs/heads/master by this push:
     new 96101238a harness,wallet-cli: notification-based testing with RPC 
wallet
96101238a is described below

commit 96101238afb82d200cf9d5005ffc2fc0391f23e4
Author: Florian Dold <florian@dold.me>
AuthorDate: Thu Feb 2 20:20:58 2023 +0100

    harness,wallet-cli: notification-based testing with RPC wallet
---
 packages/taler-harness/src/harness/harness.ts      | 117 ++++++++++++-
 packages/taler-harness/src/harness/helpers.ts      | 108 ++++++++++++
 .../integrationtests/test-wallet-notifications.ts  | 163 ++++++++++++++++++
 .../src/integrationtests/testrunner.ts             |   6 +-
 packages/taler-util/src/twrpc-impl.node.ts         |  12 +-
 packages/taler-util/src/twrpc.ts                   |   2 +
 packages/taler-wallet-cli/src/index.ts             | 163 +++---------------
 packages/taler-wallet-core/package.json            |   3 +
 packages/taler-wallet-core/src/remote.ts           | 187 +++++++++++++++++++++
 9 files changed, 609 insertions(+), 152 deletions(-)

diff --git a/packages/taler-harness/src/harness/harness.ts 
b/packages/taler-harness/src/harness/harness.ts
index 134709541..83c8f60d1 100644
--- a/packages/taler-harness/src/harness/harness.ts
+++ b/packages/taler-harness/src/harness/harness.ts
@@ -21,8 +21,6 @@
  * @author Florian Dold <dold@taler.net>
  */
 
-const logger = new Logger("harness.ts");
-
 /**
  * Imports
  */
@@ -43,6 +41,7 @@ import {
   parsePaytoUri,
   stringToBytes,
   TalerProtocolDuration,
+  WalletNotification,
 } from "@gnu-taler/taler-util";
 import {
   BankAccessApi,
@@ -57,9 +56,9 @@ import {
 import { deepStrictEqual } from "assert";
 import axiosImp, { AxiosError } from "axios";
 import { ChildProcess, spawn } from "child_process";
-import * as child_process from "child_process";
 import * as fs from "fs";
 import * as http from "http";
+import * as net from "node:net";
 import * as path from "path";
 import * as readline from "readline";
 import { URL } from "url";
@@ -76,6 +75,15 @@ import {
   TipCreateRequest,
   TippingReserveStatus,
 } from "./merchantApiTypes.js";
+import {
+  createRemoteWallet,
+  getClientFromRemoteWallet,
+  makeNotificationWaiter,
+  RemoteWallet,
+  WalletNotificationWaiter,
+} from "@gnu-taler/taler-wallet-core/remote";
+
+const logger = new Logger("harness.ts");
 
 const axios = axiosImp.default;
 
@@ -1831,7 +1839,7 @@ export async function runTestWithState(
 
   const handleSignal = (s: string) => {
     logger.warn(
-      `**** received fatal process event, terminating test ${testName}`,
+      `**** received fatal process event (${s}), terminating test ${testName}`,
     );
     gc.shutdownSync();
     process.exit(1);
@@ -1885,6 +1893,107 @@ export interface WalletCliOpts {
   cryptoWorkerType?: "sync" | "node-worker-thread";
 }
 
+function tryUnixConnect(socketPath: string): Promise<void> {
+  return new Promise((resolve, reject) => {
+    const client = net.createConnection(socketPath);
+    client.on("error", (e) => {
+      reject(e);
+    });
+    client.on("connect", () => {
+      client.end();
+      resolve();
+    });
+  });
+}
+
+export class WalletService {
+  walletProc: ProcessWrapper | undefined;
+
+  constructor(private globalState: GlobalTestState, private name: string) {}
+
+  get socketPath() {
+    const unixPath = path.join(this.globalState.testDir, `${this.name}.sock`);
+    return unixPath;
+  }
+
+  async start(): Promise<void> {
+    const dbPath = path.join(
+      this.globalState.testDir,
+      `walletdb-${this.name}.json`,
+    );
+    const unixPath = this.socketPath;
+    this.globalState.spawnService(
+      "taler-wallet-cli",
+      [
+        "--wallet-db",
+        dbPath,
+        "advanced",
+        "serve",
+        "--unix-path",
+        unixPath,
+      ],
+      `wallet-${this.name}`,
+    );
+  }
+
+  async pingUntilAvailable(): Promise<void> {
+    while (1) {
+      try {
+        await tryUnixConnect(this.socketPath);
+      } catch (e) {
+        logger.info(`connection attempt failed: ${e}`);
+        await delayMs(200);
+        continue;
+      }
+      logger.info("connection to wallet-core succeeded");
+      break;
+    }
+  }
+}
+
+export interface WalletClientArgs {
+  unixPath: string;
+  onNotification?(n: WalletNotification): void;
+}
+
+export class WalletClient {
+  remoteWallet: RemoteWallet | undefined = undefined;
+  waiter: WalletNotificationWaiter = makeNotificationWaiter();
+
+  constructor(private args: WalletClientArgs) {}
+
+  async connect(): Promise<void> {
+    const waiter = this.waiter;
+    const walletClient = this;
+    const w = await createRemoteWallet({
+      socketFilename: this.args.unixPath,
+      notificationHandler(n) {
+        if (walletClient.args.onNotification) {
+          walletClient.args.onNotification(n);
+        }
+        waiter.notify(n);
+        console.log("got notification from wallet-core in WalletClient");
+      },
+    });
+    this.remoteWallet = w;
+
+    this.waiter.waitForNotificationCond;
+  }
+
+  get client() {
+    if (!this.remoteWallet) {
+      throw Error("wallet not connected");
+    }
+    return getClientFromRemoteWallet(this.remoteWallet);
+  }
+
+  waitForNotificationCond(
+    cond: (n: WalletNotification) => boolean,
+  ): Promise<void> {
+    return this.waiter.waitForNotificationCond(cond);
+  }
+}
+
 export class WalletCli {
   private currentTimetravel: Duration | undefined;
   private _client: WalletCoreApiClient;
diff --git a/packages/taler-harness/src/harness/helpers.ts 
b/packages/taler-harness/src/harness/helpers.ts
index 96b34f9d9..59a37e4b8 100644
--- a/packages/taler-harness/src/harness/helpers.ts
+++ b/packages/taler-harness/src/harness/helpers.ts
@@ -180,6 +180,114 @@ export async function createSimpleTestkudosEnvironment(
   };
 }
 
+/**
+ * Run a test case with a simple TESTKUDOS Taler environment, consisting
+ * of one exchange, one bank and one merchant.
+ *
+ * V2 uses a daemonized wallet instead of the CLI wallet.
+ */
+export async function createSimpleTestkudosEnvironmentV2(
+  t: GlobalTestState,
+  coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")),
+  opts: EnvOptions = {},
+): Promise<SimpleTestEnvironment> {
+  const db = await setupDb(t);
+
+  const bank = await BankService.create(t, {
+    allowRegistrations: true,
+    currency: "TESTKUDOS",
+    database: db.connStr,
+    httpPort: 8082,
+  });
+
+  const exchange = ExchangeService.create(t, {
+    name: "testexchange-1",
+    currency: "TESTKUDOS",
+    httpPort: 8081,
+    database: db.connStr,
+  });
+
+  const merchant = await MerchantService.create(t, {
+    name: "testmerchant-1",
+    currency: "TESTKUDOS",
+    httpPort: 8083,
+    database: db.connStr,
+  });
+
+  const exchangeBankAccount = await bank.createExchangeAccount(
+    "myexchange",
+    "x",
+  );
+  await exchange.addBankAccount("1", exchangeBankAccount);
+
+  bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri);
+
+  await bank.start();
+
+  await bank.pingUntilAvailable();
+
+  const ageMaskSpec = opts.ageMaskSpec;
+
+  if (ageMaskSpec) {
+    exchange.enableAgeRestrictions(ageMaskSpec);
+    // Enable age restriction for all coins.
+    exchange.addCoinConfigList(
+      coinConfig.map((x) => ({
+        ...x,
+        name: `${x.name}-age`,
+        ageRestricted: true,
+      })),
+    );
+    // For mixed age restrictions, we also offer coins without age restrictions
+    if (opts.mixedAgeRestriction) {
+      exchange.addCoinConfigList(
+        coinConfig.map((x) => ({ ...x, ageRestricted: false })),
+      );
+    }
+  } else {
+    exchange.addCoinConfigList(coinConfig);
+  }
+
+  await exchange.start();
+  await exchange.pingUntilAvailable();
+
+  merchant.addExchange(exchange);
+
+  await merchant.start();
+  await merchant.pingUntilAvailable();
+
+  await merchant.addInstance({
+    id: "default",
+    name: "Default Instance",
+    paytoUris: [getPayto("merchant-default")],
+    defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+      Duration.fromSpec({ minutes: 1 }),
+    ),
+  });
+
+  await merchant.addInstance({
+    id: "minst1",
+    name: "minst1",
+    paytoUris: [getPayto("minst1")],
+    defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+      Duration.fromSpec({ minutes: 1 }),
+    ),
+  });
+
+  console.log("setup done!");
+
+  const wallet = new WalletCli(t);
+
+  return {
+    commonDb: db,
+    exchange,
+    merchant,
+    wallet,
+    bank,
+    exchangeBankAccount,
+  };
+}
+
 export interface FaultyMerchantTestEnvironment {
   commonDb: DbInfo;
   bank: BankService;
diff --git 
a/packages/taler-harness/src/integrationtests/test-wallet-notifications.ts 
b/packages/taler-harness/src/integrationtests/test-wallet-notifications.ts
new file mode 100644
index 000000000..23c71ea2f
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-notifications.ts
@@ -0,0 +1,163 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 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 {
+  Amounts,
+  Duration,
+  NotificationType,
+  PreparePayResultType,
+} from "@gnu-taler/taler-util";
+import {
+  BankAccessApi,
+  BankApi,
+  WalletApiOperation,
+} from "@gnu-taler/taler-wallet-core";
+import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+  ExchangeService,
+  FakebankService,
+  getRandomIban,
+  GlobalTestState,
+  MerchantService,
+  setupDb,
+  WalletClient,
+  WalletService,
+} from "../harness/harness.js";
+
+/**
+ * Test for wallet-core notifications.
+ */
+export async function runWalletNotificationsTest(t: GlobalTestState) {
+  // Set up test environment
+
+  const db = await setupDb(t);
+
+  const bank = await FakebankService.create(t, {
+    allowRegistrations: true,
+    currency: "TESTKUDOS",
+    database: db.connStr,
+    httpPort: 8082,
+  });
+
+  const exchange = ExchangeService.create(t, {
+    name: "testexchange-1",
+    currency: "TESTKUDOS",
+    httpPort: 8081,
+    database: db.connStr,
+  });
+
+  const merchant = await MerchantService.create(t, {
+    name: "testmerchant-1",
+    currency: "TESTKUDOS",
+    httpPort: 8083,
+    database: db.connStr,
+  });
+
+  const exchangeBankAccount = await bank.createExchangeAccount(
+    "myexchange",
+    "x",
+  );
+  exchange.addBankAccount("1", exchangeBankAccount);
+
+  bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri);
+
+  await bank.start();
+
+  await bank.pingUntilAvailable();
+
+  const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => 
x("TESTKUDOS"));
+  exchange.addCoinConfigList(coinConfig);
+
+  await exchange.start();
+  await exchange.pingUntilAvailable();
+
+  merchant.addExchange(exchange);
+
+  await merchant.start();
+  await merchant.pingUntilAvailable();
+
+  // Fakebank uses x-taler-bank, but merchant is configured to only accept 
sepa!
+  const label = "mymerchant";
+  await merchant.addInstance({
+    id: "default",
+    name: "Default Instance",
+    paytoUris: [
+      `payto://iban/SANDBOXX/${getRandomIban(label)}?receiver-name=${label}`,
+    ],
+    defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+      Duration.fromSpec({ minutes: 1 }),
+    ),
+  });
+
+  console.log("setup done!");
+
+  const walletService = new WalletService(t, "wallet");
+  await walletService.start();
+  await walletService.pingUntilAvailable();
+
+  const walletClient = new WalletClient({
+    unixPath: walletService.socketPath,
+    onNotification(n) {
+      console.log("got notification", n);
+    },
+  });
+  await walletClient.connect();
+  await walletClient.client.call(WalletApiOperation.InitWallet, {
+    skipDefaults: true,
+  });
+
+  const user = await BankApi.createRandomBankUser(bank);
+  const wop = await BankAccessApi.createWithdrawalOperation(
+    bank,
+    user,
+    "TESTKUDOS:20",
+  );
+
+  // Hand it to the wallet
+
+  await walletClient.client.call(
+    WalletApiOperation.GetWithdrawalDetailsForUri,
+    {
+      talerWithdrawUri: wop.taler_withdraw_uri,
+    },
+  );
+
+  // Withdraw (AKA select)
+
+  const withdrawalFinishedReceivedPromise =
+    walletClient.waitForNotificationCond((x) => {
+      return x.type === NotificationType.WithdrawGroupFinished;
+    });
+
+  await walletClient.client.call(
+    WalletApiOperation.AcceptBankIntegratedWithdrawal,
+    {
+      exchangeBaseUrl: exchange.baseUrl,
+      talerWithdrawUri: wop.taler_withdraw_uri,
+    },
+  );
+
+  // Confirm it
+
+  await BankApi.confirmWithdrawalOperation(bank, user, wop);
+
+  await withdrawalFinishedReceivedPromise;
+}
+
+runWalletNotificationsTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts 
b/packages/taler-harness/src/integrationtests/testrunner.ts
index f04bc2950..3d70e6860 100644
--- a/packages/taler-harness/src/integrationtests/testrunner.ts
+++ b/packages/taler-harness/src/integrationtests/testrunner.ts
@@ -92,13 +92,14 @@ import { runWithdrawalBankIntegratedTest } from 
"./test-withdrawal-bank-integrat
 import { runWithdrawalFakebankTest } from "./test-withdrawal-fakebank.js";
 import { runTestWithdrawalManualTest } from "./test-withdrawal-manual.js";
 import { runAgeRestrictionsPeerTest } from "./test-age-restrictions-peer.js";
-import { runWalletBalanceTest } from "./test-wallet-balance.js";
+import { runWalletNotificationsTest } from "./test-wallet-notifications.js";
 import { runAgeRestrictionsMixedMerchantTest } from 
"./test-age-restrictions-mixed-merchant.js";
 import { runWalletCryptoWorkerTest } from "./test-wallet-cryptoworker.js";
 import { runWithdrawalHighTest } from "./test-withdrawal-high.js";
 import { runKycTest } from "./test-kyc.js";
 import { runPaymentAbortTest } from "./test-payment-abort.js";
 import { runWithdrawalFeesTest } from "./test-withdrawal-fees.js";
+import { runWalletBalanceTest } from "./test-wallet-balance.js";
 
 /**
  * Test runner.
@@ -166,6 +167,7 @@ const allTests: TestMainFunction[] = [
   runPaymentTransientTest,
   runPaymentZeroTest,
   runPayPaidTest,
+  runWalletBalanceTest,
   runPaywallFlowTest,
   runPeerToPeerPullTest,
   runPeerToPeerPushTest,
@@ -180,7 +182,7 @@ const allTests: TestMainFunction[] = [
   runTippingTest,
   runWalletBackupBasicTest,
   runWalletBackupDoublespendTest,
-  runWalletBalanceTest,
+  runWalletNotificationsTest,
   runWalletCryptoWorkerTest,
   runWalletDblessTest,
   runWallettestingTest,
diff --git a/packages/taler-util/src/twrpc-impl.node.ts 
b/packages/taler-util/src/twrpc-impl.node.ts
index 52ab65b73..b6333da51 100644
--- a/packages/taler-util/src/twrpc-impl.node.ts
+++ b/packages/taler-util/src/twrpc-impl.node.ts
@@ -54,6 +54,9 @@ export async function connectRpc<T>(args: RpcConnectArgs<T>): 
Promise<T> {
   let sockFilename = args.socketFilename;
   return new Promise((resolve, reject) => {
     const client = net.createConnection(sockFilename);
+    client.on("error", (e) => {
+      reject(e);
+    });
     client.on("connect", () => {
       let parsingBody: string | undefined = undefined;
       let bodyChunks: string[] = [];
@@ -102,7 +105,8 @@ export async function connectRpc<T>(args: 
RpcConnectArgs<T>): Promise<T> {
               try {
                 reqJson = JSON.parse(req);
               } catch (e) {
-                logger.warn("JSON request was invalid");
+                logger.warn("JSON message from server was invalid");
+                logger.info(`message was: ${req}`);
               }
               if (reqJson !== undefined) {
                 logger.info(`request: ${req}`);
@@ -112,6 +116,7 @@ export async function connectRpc<T>(args: 
RpcConnectArgs<T>): Promise<T> {
                 client.end();
               }
               bodyChunks = [];
+              parsingBody = undefined;
             } else {
               bodyChunks.push(lineStr);
             }
@@ -187,7 +192,7 @@ export async function runRpcServer(args: RpcServerArgs): 
Promise<void> {
             try {
               reqJson = JSON.parse(req);
             } catch (e) {
-              logger.warn("JSON request was invalid");
+              logger.warn("JSON request from client was invalid");
             }
             if (reqJson !== undefined) {
               logger.info(`request: ${req}`);
@@ -197,6 +202,7 @@ export async function runRpcServer(args: RpcServerArgs): 
Promise<void> {
               sock.end();
             }
             bodyChunks = [];
+            parsingBody = undefined;
           } else {
             bodyChunks.push(lineStr);
           }
@@ -217,6 +223,6 @@ export async function runRpcServer(args: RpcServerArgs): 
Promise<void> {
         handlers.onDisconnect();
       });
     });
-    server.listen("wallet-core.sock");
+    server.listen(args.socketFilename);
   });
 }
diff --git a/packages/taler-util/src/twrpc.ts b/packages/taler-util/src/twrpc.ts
index 368e04e27..d221630d0 100644
--- a/packages/taler-util/src/twrpc.ts
+++ b/packages/taler-util/src/twrpc.ts
@@ -14,6 +14,8 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
+import { CoreApiResponse } from "./wallet-types.js";
+
 /**
  * Implementation for the wallet-core IPC protocol.
  *
diff --git a/packages/taler-wallet-cli/src/index.ts 
b/packages/taler-wallet-cli/src/index.ts
index 67d0e3784..cce982dfb 100644
--- a/packages/taler-wallet-cli/src/index.ts
+++ b/packages/taler-wallet-cli/src/index.ts
@@ -60,13 +60,15 @@ import {
   WalletCoreApiClient,
   walletCoreDebugFlags,
 } from "@gnu-taler/taler-wallet-core";
+
+import {
+  createRemoteWallet,
+  getClientFromRemoteWallet,
+  makeNotificationWaiter,
+} from "@gnu-taler/taler-wallet-core/remote";
 import fs from "fs";
 import os from "os";
-import {
-  connectRpc,
-  JsonMessage,
-  runRpcServer,
-} from "@gnu-taler/taler-util/twrpc";
+import { JsonMessage, runRpcServer } from "@gnu-taler/taler-util/twrpc";
 
 // This module also serves as the entry point for the crypto
 // thread worker, and thus must expose these two handlers.
@@ -280,162 +282,33 @@ async function createLocalWallet(
   }
 }
 
-export interface RemoteWallet {
-  /**
-   * Low-level interface for making API requests to wallet-core.
-   */
-  makeCoreApiRequest(
-    operation: string,
-    payload: unknown,
-  ): Promise<CoreApiResponse>;
-
-  /**
-   * Close the connection to the remote wallet.
-   */
-  close(): void;
-}
-
-async function createRemoteWallet(
-  notificationHandler?: (n: WalletNotification) => void,
-): Promise<RemoteWallet> {
-  let nextRequestId = 1;
-  let requestMap: Map<
-    string,
-    {
-      promiseCapability: OpenedPromise<CoreApiResponse>;
-    }
-  > = new Map();
-
-  const ctx = await connectRpc<RemoteWallet>({
-    socketFilename: "wallet-core.sock",
-    onEstablished(connection) {
-      const ctx: RemoteWallet = {
-        makeCoreApiRequest(operation, payload) {
-          const id = `req-${nextRequestId}`;
-          const req: CoreApiRequestEnvelope = {
-            operation,
-            id,
-            args: payload,
-          };
-          const promiseCap = openPromise<CoreApiResponse>();
-          requestMap.set(id, {
-            promiseCapability: promiseCap,
-          });
-          connection.sendMessage(req as unknown as JsonMessage);
-          return promiseCap.promise;
-        },
-        close() {
-          connection.close();
-        },
-      };
-      return {
-        result: ctx,
-        onDisconnect() {
-          logger.info("remote wallet disconnected");
-        },
-        onMessage(m) {
-          // FIXME: use a codec for parsing the response envelope!
-
-          logger.info(`got message from remote wallet: ${j2s(m)}`);
-          if (typeof m !== "object" || m == null) {
-            logger.warn("message from wallet not understood (wrong type)");
-            return;
-          }
-          const type = (m as any).type;
-          if (type === "response" || type === "error") {
-            const id = (m as any).id;
-            if (typeof id !== "string") {
-              logger.warn(
-                "message from wallet not understood (no id in response)",
-              );
-              return;
-            }
-            const h = requestMap.get(id);
-            if (!h) {
-              logger.warn(`no handler registered for response id ${id}`);
-              return;
-            }
-            h.promiseCapability.resolve(m as any);
-          } else if (type === "notification") {
-            logger.info("got notification");
-            if (notificationHandler) {
-              notificationHandler((m as any).payload);
-            }
-          } else {
-            logger.warn("message from wallet not understood");
-          }
-        },
-      };
-    },
-  });
-  return ctx;
-}
-
-/**
- * Get a high-level API client from a remove wallet.
- */
-function getClientFromRemoteWallet(w: RemoteWallet): WalletCoreApiClient {
-  const client: WalletCoreApiClient = {
-    async call(op, payload): Promise<any> {
-      const res = await w.makeCoreApiRequest(op, payload);
-      switch (res.type) {
-        case "error":
-          throw TalerError.fromUncheckedDetail(res.error);
-        case "response":
-          return res.result;
-      }
-    },
-  };
-  return client;
-}
-
 async function withWallet<T>(
   walletCliArgs: WalletCliArgsType,
   f: (ctx: WalletContext) => Promise<T>,
 ): Promise<T> {
-  // Bookkeeping for waiting on notification conditions
-  let nextCondIndex = 1;
-  const condMap: Map<
-    number,
-    {
-      condition: (n: WalletNotification) => boolean;
-      promiseCapability: OpenedPromise<void>;
-    }
-  > = new Map();
-  function onNotification(n: WalletNotification) {
-    condMap.forEach((cond, condKey) => {
-      if (cond.condition(n)) {
-        cond.promiseCapability.resolve();
-      }
-    });
-  }
-  function waitForNotificationCond(cond: (n: WalletNotification) => boolean) {
-    const promCap = openPromise<void>();
-    condMap.set(nextCondIndex++, {
-      condition: cond,
-      promiseCapability: promCap,
-    });
-    return promCap.promise;
-  }
+  const waiter = makeNotificationWaiter();
 
   if (walletCliArgs.wallet.walletConnection) {
     logger.info("creating remote wallet");
-    const w = await createRemoteWallet(onNotification);
+    const w = await createRemoteWallet({
+      notificationHandler: waiter.notify,
+      socketFilename: walletCliArgs.wallet.walletConnection,
+    });
     const ctx: WalletContext = {
       makeCoreApiRequest(operation, payload) {
         return w.makeCoreApiRequest(operation, payload);
       },
       client: getClientFromRemoteWallet(w),
-      waitForNotificationCond,
+      waitForNotificationCond: waiter.waitForNotificationCond,
     };
     const res = await f(ctx);
     w.close();
     return res;
   } else {
-    const w = await createLocalWallet(walletCliArgs, onNotification);
+    const w = await createLocalWallet(walletCliArgs, waiter.notify);
     const ctx: WalletContext = {
       client: w.client,
-      waitForNotificationCond,
+      waitForNotificationCond: waiter.waitForNotificationCond,
       makeCoreApiRequest(operation, payload) {
         return w.handleCoreApiRequest(operation, "my-req", payload);
       },
@@ -1053,7 +926,11 @@ advancedCli
   .subcommand("serve", "serve", {
     help: "Serve the wallet API via a unix domain socket.",
   })
+  .requiredOption("unixPath", ["--unix-path"], clk.STRING, {
+    default: "wallet-core.sock",
+  })
   .action(async (args) => {
+    logger.info(`serving at ${args.serve.unixPath}`);
     const w = await createLocalWallet(args);
     w.runTaskLoop()
       .then((res) => {
@@ -1070,7 +947,7 @@ advancedCli
       });
     });
     await runRpcServer({
-      socketFilename: "wallet-core.sock",
+      socketFilename: args.serve.unixPath,
       onConnect(client) {
         logger.info("connected");
         const clientId = nextClientId++;
diff --git a/packages/taler-wallet-core/package.json 
b/packages/taler-wallet-core/package.json
index a0047a03f..4f1692872 100644
--- a/packages/taler-wallet-core/package.json
+++ b/packages/taler-wallet-core/package.json
@@ -36,6 +36,9 @@
       "browser": "./lib/index.browser.js",
       "node": "./lib/index.node.js",
       "default": "./lib/index.js"
+    },
+    "./remote": {
+      "node": "./lib/remote.js"
     }
   },
   "devDependencies": {
diff --git a/packages/taler-wallet-core/src/remote.ts 
b/packages/taler-wallet-core/src/remote.ts
new file mode 100644
index 000000000..a240d4606
--- /dev/null
+++ b/packages/taler-wallet-core/src/remote.ts
@@ -0,0 +1,187 @@
+/*
+ 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 {
+  CoreApiRequestEnvelope,
+  CoreApiResponse,
+  j2s,
+  Logger,
+  WalletNotification,
+} from "@gnu-taler/taler-util";
+import { connectRpc, JsonMessage } from "@gnu-taler/taler-util/twrpc";
+import { TalerError } from "./errors.js";
+import { OpenedPromise, openPromise } from "./index.js";
+import { WalletCoreApiClient } from "./wallet-api-types.js";
+
+const logger = new Logger("remote.ts");
+
+export interface RemoteWallet {
+  /**
+   * Low-level interface for making API requests to wallet-core.
+   */
+  makeCoreApiRequest(
+    operation: string,
+    payload: unknown,
+  ): Promise<CoreApiResponse>;
+
+  /**
+   * Close the connection to the remote wallet.
+   */
+  close(): void;
+}
+
+export interface RemoteWalletConnectArgs {
+  socketFilename: string;
+  notificationHandler?: (n: WalletNotification) => void;
+}
+
+export async function createRemoteWallet(
+  args: RemoteWalletConnectArgs,
+): Promise<RemoteWallet> {
+  let nextRequestId = 1;
+  let requestMap: Map<
+    string,
+    {
+      promiseCapability: OpenedPromise<CoreApiResponse>;
+    }
+  > = new Map();
+
+  const ctx = await connectRpc<RemoteWallet>({
+    socketFilename: args.socketFilename,
+    onEstablished(connection) {
+      const ctx: RemoteWallet = {
+        makeCoreApiRequest(operation, payload) {
+          const id = `req-${nextRequestId}`;
+          const req: CoreApiRequestEnvelope = {
+            operation,
+            id,
+            args: payload,
+          };
+          const promiseCap = openPromise<CoreApiResponse>();
+          requestMap.set(id, {
+            promiseCapability: promiseCap,
+          });
+          connection.sendMessage(req as unknown as JsonMessage);
+          return promiseCap.promise;
+        },
+        close() {
+          connection.close();
+        },
+      };
+      return {
+        result: ctx,
+        onDisconnect() {
+          logger.info("remote wallet disconnected");
+        },
+        onMessage(m) {
+          // FIXME: use a codec for parsing the response envelope!
+
+          logger.info(`got message from remote wallet: ${j2s(m)}`);
+          if (typeof m !== "object" || m == null) {
+            logger.warn("message from wallet not understood (wrong type)");
+            return;
+          }
+          const type = (m as any).type;
+          if (type === "response" || type === "error") {
+            const id = (m as any).id;
+            if (typeof id !== "string") {
+              logger.warn(
+                "message from wallet not understood (no id in response)",
+              );
+              return;
+            }
+            const h = requestMap.get(id);
+            if (!h) {
+              logger.warn(`no handler registered for response id ${id}`);
+              return;
+            }
+            h.promiseCapability.resolve(m as any);
+          } else if (type === "notification") {
+            logger.info("got notification");
+            if (args.notificationHandler) {
+              args.notificationHandler((m as any).payload);
+            }
+          } else {
+            logger.warn("message from wallet not understood");
+          }
+        },
+      };
+    },
+  });
+  return ctx;
+}
+
+/**
+ * Get a high-level API client from a remove wallet.
+ */
+export function getClientFromRemoteWallet(
+  w: RemoteWallet,
+): WalletCoreApiClient {
+  const client: WalletCoreApiClient = {
+    async call(op, payload): Promise<any> {
+      const res = await w.makeCoreApiRequest(op, payload);
+      switch (res.type) {
+        case "error":
+          throw TalerError.fromUncheckedDetail(res.error);
+        case "response":
+          return res.result;
+      }
+    },
+  };
+  return client;
+}
+
+export interface WalletNotificationWaiter {
+  notify(wn: WalletNotification): void;
+  waitForNotificationCond(
+    cond: (n: WalletNotification) => boolean,
+  ): Promise<void>;
+}
+
+/**
+ * Helper that allows creating a promise that resolves when the
+ * wallet
+ */
+export function makeNotificationWaiter(): WalletNotificationWaiter {
+  // Bookkeeping for waiting on notification conditions
+  let nextCondIndex = 1;
+  const condMap: Map<
+    number,
+    {
+      condition: (n: WalletNotification) => boolean;
+      promiseCapability: OpenedPromise<void>;
+    }
+  > = new Map();
+  function onNotification(n: WalletNotification) {
+    condMap.forEach((cond, condKey) => {
+      if (cond.condition(n)) {
+        cond.promiseCapability.resolve();
+      }
+    });
+  }
+  function waitForNotificationCond(cond: (n: WalletNotification) => boolean) {
+    const promCap = openPromise<void>();
+    condMap.set(nextCondIndex++, {
+      condition: cond,
+      promiseCapability: promCap,
+    });
+    return promCap.promise;
+  }
+  return {
+    waitForNotificationCond,
+    notify: onNotification,
+  };
+}

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]