taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 6ae996e78eba29c4aaf19a347dccddf8003441ee
parent 51d902402de1356a9089d9ab867d340dcaf83315
Author: Florian Dold <florian@dold.me>
Date:   Fri, 20 Jun 2025 22:03:32 +0200

wallet-core: implement importDbFromFile

Diffstat:
Mpackages/taler-util/src/qtart.ts | 12++++++++++++
Mpackages/taler-util/src/types-taler-wallet.ts | 13+++++++++++++
Mpackages/taler-wallet-core/src/host-impl.node.ts | 10+++++++---
Mpackages/taler-wallet-core/src/host-impl.qtart.ts | 23+++++++++++++++++------
Mpackages/taler-wallet-core/src/wallet-api-types.ts | 14++++++++++++++
Mpackages/taler-wallet-core/src/wallet.ts | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
6 files changed, 144 insertions(+), 17 deletions(-)

diff --git a/packages/taler-util/src/qtart.ts b/packages/taler-util/src/qtart.ts @@ -20,15 +20,27 @@ export interface ErrReceiver { errno?: number; } +/** + * File handle. Thin wrapper around a libc FILE object. + */ export interface QjsFile { /** * Close the file. Return 0 if OK or -errno in case of I/O error. */ close(): number; + /** * Outputs the string with UTF-8 encoding. */ puts(str: string): void; + + /** + * Read max_size bytes from the file and return them as a string + * assuming UTF-8 encoding. + * + * If max_size is not present, the file is read up its end. + */ + readAsString(max_size?: number): string; } export interface QjsOsLib { diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts @@ -4135,3 +4135,16 @@ export interface ExportDbToFileResponse { */ path: string; } + +export interface ImportDbFromFileRequest { + /** + * Full path to the backup. + */ + path: string; +} + +export const codecForImportDbFromFileRequest = + (): Codec<ImportDbFromFileRequest> => + buildCodecForObject<ImportDbFromFileRequest>() + .property("path", codecForString()) + .build("ImportDbFromFileRequest"); diff --git a/packages/taler-wallet-core/src/host-impl.node.ts b/packages/taler-wallet-core/src/host-impl.node.ts @@ -29,9 +29,7 @@ import { createSqliteBackend, shimIndexedDB, } from "@gnu-taler/idb-bridge"; -import { - createNodeHelperSqlite3Impl, -} from "@gnu-taler/idb-bridge/node-helper-sqlite3-impl"; +import { createNodeHelperSqlite3Impl } from "@gnu-taler/idb-bridge/node-helper-sqlite3-impl"; import { Logger, SetTimeoutTimerAPI, @@ -104,6 +102,9 @@ async function makeFileDb( exportToFile(directory, stem) { throw Error("not supported"); }, + async readBackupJson(path: string): Promise<any> { + throw Error("not supported"); + }, }; } @@ -143,6 +144,9 @@ async function makeSqliteDb( path, }; }, + async readBackupJson(path: string): Promise<any> { + throw Error("not supported"); + }, idbFactory: myBridgeIdbFactory, }; } diff --git a/packages/taler-wallet-core/src/host-impl.qtart.ts b/packages/taler-wallet-core/src/host-impl.qtart.ts @@ -29,7 +29,6 @@ import type { } from "@gnu-taler/idb-bridge"; // eslint-disable-next-line no-duplicate-imports import { - AccessStats, BridgeIDBFactory, createSqliteBackend, MemoryBackend, @@ -54,11 +53,6 @@ import { Wallet, WalletDatabaseImplementation } 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<Sqlite3Interface> { @@ -147,6 +141,20 @@ async function makeSqliteDb( throw Error(`forcing format ${forceFormat} not supported`); } }, + async readBackupJson(path: string): Promise<any> { + const errObj = { errno: undefined }; + const file = qjsStd.open(path, "r", errObj); + if (!path.endsWith(".json")) { + throw Error("DB file import only supports .json files at the moment"); + } + if (!file) { + throw Error(`could not open file (errno=${errObj.errno})`); + } + const dumpStr = file.readAsString(); + file.close(); + const dump = JSON.parse(dumpStr); + return dump; + }, idbFactory: myBridgeIdbFactory, }; } @@ -192,6 +200,9 @@ async function makeFileDb( exportToFile(directory, stem) { throw Error("not supported"); }, + async readBackupJson(path: string): Promise<any> { + throw Error("not supported"); + }, }; } diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -106,6 +106,7 @@ import { GetWithdrawalDetailsForAmountRequest, GetWithdrawalDetailsForUriRequest, HintNetworkAvailabilityRequest, + ImportDbFromFileRequest, ImportDbRequest, InitRequest, InitResponse, @@ -255,6 +256,7 @@ export enum WalletApiOperation { ImportDb = "importDb", ExportDb = "exportDb", ExportDbToFile = "exportDbToFile", + ImportDbFromFile = "importDbFromFile", PreparePeerPushCredit = "preparePeerPushCredit", CheckPeerPushDebit = "checkPeerPushDebit", CheckPeerPushDebitV2 = "checkPeerPushDebitV2", @@ -946,6 +948,17 @@ export type ExportDbToFileOp = { }; /** + * Export the database from a file. + * + * CAUTION: Overrides existing data. + */ +export type ImportDbFromFileOp = { + op: WalletApiOperation.ImportDbFromFile; + request: ImportDbFromFileRequest; + response: EmptyObject; +}; + +/** * Add a new backup provider. */ export type AddBackupProviderOp = { @@ -1440,6 +1453,7 @@ export type WalletOperations = { [WalletApiOperation.RunBackupCycle]: RunBackupCycleOp; [WalletApiOperation.ExportBackup]: ExportBackupOp; [WalletApiOperation.ExportDbToFile]: ExportDbToFileOp; + [WalletApiOperation.ImportDbFromFile]: ImportDbFromFileOp; [WalletApiOperation.AddBackupProvider]: AddBackupProviderOp; [WalletApiOperation.RemoveBackupProvider]: RemoveBackupProviderOp; [WalletApiOperation.GetBackupInfo]: GetBackupInfoOp; diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts @@ -45,6 +45,7 @@ import { AmountJson, AmountString, Amounts, + AsyncCondition, CancellationToken, CanonicalizeBaseUrlRequest, CanonicalizeBaseUrlResponse, @@ -83,6 +84,8 @@ import { GetQrCodesForPaytoRequest, GetQrCodesForPaytoResponse, HintNetworkAvailabilityRequest, + ImportDbFromFileRequest, + ImportDbRequest, InitRequest, InitResponse, IntegrationTestArgs, @@ -185,6 +188,7 @@ import { codecForGetWithdrawalDetailsForAmountRequest, codecForGetWithdrawalDetailsForUri, codecForHintNetworkAvailabilityRequest, + codecForImportDbFromFileRequest, codecForImportDbRequest, codecForInitRequest, codecForInitiatePeerPullPaymentRequest, @@ -1677,6 +1681,32 @@ async function handleExportDbToFile( }; } +async function handleImportDb( + wex: WalletExecutionContext, + req: ImportDbRequest, +): Promise<EmptyObject> { + // FIXME: This should atomically re-materialize transactions! + await importDb(wex.db.idbHandle(), req.dump); + await wex.db.runAllStoresReadWriteTx({}, async (tx) => { + await rematerializeTransactions(wex, tx); + }); + return {}; +} + +async function handleImportDbFromFile( + wex: WalletExecutionContext, + req: ImportDbFromFileRequest, +): Promise<EmptyObject> { + if (req.path.endsWith(".json")) { + const dump = await wex.ws.dbImplementation.readBackupJson(req.path); + return await handleImportDb(wex, { + dump, + }); + } else { + throw Error("DB file import only supports .json files at the moment"); + } +} + async function handleAcceptBankIntegratedWithdrawal( wex: WalletExecutionContext, req: AcceptBankIntegratedWithdrawalRequest, @@ -2241,14 +2271,11 @@ const handlers: { [T in WalletApiOperation]: HandlerWithValidator<T> } = { }, [WalletApiOperation.ImportDb]: { codec: codecForImportDbRequest(), - handler: async (wex, req) => { - // FIXME: This should atomically re-materialize transactions! - await importDb(wex.db.idbHandle(), req.dump); - await wex.db.runAllStoresReadWriteTx({}, async (tx) => { - await rematerializeTransactions(wex, tx); - }); - return {}; - }, + handler: handleImportDb, + }, + [WalletApiOperation.ImportDbFromFile]: { + codec: codecForImportDbFromFileRequest(), + handler: handleImportDbFromFile, }, [WalletApiOperation.CheckPeerPushDebit]: { codec: codecForCheckPeerPushDebitRequest(), @@ -2563,6 +2590,7 @@ export interface WalletDatabaseImplementation { stem: string, forceFormat?: string, ) => Promise<{ path: string }>; + readBackupJson(path: string): Promise<any>; } /** @@ -2782,6 +2810,10 @@ export class InternalWalletState { longpollQueue = new LongpollQueue(); + private loadingDb: boolean = false; + + private loadingDbCond: AsyncCondition = new AsyncCondition(); + public get idbFactory(): BridgeIDBFactory { return this.dbImplementation.idbFactory; } @@ -2877,6 +2909,12 @@ export class InternalWalletState { if (this._indexedDbHandle) { return; } + if (this.loadingDb) { + while (this.loadingDb) { + await this.loadingDbCond.wait(); + } + } + this.loadingDb = true; const myVersionChange = async (): Promise<void> => { logger.info("version change requested for Taler DB"); }; @@ -2890,9 +2928,44 @@ export class InternalWalletState { throw TalerError.fromDetail(TalerErrorCode.WALLET_DB_UNAVAILABLE, { innerError: getErrorDetailFromException(e), }); + } finally { + this.loadingDb = false; + this.loadingDbCond.trigger(); } } + /** + * Prepare database for import by closing it. + */ + async suspendDatabase(): Promise<void> { + if (this.loadingDb) { + while (this.loadingDb) { + await this.loadingDbCond.wait(); + } + } + this.loadingDb = true; + const dbh = this._indexedDbHandle; + if (!dbh) { + return; + } + this._indexedDbHandle = undefined; + return new Promise((resolve, reject) => { + dbh.addEventListener("close", () => { + resolve(); + }); + dbh.close(); + }); + } + + /** + * Resume database by re-opening it. + */ + async resumeDatabase(): Promise<void> { + this.loadingDb = false; + this.loadingDbCond.trigger(); + await this.ensureWalletDbOpen(); + } + notify(n: WalletNotification): void { logger.trace(`Notification: ${j2s(n)}`); for (const l of this.listeners) {