/* 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 */ 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; /** * 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 { let nextRequestId = 1; let requestMap: Map< string, { promiseCapability: OpenedPromise; } > = new Map(); const ctx = await connectRpc({ 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(); 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 { 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) => T | false | undefined, ): Promise; } interface NotificationCondEntry { condition: (n: WalletNotification) => T | false | undefined; promiseCapability: OpenedPromise; } /** * 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> = new Map(); function onNotification(n: WalletNotification) { condMap.forEach((cond, condKey) => { const res = cond.condition(n); if (res) { cond.promiseCapability.resolve(res); } }); } function waitForNotificationCond( cond: (n: WalletNotification) => T | false | undefined, ) { const promCap = openPromise(); condMap.set(nextCondIndex++, { condition: cond, promiseCapability: promCap, }); return promCap.promise; } return { waitForNotificationCond, notify: onNotification, }; }