/* This file is part of GNU Taler (C) 2022 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 */ /** * Messaging for the WebExtensions wallet. Should contain * parts that are specific for WebExtensions, but as little business * logic as possible. */ /** * Imports. */ import { AbsoluteTime, BalanceFlag, LogLevel, Logger, NotificationType, OpenedPromise, SetTimeoutTimerAPI, TalerError, TalerErrorCode, TalerErrorDetail, TransactionMajorState, TransactionMinorState, WalletNotification, getErrorDetailFromException, makeErrorDetail, openPromise, setGlobalLogLevelFromString, setLogLevelFromString, } from "@gnu-taler/taler-util"; import { HttpRequestLibrary } from "@gnu-taler/taler-util/http"; import { DbAccess, SynchronousCryptoWorkerFactoryPlain, Wallet, WalletApiOperation, WalletOperations, WalletStoresV1, deleteTalerDatabase, exportDb, importDb, } from "@gnu-taler/taler-wallet-core"; import { MessageFromFrontend, MessageResponse } from "./platform/api.js"; import { platform } from "./platform/background.js"; import { ExtensionOperations } from "./taler-wallet-interaction-loader.js"; import { BackgroundOperations, WalletEvent } from "./wxApi.js"; import { BrowserFetchHttpLib } from "@gnu-taler/web-util/browser"; /** * Currently active wallet instance. Might be unloaded and * re-instantiated when the database is reset. * * FIXME: Maybe move the wallet resetting into the Wallet class? */ let currentWallet: Wallet | undefined; let currentDatabase: DbAccess | undefined; const walletInit: OpenedPromise = openPromise(); const logger = new Logger("wxBackend.ts"); type BackendHandlerType = { [Op in keyof BackgroundOperations]: ( req: BackgroundOperations[Op]["request"], ) => Promise; }; type ExtensionHandlerType = { [Op in keyof ExtensionOperations]: ( req: ExtensionOperations[Op]["request"], ) => Promise; }; async function resetDb(): Promise { await deleteTalerDatabase(indexedDB as any); await reinitWallet(); } //FIXME: maybe circular buffer const notifications: WalletEvent[] = []; async function getNotifications(): Promise { return notifications; } async function clearNotifications(): Promise { notifications.splice(0, notifications.length); } async function runGarbageCollector(): Promise { const dbBeforeGc = currentDatabase; if (!dbBeforeGc) { throw Error("no current db before running gc"); } const dump = await exportDb(indexedDB as any); await deleteTalerDatabase(indexedDB as any); logger.info("cleaned"); await reinitWallet(); logger.info("init"); const dbAfterGc = currentDatabase; if (!dbAfterGc) { throw Error("no current db before running gc"); } await importDb(dbAfterGc.idbHandle(), dump); logger.info("imported"); } const extensionHandlers: ExtensionHandlerType = { isAutoOpenEnabled, isDomainTrusted, }; async function isAutoOpenEnabled(): Promise { const settings = await platform.getSettingsFromStorage(); return settings.autoOpen === true; } async function isDomainTrusted(): Promise { const settings = await platform.getSettingsFromStorage(); return settings.injectTalerSupport === true; } const backendHandlers: BackendHandlerType = { resetDb, runGarbageCollector, getNotifications, clearNotifications, reinitWallet, setLoggingLevel, }; async function setLoggingLevel({ tag, level, }: { tag?: string; level: LogLevel; }): Promise { logger.info(`setting ${tag} to ${level}`); if (!tag) { setGlobalLogLevelFromString(level); } else { setLogLevelFromString(tag, level); } } let nextMessageIndex = 0; async function dispatch< Op extends WalletOperations | BackgroundOperations | ExtensionOperations, >(req: MessageFromFrontend & { id: string }): Promise { nextMessageIndex = (nextMessageIndex + 1) % (Number.MAX_SAFE_INTEGER - 100); switch (req.channel) { case "background": { const handler = backendHandlers[req.operation] as (req: any) => any; if (!handler) { return { type: "error", id: req.id, operation: String(req.operation), error: getErrorDetailFromException( Error(`unknown background operation`), ), }; } try { const result = await handler(req.payload); return { type: "response", id: req.id, operation: String(req.operation), result, }; } catch (er) { return { type: "error", id: req.id, error: getErrorDetailFromException(er), operation: String(req.operation), }; } } case "extension": { const handler = extensionHandlers[req.operation] as (req: any) => any; if (!handler) { return { type: "error", id: req.id, operation: String(req.operation), error: getErrorDetailFromException( Error(`unknown extension operation`), ), }; } try { const result = await handler(req.payload); return { type: "response", id: req.id, operation: String(req.operation), result, }; } catch (er) { return { type: "error", id: req.id, error: getErrorDetailFromException(er), operation: String(req.operation), }; } } case "wallet": { const w = currentWallet; if (!w) { const lastError: TalerErrorDetail = walletInit.lastError instanceof TalerError ? walletInit.lastError.errorDetail : undefined; return { type: "error", id: req.id, operation: req.operation, error: makeErrorDetail( TalerErrorCode.WALLET_CORE_NOT_AVAILABLE, { lastError }, `wallet core not available${ !lastError ? "" : `,last error: ${lastError.hint}` }`, ), }; } //multiple client can create the same id, send the wallet an unique key const newId = `${req.id}_${nextMessageIndex}`; const resp = await w.handleCoreApiRequest( req.operation, newId, req.payload, ); //return to the client the original id resp.id = req.id; return resp; } } const anyReq = req as any; return { type: "error", id: anyReq.id, operation: String(anyReq.operation), error: getErrorDetailFromException( Error( `unknown channel ${anyReq.channel}, should be "background", "extension" or "wallet"`, ), ), }; } async function reinitWallet(): Promise { if (currentWallet) { await currentWallet.client.call(WalletApiOperation.Shutdown, {}); currentWallet = undefined; } currentDatabase = undefined; // setBadgeText({ text: "" }); let cryptoWorker; let timer; const httpFactory = (): HttpRequestLibrary => { return new BrowserFetchHttpLib({ // enableThrottling: false, }); }; if (platform.useServiceWorkerAsBackgroundProcess()) { cryptoWorker = new SynchronousCryptoWorkerFactoryPlain(); timer = new SetTimeoutTimerAPI(); } else { // We could (should?) use the BrowserCryptoWorkerFactory here, // but right now we don't, to have less platform differences. // cryptoWorker = new BrowserCryptoWorkerFactory(); cryptoWorker = new SynchronousCryptoWorkerFactoryPlain(); timer = new SetTimeoutTimerAPI(); } const settings = await platform.getSettingsFromStorage(); logger.info("Setting up wallet"); const wallet = await Wallet.create( indexedDB as any, httpFactory as any, timer, cryptoWorker, ); try { await wallet.handleCoreApiRequest("initWallet", "native-init", { config: { testing: { emitObservabilityEvents: settings.showWalletActivity, devModeActive: settings.advancedMode, }, features: { allowHttp: settings.walletAllowHttp, }, }, }); } catch (e) { logger.error("could not initialize wallet", e); walletInit.reject(e); return; } wallet.addNotificationListener((message) => { if (settings.showWalletActivity) { notifications.push({ notification: message, when: AbsoluteTime.now(), }); } processWalletNotification(message); platform.sendMessageToAllChannels({ type: "wallet", notification: message, }); }); // Useful for debugging in the background page. if (typeof window !== "undefined") { (window as any).talerWallet = wallet; } currentWallet = wallet; updateIconBasedOnBalance(); return walletInit.resolve(); } /** * Main function to run for the WebExtension backend. * * Sets up all event handlers and other machinery. */ export async function wxMain(): Promise { logger.trace("starting"); const afterWalletIsInitialized = reinitWallet(); logger.trace("reload on new version"); platform.registerReloadOnNewVersion(); // Handlers for messages coming directly from the content // script on the page logger.trace("listen all channels"); platform.listenToAllChannels(async (message) => { //wait until wallet is initialized await afterWalletIsInitialized; const result = await dispatch(message); return result; }); logger.trace("register all incoming connections"); platform.registerAllIncomingConnections(); logger.trace("redirect if first start"); try { platform.registerOnInstalled(() => { platform.openWalletPage("/welcome"); }); } catch (e) { console.error(e); } } async function updateIconBasedOnBalance() { const balance = await currentWallet?.client.call( WalletApiOperation.GetBalances, {}, ); if (balance) { let showAlert = false; for (const b of balance.balances) { if (b.flags.length > 0) { console.log("b.flags", JSON.stringify(b.flags)) showAlert = true; break; } } if (showAlert) { platform.setAlertedIcon(); } else { platform.setNormalIcon(); } } } /** * All the actions triggered by notification that need to be * run in the background. * * @param message */ async function processWalletNotification(message: WalletNotification) { if ( message.type === NotificationType.TransactionStateTransition && (message.newTxState.minor === TransactionMinorState.KycRequired || message.oldTxState.minor === TransactionMinorState.KycRequired || message.newTxState.minor === TransactionMinorState.AmlRequired || message.oldTxState.minor === TransactionMinorState.AmlRequired || message.newTxState.minor === TransactionMinorState.BankConfirmTransfer || message.oldTxState.minor === TransactionMinorState.BankConfirmTransfer) ) { await updateIconBasedOnBalance(); } }