2023-02-02 20:20:58 +01:00
|
|
|
/*
|
|
|
|
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;
|
2023-02-12 19:30:59 +01:00
|
|
|
waitForNotificationCond<T>(
|
|
|
|
cond: (n: WalletNotification) => T | false | undefined,
|
|
|
|
): Promise<T>;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface NotificationCondEntry<T> {
|
|
|
|
condition: (n: WalletNotification) => T | false | undefined;
|
|
|
|
promiseCapability: OpenedPromise<T>;
|
2023-02-02 20:20:58 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Helper that allows creating a promise that resolves when the
|
|
|
|
* wallet
|
|
|
|
*/
|
|
|
|
export function makeNotificationWaiter(): WalletNotificationWaiter {
|
|
|
|
// Bookkeeping for waiting on notification conditions
|
|
|
|
let nextCondIndex = 1;
|
2023-02-12 19:30:59 +01:00
|
|
|
const condMap: Map<number, NotificationCondEntry<any>> = new Map();
|
2023-02-02 20:20:58 +01:00
|
|
|
function onNotification(n: WalletNotification) {
|
|
|
|
condMap.forEach((cond, condKey) => {
|
2023-02-12 19:30:59 +01:00
|
|
|
const res = cond.condition(n);
|
|
|
|
if (res) {
|
|
|
|
cond.promiseCapability.resolve(res);
|
2023-02-02 20:20:58 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2023-02-12 19:30:59 +01:00
|
|
|
function waitForNotificationCond<T>(
|
|
|
|
cond: (n: WalletNotification) => T | false | undefined,
|
|
|
|
) {
|
|
|
|
const promCap = openPromise<T>();
|
2023-02-02 20:20:58 +01:00
|
|
|
condMap.set(nextCondIndex++, {
|
|
|
|
condition: cond,
|
|
|
|
promiseCapability: promCap,
|
|
|
|
});
|
|
|
|
return promCap.promise;
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
waitForNotificationCond,
|
|
|
|
notify: onNotification,
|
|
|
|
};
|
|
|
|
}
|