/*
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,
};
}