diff options
Diffstat (limited to 'packages/taler-wallet-webextension/src/wxBackend.ts')
-rw-r--r-- | packages/taler-wallet-webextension/src/wxBackend.ts | 533 |
1 files changed, 297 insertions, 236 deletions
diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts index 28adfa037..008f80c57 100644 --- a/packages/taler-wallet-webextension/src/wxBackend.ts +++ b/packages/taler-wallet-webextension/src/wxBackend.ts @@ -24,32 +24,42 @@ * Imports. */ import { - classifyTalerUri, - CoreApiResponse, - CoreApiResponseSuccess, + AbsoluteTime, + BalanceFlag, + LogLevel, Logger, + NotificationType, + OpenedPromise, + SetTimeoutTimerAPI, + TalerError, TalerErrorCode, - TalerUriType, - WalletDiagnostics, + 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, - makeErrorDetail, - OpenedPromise, - openPromise, - openTalerDatabase, - Wallet, - WalletStoresV1, } from "@gnu-taler/taler-wallet-core"; -import { SetTimeoutTimerAPI } from "@gnu-taler/taler-wallet-core"; -import { BrowserCryptoWorkerFactory } from "./browserCryptoWorkerFactory.js"; -import { BrowserHttpLib } from "./browserHttpLib.js"; -import { MessageFromBackend, platform } from "./platform/api.js"; -import { SynchronousCryptoWorkerFactory } from "./serviceWorkerCryptoWorkerFactory.js"; -import { ServiceWorkerHttpLib } from "./serviceWorkerHttpLib.js"; +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 @@ -61,260 +71,285 @@ let currentWallet: Wallet | undefined; let currentDatabase: DbAccess<typeof WalletStoresV1> | undefined; -/** - * Last version of an outdated DB, if applicable. - */ -let outdatedDbVersion: number | undefined; - const walletInit: OpenedPromise<void> = openPromise<void>(); const logger = new Logger("wxBackend.ts"); -async function getDiagnostics(): Promise<WalletDiagnostics> { - const manifestData = platform.getWalletWebExVersion(); - const errors: string[] = []; - let firefoxIdbProblem = false; - let dbOutdated = false; - try { - await walletInit.promise; - } catch (e) { - errors.push("Error during wallet initialization: " + e); - if ( - currentDatabase === undefined && - outdatedDbVersion === undefined && - platform.isFirefox() - ) { - firefoxIdbProblem = true; - } - } - if (!currentWallet) { - errors.push("Could not create wallet backend."); +type BackendHandlerType = { + [Op in keyof BackgroundOperations]: ( + req: BackgroundOperations[Op]["request"], + ) => Promise<BackgroundOperations[Op]["response"]>; +}; + +type ExtensionHandlerType = { + [Op in keyof ExtensionOperations]: ( + req: ExtensionOperations[Op]["request"], + ) => Promise<ExtensionOperations[Op]["response"]>; +}; + +async function resetDb(): Promise<void> { + await deleteTalerDatabase(indexedDB as any); + await reinitWallet(); +} + +//FIXME: maybe circular buffer +const notifications: WalletEvent[] = []; +async function getNotifications(): Promise<WalletEvent[]> { + return notifications; +} + +async function clearNotifications(): Promise<void> { + notifications.splice(0, notifications.length); +} + +async function runGarbageCollector(): Promise<void> { + const dbBeforeGc = currentDatabase; + if (!dbBeforeGc) { + throw Error("no current db before running gc"); } - if (!currentDatabase) { - errors.push("Could not open database"); + 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"); } - if (outdatedDbVersion !== undefined) { - errors.push(`Outdated DB version: ${outdatedDbVersion}`); - dbOutdated = true; + await importDb(dbAfterGc.idbHandle(), dump); + logger.info("imported"); +} + +const extensionHandlers: ExtensionHandlerType = { + isAutoOpenEnabled, + isDomainTrusted, +}; + +async function isAutoOpenEnabled(): Promise<boolean> { + const settings = await platform.getSettingsFromStorage(); + return settings.autoOpen === true; +} + +async function isDomainTrusted(): Promise<boolean> { + 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<void> { + logger.info(`setting ${tag} to ${level}`); + if (!tag) { + setGlobalLogLevelFromString(level); + } else { + setLogLevelFromString(tag, level); } - const diagnostics: WalletDiagnostics = { - walletManifestDisplayVersion: manifestData.version_name || "(undefined)", - walletManifestVersion: manifestData.version, - errors, - firefoxIdbProblem, - dbOutdated, - }; - return diagnostics; } +let nextMessageIndex = 0; -async function dispatch( - req: any, - sender: any, - sendResponse: any, -): Promise<void> { - let r: CoreApiResponse; - - const wrapResponse = (result: unknown): CoreApiResponseSuccess => { - return { - type: "response", - id: req.id, - operation: req.operation, - result, - }; - }; +async function dispatch< + Op extends WalletOperations | BackgroundOperations | ExtensionOperations, +>(req: MessageFromFrontend<Op> & { id: string }): Promise<MessageResponse> { + nextMessageIndex = (nextMessageIndex + 1) % (Number.MAX_SAFE_INTEGER - 100); - try { - switch (req.operation) { - case "wxGetDiagnostics": { - r = wrapResponse(await getDiagnostics()); - break; - } - case "reset-db": { - await deleteTalerDatabase(indexedDB as any); - r = wrapResponse(await reinitWallet()); - break; + 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`), + ), + }; } - case "run-gc": { - logger.info("gc"); - const dump = await exportDb(currentDatabase!.idbHandle()); - await deleteTalerDatabase(indexedDB as any); - logger.info("cleaned"); - await reinitWallet(); - logger.info("init"); - await importDb(currentDatabase!.idbHandle(), dump); - logger.info("imported"); - r = wrapResponse({ result: true }); - break; - } - case "containsHeaderListener": { - const res = await platform.containsTalerHeaderListener(); - r = wrapResponse({ newValue: res }); - break; + 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), + }; } - //FIXME: implement type checked api like WalletCoreApi - case "toggleHeaderListener": { - const newVal = req.payload.value; - logger.trace("new extended permissions value", newVal); - if (newVal) { - platform.registerTalerHeaderListener(parseTalerUriAndRedirect); - r = wrapResponse({ newValue: true }); - } else { - const rem = await platform - .getPermissionsApi() - .removeHostPermissions(); - logger.trace("permissions removed:", rem); - r = wrapResponse({ newVal: false }); - } - break; + } + 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`), + ), + }; } - default: { - const w = currentWallet; - if (!w) { - r = { - type: "error", - id: req.id, - operation: req.operation, - error: makeErrorDetail( - TalerErrorCode.WALLET_CORE_NOT_AVAILABLE, - {}, - "wallet core not available", - ), - }; - break; - } - r = await w.handleCoreApiRequest(req.operation, req.id, req.payload); - console.log("response received from wallet", r); - break; + 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; - sendResponse(r); - } catch (e) { - logger.error(`Error sending operation: ${req.operation}`, e); - // might fail if tab disconnected + 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<void> { if (currentWallet) { - currentWallet.stop(); + await currentWallet.client.call(WalletApiOperation.Shutdown, {}); currentWallet = undefined; } currentDatabase = undefined; // setBadgeText({ text: "" }); - try { - currentDatabase = await openTalerDatabase(indexedDB as any, reinitWallet); - } catch (e) { - logger.error("could not open database", e); - walletInit.reject(e); - return; - } - let httpLib; let cryptoWorker; let timer; + const httpFactory = (): HttpRequestLibrary => { + return new BrowserFetchHttpLib({ + // enableThrottling: false, + }); + }; + if (platform.useServiceWorkerAsBackgroundProcess()) { - httpLib = new ServiceWorkerHttpLib(); - cryptoWorker = new SynchronousCryptoWorkerFactory(); + cryptoWorker = new SynchronousCryptoWorkerFactoryPlain(); timer = new SetTimeoutTimerAPI(); } else { - httpLib = new BrowserHttpLib(); // We could (should?) use the BrowserCryptoWorkerFactory here, // but right now we don't, to have less platform differences. // cryptoWorker = new BrowserCryptoWorkerFactory(); - cryptoWorker = new SynchronousCryptoWorkerFactory(); + cryptoWorker = new SynchronousCryptoWorkerFactoryPlain(); timer = new SetTimeoutTimerAPI(); } + const settings = await platform.getSettingsFromStorage(); logger.info("Setting up wallet"); const wallet = await Wallet.create( - currentDatabase, - httpLib, + indexedDB as any, + httpFactory as any, timer, cryptoWorker, ); try { - await wallet.handleCoreApiRequest("initWallet", "native-init", {}); + 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((x) => { - const message: MessageFromBackend = { type: x.type }; - platform.sendMessageToAllChannels(message); - }); + wallet.addNotificationListener((message) => { + if (settings.showWalletActivity) { + notifications.push({ + notification: message, + when: AbsoluteTime.now(), + }); + } + + processWalletNotification(message); - platform.keepAlive(() => { - return wallet.runTaskLoop().catch((e) => { - logger.error("error during wallet task loop", e); + 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(); } -function parseTalerUriAndRedirect(tabId: number, talerUri: string): void { - const uriType = classifyTalerUri(talerUri); - switch (uriType) { - case TalerUriType.TalerWithdraw: - return platform.redirectTabToWalletPage( - tabId, - `/cta/withdraw?talerWithdrawUri=${talerUri}`, - ); - case TalerUriType.TalerPay: - return platform.redirectTabToWalletPage( - tabId, - `/cta/pay?talerPayUri=${talerUri}`, - ); - case TalerUriType.TalerTip: - return platform.redirectTabToWalletPage( - tabId, - `/cta/tip?talerTipUri=${talerUri}`, - ); - case TalerUriType.TalerRefund: - return platform.redirectTabToWalletPage( - tabId, - `/cta/refund?talerRefundUri=${talerUri}`, - ); - case TalerUriType.TalerPayPull: - return platform.redirectTabToWalletPage( - tabId, - `/cta/invoice/pay?talerPayPullUri=${talerUri}`, - ); - case TalerUriType.TalerPayPush: - return platform.redirectTabToWalletPage( - tabId, - `/cta/transfer/pickup?talerPayPushUri=${talerUri}`, - ); - case TalerUriType.TalerRecovery: - return platform.redirectTabToWalletPage( - tabId, - `/cta/transfer/recovery?talerBackupUri=${talerUri}`, - ); - case TalerUriType.Unknown: - logger.warn( - `Response with HTTP 402 the Taler header but could not classify ${talerUri}`, - ); - return; - case TalerUriType.TalerDevExperiment: - // FIXME: Implement! - logger.warn("not implemented"); - return; - default: { - const error: never = uriType; - logger.warn( - `Response with HTTP 402 the Taler header "${error}", but header value is not a taler:// URI.`, - ); - return; - } - } -} - /** * Main function to run for the WebExtension backend. * @@ -324,45 +359,71 @@ export async function wxMain(): Promise<void> { 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 - platform.listenToAllChannels((message, sender, callback) => { - afterWalletIsInitialized.then(() => { - dispatch(message, sender, (response: CoreApiResponse) => { - callback(response); - }); - }); + 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"); - - // - try { - platform.registerTalerHeaderListener(parseTalerUriAndRedirect); - } catch (e) { - logger.error("could not register header listener", e); - } }); } catch (e) { console.error(e); } +} - // On platforms that support it, also listen to external - // modification of permissions. - platform.getPermissionsApi().addPermissionsListener((perm, lastError) => { - if (lastError) { - logger.error( - `there was a problem trying to get permission ${perm}`, - lastError, - ); - return; +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; + } } - platform.registerTalerHeaderListener(parseTalerUriAndRedirect); - }); + + 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(); + } } |