From 88f7338d7c84ac2a774b483ccff25faf6ceeb879 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 30 Aug 2023 15:54:44 +0200 Subject: [PATCH] wallet-core,wallet-cli: properly serialize manual DB export --- packages/taler-wallet-cli/src/index.ts | 99 ++---------------- packages/taler-wallet-core/src/db.ts | 137 +++++++++++++++++++------ 2 files changed, 116 insertions(+), 120 deletions(-) diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts index 9d840e5bb..d7966a9ca 100644 --- a/packages/taler-wallet-cli/src/index.ts +++ b/packages/taler-wallet-cli/src/index.ts @@ -257,8 +257,7 @@ async function createLocalWallet( }, cryptoWorkerType: walletCliArgs.wallet.cryptoWorker as any, config: { - features: { - }, + features: {}, testing: { devModeActive: checkEnvFlag("TALER_WALLET_DEV_MODE"), denomselAllowLate: checkEnvFlag( @@ -651,9 +650,12 @@ walletCli }); break; case TalerUriAction.Reward: { - const res = await wallet.client.call(WalletApiOperation.PrepareReward, { - talerRewardUri: uri, - }); + const res = await wallet.client.call( + WalletApiOperation.PrepareReward, + { + talerRewardUri: uri, + }, + ); console.log("tip status", res); await wallet.client.call(WalletApiOperation.AcceptReward, { walletRewardId: res.walletRewardId, @@ -874,96 +876,13 @@ const backupCli = walletCli.subcommand("backupArgs", "backup", { help: "Subcommands for backups", }); -backupCli - .subcommand("setDeviceId", "set-device-id") - .requiredArgument("deviceId", clk.STRING, { - help: "new device ID", - }) - .action(async (args) => { - await withWallet(args, async (wallet) => { - await wallet.client.call(WalletApiOperation.SetWalletDeviceId, { - walletDeviceId: args.setDeviceId.deviceId, - }); - }); - }); - -backupCli.subcommand("exportPlain", "export-plain").action(async (args) => { +backupCli.subcommand("exportDb", "export-db").action(async (args) => { await withWallet(args, async (wallet) => { - const backup = await wallet.client.call( - WalletApiOperation.ExportBackupPlain, - {}, - ); + const backup = await wallet.client.call(WalletApiOperation.ExportDb, {}); console.log(JSON.stringify(backup, undefined, 2)); }); }); -backupCli.subcommand("recoverySave", "save-recovery").action(async (args) => { - await withWallet(args, async (wallet) => { - const recoveryJson = await wallet.client.call( - WalletApiOperation.ExportBackupRecovery, - {}, - ); - console.log(JSON.stringify(recoveryJson, undefined, 2)); - }); -}); - -backupCli.subcommand("run", "run").action(async (args) => { - await withWallet(args, async (wallet) => { - await wallet.client.call(WalletApiOperation.RunBackupCycle, {}); - }); -}); - -backupCli.subcommand("status", "status").action(async (args) => { - await withWallet(args, async (wallet) => { - const status = await wallet.client.call( - WalletApiOperation.GetBackupInfo, - {}, - ); - console.log(JSON.stringify(status, undefined, 2)); - }); -}); - -backupCli - .subcommand("recoveryLoad", "load-recovery") - .maybeOption("strategy", ["--strategy"], clk.STRING, { - help: "Strategy for resolving a conflict with the existing wallet key ('theirs' or 'ours')", - }) - .action(async (args) => { - await withWallet(args, async (wallet) => { - const data = JSON.parse(await read(process.stdin)); - let strategy: RecoveryMergeStrategy | undefined; - const stratStr = args.recoveryLoad.strategy; - if (stratStr) { - if (stratStr === "theirs") { - strategy = RecoveryMergeStrategy.Theirs; - } else if (stratStr === "ours") { - strategy = RecoveryMergeStrategy.Theirs; - } else { - throw Error("invalid recovery strategy"); - } - } - await wallet.client.call(WalletApiOperation.ImportBackupRecovery, { - recovery: data, - strategy, - }); - }); - }); - -backupCli - .subcommand("addProvider", "add-provider") - .requiredArgument("url", clk.STRING) - .maybeArgument("name", clk.STRING) - .flag("activate", ["--activate"]) - .action(async (args) => { - await withWallet(args, async (wallet) => { - await wallet.client.call(WalletApiOperation.AddBackupProvider, { - backupProviderBaseUrl: args.addProvider.url, - activate: args.addProvider.activate, - name: args.addProvider.name || args.addProvider.url, - }); - }); - }); - const depositCli = walletCli.subcommand("depositArgs", "deposit", { help: "Subcommands for depositing money to payto:// accounts", }); diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index c550ab675..2dbf5dade 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -23,6 +23,7 @@ import { IDBFactory, IDBObjectStore, IDBTransaction, + structuredEncapsulate, } from "@gnu-taler/idb-bridge"; import { AgeCommitmentProof, @@ -566,10 +567,31 @@ export interface ExchangeDetailsPointer { updateClock: TalerPreciseTimestamp; } +export enum ExchangeEntryDbRecordStatus { + Preset = 1, + Ephemeral = 2, + Used = 3, +} + +export enum ExchangeEntryDbUpdateStatus { + Initial = 1, + InitialUpdate = 2, + Suspended = 3, + Failed = 4, + OutdatedUpdate = 5, + Ready = 6, + ReadyUpdate = 7, +} + +/** + * Timestamp stored as a IEEE 754 double, in milliseconds. + */ +export type DbIndexableTimestampMs = number; + /** * Exchange record as stored in the wallet's database. */ -export interface ExchangeRecord { +export interface ExchangeEntryRecord { /** * Base url of the exchange. */ @@ -594,13 +616,12 @@ export interface ExchangeRecord { */ detailsPointer: ExchangeDetailsPointer | undefined; - /** - * Is this a permanent or temporary exchange record? - */ - permanent: boolean; + entryStatus: ExchangeEntryDbRecordStatus; + + updateStatus: ExchangeEntryDbUpdateStatus; /** - * Last time when the exchange was updated (both /keys and /wire). + * Last time when the exchange /keys info was updated. */ lastUpdate: TalerPreciseTimestamp | undefined; @@ -608,20 +629,21 @@ export interface ExchangeRecord { * Next scheduled update for the exchange. * * (This field must always be present, so we can index on the timestamp.) + * + * FIXME: To index on the timestamp, this needs to be a number of + * binary timestamp! */ - nextUpdate: TalerPreciseTimestamp; + nextUpdateStampMs: DbIndexableTimestampMs; lastKeysEtag: string | undefined; - lastWireEtag: string | undefined; - /** * Next time that we should check if coins need to be refreshed. * * Updated whenever the exchange's denominations are updated or when * the refresh check has been done. */ - nextRefreshCheck: TalerPreciseTimestamp; + nextRefreshCheckStampMs: DbIndexableTimestampMs; /** * Public key of the reserve that we're currently using for @@ -2424,7 +2446,7 @@ export const WalletStoresV1 = { ), exchanges: describeStore( "exchanges", - describeContents({ + describeContents({ keyPath: "baseUrl", }), {}, @@ -2713,11 +2735,10 @@ export type WalletDbReadOnlyTransaction< Stores extends StoreNames & string, > = DbReadOnlyTransaction; -export type WalletReadWriteTransaction< +export type WalletDbReadWriteTransaction< Stores extends StoreNames & string, > = DbReadWriteTransaction; - /** * An applied migration. */ @@ -2748,32 +2769,88 @@ export const walletMetadataStore = { ), }; -export function exportDb(db: IDBDatabase): Promise { - const dump = { - name: db.name, - stores: {} as { [s: string]: any }, - version: db.version, +export interface DbDumpRecord { + /** + * Key, serialized with structuredEncapsulated. + */ + key: any; + /** + * Value, serialized with structuredEncapsulated. + */ + value: any; +} + +export interface DbIndexDump { + keyPath: string | string[]; + multiEntry: boolean; + unique: boolean; +} + +export interface DbStoreDump { + keyPath?: string | string[]; + autoIncrement: boolean; + indexes: { [indexName: string]: DbIndexDump }; + records: DbDumpRecord[]; +} + +export interface DbDumpDatabase { + version: number; + stores: { [storeName: string]: DbStoreDump }; +} + +export interface DbDump { + databases: { + [name: string]: DbDumpDatabase; }; +} + +export function exportDb(db: IDBDatabase): Promise { + const dbDump: DbDump = { + databases: {}, + }; + + const walletDb: DbDumpDatabase = { + version: db.version, + stores: {}, + }; + dbDump.databases[db.name] = walletDb; return new Promise((resolve, reject) => { const tx = db.transaction(Array.from(db.objectStoreNames)); tx.addEventListener("complete", () => { - resolve(dump); + resolve(dbDump); }); // tslint:disable-next-line:prefer-for-of for (let i = 0; i < db.objectStoreNames.length; i++) { const name = db.objectStoreNames[i]; - const storeDump = {} as { [s: string]: any }; - dump.stores[name] = storeDump; - tx.objectStore(name) - .openCursor() - .addEventListener("success", (e: Event) => { - const cursor = (e.target as any).result; - if (cursor) { - storeDump[cursor.key] = cursor.value; - cursor.continue(); - } - }); + const store = tx.objectStore(name); + const storeDump: DbStoreDump = { + autoIncrement: store.autoIncrement, + keyPath: store.keyPath, + indexes: {}, + records: [], + }; + const indexNames = store.indexNames; + for (let j = 0; j < indexNames.length; j++) { + const idxName = indexNames[j]; + const index = store.index(idxName); + storeDump.indexes[idxName] = { + keyPath: index.keyPath, + multiEntry: index.multiEntry, + unique: index.unique, + }; + } + walletDb.stores[name] = storeDump; + store.openCursor().addEventListener("success", (e: Event) => { + const cursor = (e.target as any).result; + if (cursor) { + storeDump.records.push({ + key: structuredEncapsulate(cursor.key), + value: structuredEncapsulate(cursor.value), + }); + cursor.continue(); + } + }); } }); }