diff options
Diffstat (limited to 'packages/taler-wallet-core/src/remote.ts')
-rw-r--r-- | packages/taler-wallet-core/src/remote.ts | 191 |
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, + }; +} |