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