wallet-core,wallet-cli: properly serialize manual DB export

This commit is contained in:
Florian Dold 2023-08-30 15:54:44 +02:00
parent 557213f9c4
commit 88f7338d7c
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
2 changed files with 116 additions and 120 deletions

View File

@ -257,8 +257,7 @@ async function createLocalWallet(
}, },
cryptoWorkerType: walletCliArgs.wallet.cryptoWorker as any, cryptoWorkerType: walletCliArgs.wallet.cryptoWorker as any,
config: { config: {
features: { features: {},
},
testing: { testing: {
devModeActive: checkEnvFlag("TALER_WALLET_DEV_MODE"), devModeActive: checkEnvFlag("TALER_WALLET_DEV_MODE"),
denomselAllowLate: checkEnvFlag( denomselAllowLate: checkEnvFlag(
@ -651,9 +650,12 @@ walletCli
}); });
break; break;
case TalerUriAction.Reward: { case TalerUriAction.Reward: {
const res = await wallet.client.call(WalletApiOperation.PrepareReward, { const res = await wallet.client.call(
talerRewardUri: uri, WalletApiOperation.PrepareReward,
}); {
talerRewardUri: uri,
},
);
console.log("tip status", res); console.log("tip status", res);
await wallet.client.call(WalletApiOperation.AcceptReward, { await wallet.client.call(WalletApiOperation.AcceptReward, {
walletRewardId: res.walletRewardId, walletRewardId: res.walletRewardId,
@ -874,96 +876,13 @@ const backupCli = walletCli.subcommand("backupArgs", "backup", {
help: "Subcommands for backups", help: "Subcommands for backups",
}); });
backupCli backupCli.subcommand("exportDb", "export-db").action(async (args) => {
.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) => {
await withWallet(args, async (wallet) => { await withWallet(args, async (wallet) => {
const backup = await wallet.client.call( const backup = await wallet.client.call(WalletApiOperation.ExportDb, {});
WalletApiOperation.ExportBackupPlain,
{},
);
console.log(JSON.stringify(backup, undefined, 2)); 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", { const depositCli = walletCli.subcommand("depositArgs", "deposit", {
help: "Subcommands for depositing money to payto:// accounts", help: "Subcommands for depositing money to payto:// accounts",
}); });

View File

@ -23,6 +23,7 @@ import {
IDBFactory, IDBFactory,
IDBObjectStore, IDBObjectStore,
IDBTransaction, IDBTransaction,
structuredEncapsulate,
} from "@gnu-taler/idb-bridge"; } from "@gnu-taler/idb-bridge";
import { import {
AgeCommitmentProof, AgeCommitmentProof,
@ -566,10 +567,31 @@ export interface ExchangeDetailsPointer {
updateClock: TalerPreciseTimestamp; 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. * Exchange record as stored in the wallet's database.
*/ */
export interface ExchangeRecord { export interface ExchangeEntryRecord {
/** /**
* Base url of the exchange. * Base url of the exchange.
*/ */
@ -594,13 +616,12 @@ export interface ExchangeRecord {
*/ */
detailsPointer: ExchangeDetailsPointer | undefined; detailsPointer: ExchangeDetailsPointer | undefined;
/** entryStatus: ExchangeEntryDbRecordStatus;
* Is this a permanent or temporary exchange record?
*/ updateStatus: ExchangeEntryDbUpdateStatus;
permanent: boolean;
/** /**
* Last time when the exchange was updated (both /keys and /wire). * Last time when the exchange /keys info was updated.
*/ */
lastUpdate: TalerPreciseTimestamp | undefined; lastUpdate: TalerPreciseTimestamp | undefined;
@ -608,20 +629,21 @@ export interface ExchangeRecord {
* Next scheduled update for the exchange. * Next scheduled update for the exchange.
* *
* (This field must always be present, so we can index on the timestamp.) * (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; lastKeysEtag: string | undefined;
lastWireEtag: string | undefined;
/** /**
* Next time that we should check if coins need to be refreshed. * Next time that we should check if coins need to be refreshed.
* *
* Updated whenever the exchange's denominations are updated or when * Updated whenever the exchange's denominations are updated or when
* the refresh check has been done. * the refresh check has been done.
*/ */
nextRefreshCheck: TalerPreciseTimestamp; nextRefreshCheckStampMs: DbIndexableTimestampMs;
/** /**
* Public key of the reserve that we're currently using for * Public key of the reserve that we're currently using for
@ -2424,7 +2446,7 @@ export const WalletStoresV1 = {
), ),
exchanges: describeStore( exchanges: describeStore(
"exchanges", "exchanges",
describeContents<ExchangeRecord>({ describeContents<ExchangeEntryRecord>({
keyPath: "baseUrl", keyPath: "baseUrl",
}), }),
{}, {},
@ -2713,11 +2735,10 @@ export type WalletDbReadOnlyTransaction<
Stores extends StoreNames<typeof WalletStoresV1> & string, Stores extends StoreNames<typeof WalletStoresV1> & string,
> = DbReadOnlyTransaction<typeof WalletStoresV1, Stores>; > = DbReadOnlyTransaction<typeof WalletStoresV1, Stores>;
export type WalletReadWriteTransaction< export type WalletDbReadWriteTransaction<
Stores extends StoreNames<typeof WalletStoresV1> & string, Stores extends StoreNames<typeof WalletStoresV1> & string,
> = DbReadWriteTransaction<typeof WalletStoresV1, Stores>; > = DbReadWriteTransaction<typeof WalletStoresV1, Stores>;
/** /**
* An applied migration. * An applied migration.
*/ */
@ -2748,32 +2769,88 @@ export const walletMetadataStore = {
), ),
}; };
export function exportDb(db: IDBDatabase): Promise<any> { export interface DbDumpRecord {
const dump = { /**
name: db.name, * Key, serialized with structuredEncapsulated.
stores: {} as { [s: string]: any }, */
version: db.version, 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<DbDump> {
const dbDump: DbDump = {
databases: {},
};
const walletDb: DbDumpDatabase = {
version: db.version,
stores: {},
};
dbDump.databases[db.name] = walletDb;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const tx = db.transaction(Array.from(db.objectStoreNames)); const tx = db.transaction(Array.from(db.objectStoreNames));
tx.addEventListener("complete", () => { tx.addEventListener("complete", () => {
resolve(dump); resolve(dbDump);
}); });
// tslint:disable-next-line:prefer-for-of // tslint:disable-next-line:prefer-for-of
for (let i = 0; i < db.objectStoreNames.length; i++) { for (let i = 0; i < db.objectStoreNames.length; i++) {
const name = db.objectStoreNames[i]; const name = db.objectStoreNames[i];
const storeDump = {} as { [s: string]: any }; const store = tx.objectStore(name);
dump.stores[name] = storeDump; const storeDump: DbStoreDump = {
tx.objectStore(name) autoIncrement: store.autoIncrement,
.openCursor() keyPath: store.keyPath,
.addEventListener("success", (e: Event) => { indexes: {},
const cursor = (e.target as any).result; records: [],
if (cursor) { };
storeDump[cursor.key] = cursor.value; const indexNames = store.indexNames;
cursor.continue(); 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();
}
});
} }
}); });
} }