summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/remote.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-core/src/remote.ts')
-rw-r--r--packages/taler-wallet-core/src/remote.ts191
1 files changed, 191 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..d7623baab
--- /dev/null
+++ b/packages/taler-wallet-core/src/remote.ts
@@ -0,0 +1,191 @@
+/*
+ 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,
+ Logger,
+ OpenedPromise,
+ openPromise,
+ TalerError,
+ WalletNotification,
+} from "@gnu-taler/taler-util";
+import { connectRpc, JsonMessage } from "@gnu-taler/taler-util/twrpc";
+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 {
+ name?: string;
+ 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}`;
+ nextRequestId += 1;
+ 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(`${args.name}: remote wallet disconnected`);
+ },
+ onMessage(m) {
+ // FIXME: use a codec for parsing the response envelope!
+
+ if (typeof m !== "object" || m == null) {
+ logger.warn(`${args.name}: message 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(
+ `${args.name}: message not understood (no id in response)`,
+ );
+ return;
+ }
+ const h = requestMap.get(id);
+ if (!h) {
+ logger.warn(
+ `${args.name}: no handler registered for response id ${id}`,
+ );
+ return;
+ }
+ h.promiseCapability.resolve(m as any);
+ } else if (type === "notification") {
+ if (args.notificationHandler) {
+ args.notificationHandler((m as any).payload);
+ }
+ } else {
+ logger.warn(`${args.name}: message 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<T>(
+ cond: (n: WalletNotification) => T | false | undefined,
+ ): Promise<T>;
+}
+
+interface NotificationCondEntry<T> {
+ condition: (n: WalletNotification) => T | false | undefined;
+ promiseCapability: OpenedPromise<T>;
+}
+
+/**
+ * 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, NotificationCondEntry<any>> = new Map();
+ function onNotification(n: WalletNotification) {
+ condMap.forEach((cond, condKey) => {
+ const res = cond.condition(n);
+ if (res) {
+ cond.promiseCapability.resolve(res);
+ }
+ });
+ }
+ function waitForNotificationCond<T>(
+ cond: (n: WalletNotification) => T | false | undefined,
+ ) {
+ const promCap = openPromise<T>();
+ condMap.set(nextCondIndex++, {
+ condition: cond,
+ promiseCapability: promCap,
+ });
+ return promCap.promise;
+ }
+ return {
+ waitForNotificationCond,
+ notify: onNotification,
+ };
+}