/*
This file is part of GNU Taler
(C) 2019 GNUnet e.V.
(C) 2024 Taler Systems SA
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
*/
/**
* Entry-point for the wallet under qtart, the QuickJS-based GNU Taler runtime.
*/
/**
* Imports.
*/
import {
discoverPolicies,
getBackupStartState,
getRecoveryStartState,
mergeDiscoveryAggregate,
reduceAction,
} from "@gnu-taler/anastasis-core";
import {
CoreApiMessageEnvelope,
CoreApiResponse,
CoreApiResponseSuccess,
Logger,
WalletNotification,
enableNativeLogging,
getErrorDetailFromException,
openPromise,
performanceNow,
setGlobalLogLevelFromString,
} from "@gnu-taler/taler-util";
import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
import { qjsOs } from "@gnu-taler/taler-util/qtart";
import { Wallet, createNativeWalletHost2 } from "@gnu-taler/taler-wallet-core";
import {
testArgon2id,
testWithFdold,
testWithGv,
testWithLocal,
} from './wallet-qjs-tests.js';
setGlobalLogLevelFromString("trace");
const logger = new Logger("taler-wallet-embedded/index.ts");
/**
* Sends JSON to the host application, i.e. the process that
* runs the JavaScript interpreter (quickjs / qtart) to run
* the embedded wallet.
*/
function sendNativeMessage(ev: CoreApiMessageEnvelope): void {
const m = JSON.stringify(ev);
qjsOs.postMessageToHost(m);
}
class NativeWalletMessageHandler {
wp = openPromise();
httpLib = createPlatformHttpLib();
/**
* Handle a request from the native wallet.
*/
async handleMessage(
operation: string,
id: string,
args: any,
): Promise {
const wrapSuccessResponse = (result: unknown): CoreApiResponseSuccess => {
return {
type: "response",
id,
operation,
result,
};
};
switch (operation) {
case "init": {
const wR = await createNativeWalletHost2({
notifyHandler: async (notification: WalletNotification) => {
sendNativeMessage({ type: "notification", payload: notification });
},
persistentStoragePath: args.persistentStoragePath,
httpLib: this.httpLib,
cryptoWorkerType: args.cryptoWorkerType,
...args,
});
if (args.logLevel) {
setGlobalLogLevelFromString(args.logLevel);
}
if (args.useNativeLogging === true) {
enableNativeLogging();
}
const resp = await wR.wallet.handleCoreApiRequest("initWallet", "native-init", {
config: args.config ?? {},
});
let initResponse: any = resp.type == "response" ? resp.result : resp.error;
this.wp.resolve(wR.wallet);
return wrapSuccessResponse({
...initResponse,
});
}
default: {
const wallet = await this.wp.promise;
return await wallet.handleCoreApiRequest(operation, id, args);
}
}
}
}
/**
* Handle an Anastasis request from the native app.
*/
async function handleAnastasisRequest(
operation: string,
id: string,
args: any,
): Promise {
const wrapSuccessResponse = (result: unknown): CoreApiResponseSuccess => {
return {
type: "response",
id,
operation,
result,
};
};
let req = args ?? {};
switch (operation) {
case "anastasisReduce": {
let reduceRes = await reduceAction(req.state, req.action, req.args ?? {});
// For now, this will return "success" even if the wrapped Anastasis
// response is a ReducerStateError.
return wrapSuccessResponse(reduceRes);
}
case "anastasisStartBackup": {
return wrapSuccessResponse(await getBackupStartState());
}
case "anastasisStartRecovery": {
return wrapSuccessResponse(await getRecoveryStartState());
}
case "anastasisDiscoverPolicies": {
let discoverRes = await discoverPolicies(req.state, req.cursor);
let aggregatedPolicies = mergeDiscoveryAggregate(
discoverRes.policies ?? [],
req.state.discoveryState?.aggregatedPolicies ?? [],
);
return wrapSuccessResponse({
...req.state,
discoveryState: {
state: "finished",
aggregatedPolicies,
cursor: discoverRes.cursor,
},
});
}
default: {
throw Error("unsupported anastasis operation");
}
}
}
export function installNativeWalletListener(): void {
setGlobalLogLevelFromString("trace");
const handler = new NativeWalletMessageHandler();
const onMessage = async (msgStr: any): Promise => {
if (typeof msgStr !== "string") {
logger.error("expected string as message");
return;
}
const msg = JSON.parse(msgStr);
const operation = msg.operation;
if (typeof operation !== "string") {
logger.error(
"message to native wallet helper must contain operation of type string",
);
return;
}
const id = msg.id;
logger.info(`native listener: got request for ${operation} (${id})`);
const startTimeMs = performanceNow();
let respMsg: CoreApiResponse;
try {
if (msg.operation.startsWith("anastasis")) {
// Entry point for Anastasis
respMsg = await handleAnastasisRequest(operation, id, msg.args ?? {});
} else if (msg.operation === "testing-dangerously-eval") {
// Eval code, used only for testing. No client may rely on this.
logger.info(`evaluating ${msg.args.jscode}`);
const f = new Function(msg.args.jscode);
f();
respMsg = {
type: "response",
result: {},
operation: "testing-dangerously-eval",
id: msg.id,
};
} else {
// Entry point for wallet-core
respMsg = await handler.handleMessage(operation, id, msg.args ?? {});
}
} catch (e) {
respMsg = {
type: "error",
id,
operation,
error: getErrorDetailFromException(e),
};
}
const endTimeMs = performanceNow();
const requestDurationMs = Math.round(
Number((endTimeMs - startTimeMs) / 1000n / 1000n),
);
logger.info(
`native listener: sending back ${respMsg.type} message for operation ${operation} (${id}) after ${requestDurationMs} ms`,
);
sendNativeMessage(respMsg);
};
qjsOs.setMessageFromHostHandler((m) => onMessage(m));
logger.info("native wallet listener installed");
}
// @ts-ignore
globalThis.installNativeWalletListener = installNativeWalletListener;
// @ts-ignore
globalThis.testWithGv = testWithGv;
// @ts-ignore
globalThis.testWithLocal = testWithLocal;
// @ts-ignore
globalThis.testArgon2id = testArgon2id;
// @ts-ignore
globalThis.testReduceAction = reduceAction;
// @ts-ignore
globalThis.testDiscoverPolicies = discoverPolicies;
// @ts-ignore
globalThis.testWithFdold = testWithFdold;