summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/remote.ts
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2023-02-02 20:20:58 +0100
committerFlorian Dold <florian@dold.me>2023-02-02 20:21:04 +0100
commit96101238afb82d200cf9d5005ffc2fc0391f23e4 (patch)
treedcade21b174dcc7e2d479de61bf53b07b2e3a187 /packages/taler-wallet-core/src/remote.ts
parentab9a5e1e8ac60bbf55104e84490e581dfad5de02 (diff)
downloadwallet-core-96101238afb82d200cf9d5005ffc2fc0391f23e4.tar.gz
wallet-core-96101238afb82d200cf9d5005ffc2fc0391f23e4.tar.bz2
wallet-core-96101238afb82d200cf9d5005ffc2fc0391f23e4.zip
harness,wallet-cli: notification-based testing with RPC wallet
Diffstat (limited to 'packages/taler-wallet-core/src/remote.ts')
-rw-r--r--packages/taler-wallet-core/src/remote.ts187
1 files changed, 187 insertions, 0 deletions
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,
+ };
+}