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