/* This file is part of GNU Taler (C) 2019 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 */ /** * Helpers to create headless wallets. * @author Florian Dold */ /** * Imports. */ import type { IDBFactory, ResultRow, Sqlite3Interface, Sqlite3Statement, } from "@gnu-taler/idb-bridge"; // eslint-disable-next-line no-duplicate-imports import { AccessStats, BridgeIDBFactory, MemoryBackend, createSqliteBackend, shimIndexedDB, } from "@gnu-taler/idb-bridge"; import { Logger, SetTimeoutTimerAPI, WalletRunConfig, } from "@gnu-taler/taler-util"; import { createPlatformHttpLib } from "@gnu-taler/taler-util/http"; import { qjsOs, qjsStd } from "@gnu-taler/taler-util/qtart"; import { SynchronousCryptoWorkerFactoryPlain } from "./crypto/workers/synchronousWorkerFactoryPlain.js"; import { DefaultNodeWalletArgs, makeTempfileId } from "./host-common.js"; import { Wallet } from "./wallet.js"; const logger = new Logger("host-impl.qtart.ts"); interface MakeDbResult { idbFactory: BridgeIDBFactory; getStats: () => AccessStats; } let numStmt = 0; export async function createQtartSqlite3Impl(): Promise { const tart: any = (globalThis as any)._tart; if (!tart) { throw Error("globalThis._qtart not defined"); } return { open(filename: string) { const internalDbHandle = tart.sqlite3Open(filename); return { internalDbHandle, close() { tart.sqlite3Close(internalDbHandle); }, prepare(stmtStr): Sqlite3Statement { const stmtHandle = tart.sqlite3Prepare(internalDbHandle, stmtStr); return { internalStatement: stmtHandle, getAll(params): ResultRow[] { numStmt++; return tart.sqlite3StmtGetAll(stmtHandle, params); }, getFirst(params): ResultRow | undefined { numStmt++; return tart.sqlite3StmtGetFirst(stmtHandle, params); }, run(params) { numStmt++; return tart.sqlite3StmtRun(stmtHandle, params); }, }; }, exec(sqlStr): void { numStmt++; tart.sqlite3Exec(internalDbHandle, sqlStr); }, }; }, }; } async function makeSqliteDb( args: DefaultNodeWalletArgs, ): Promise { BridgeIDBFactory.enableTracing = false; const imp = await createQtartSqlite3Impl(); const myBackend = await createSqliteBackend(imp, { filename: args.persistentStoragePath ?? ":memory:", }); myBackend.trackStats = true; myBackend.enableTracing = false; const myBridgeIdbFactory = new BridgeIDBFactory(myBackend); return { getStats() { return { ...myBackend.accessStats, primitiveStatements: numStmt, }; }, idbFactory: myBridgeIdbFactory, }; } async function makeFileDb( args: DefaultNodeWalletArgs = {}, ): Promise { BridgeIDBFactory.enableTracing = false; const myBackend = new MemoryBackend(); myBackend.enableTracing = false; const storagePath = args.persistentStoragePath; if (storagePath) { const dbContentStr = qjsStd.loadFile(storagePath); if (dbContentStr != null) { const dbContent = JSON.parse(dbContentStr); myBackend.importDump(dbContent); } myBackend.afterCommitCallback = async () => { logger.trace("committing database"); // Allow caller to stop persisting the wallet. if (args.persistentStoragePath === undefined) { return; } const tmpPath = `${args.persistentStoragePath}-${makeTempfileId(5)}.tmp`; const dbContent = myBackend.exportDump(); logger.trace("exported DB dump"); qjsStd.writeFile(tmpPath, JSON.stringify(dbContent, undefined, 2)); // Atomically move the temporary file onto the DB path. const res = qjsOs.rename(tmpPath, args.persistentStoragePath); if (res != 0) { throw Error("db commit failed at rename"); } logger.trace("committing database done"); }; } const myBridgeIdbFactory = new BridgeIDBFactory(myBackend); return { idbFactory: myBridgeIdbFactory, getStats: () => myBackend.accessStats, }; } export async function createNativeWalletHost2( args: DefaultNodeWalletArgs = {}, ): Promise<{ wallet: Wallet; getDbStats: () => AccessStats; }> { BridgeIDBFactory.enableTracing = false; let dbResp: MakeDbResult; if ( args.persistentStoragePath && args.persistentStoragePath.endsWith(".json") ) { logger.info("using JSON file DB backend (slow, only use for testing)"); dbResp = await makeFileDb(args); } else { logger.info("using sqlite3 DB backend"); dbResp = await makeSqliteDb(args); } const myIdbFactory: IDBFactory = dbResp.idbFactory as any as IDBFactory; shimIndexedDB(dbResp.idbFactory); const myHttpFactory = (config: WalletRunConfig) => { let myHttpLib; if (args.httpLib) { myHttpLib = args.httpLib; } else { myHttpLib = createPlatformHttpLib({ enableThrottling: true, requireTls: !config.features.allowHttp, }); } return myHttpLib; }; let workerFactory; workerFactory = new SynchronousCryptoWorkerFactoryPlain(); const timer = new SetTimeoutTimerAPI(); const w = await Wallet.create( myIdbFactory, myHttpFactory, timer, workerFactory, ); if (args.notifyHandler) { w.addNotificationListener(args.notifyHandler); } return { wallet: w, getDbStats: dbResp.getStats, }; }