187 lines
5.2 KiB
TypeScript
187 lines
5.2 KiB
TypeScript
|
/*
|
||
|
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") {
|
||
|
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,
|
||
|
};
|
||
|
}
|