diff options
Diffstat (limited to 'packages/taler-wallet-core/src')
17 files changed, 560 insertions, 1882 deletions
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 0fad66d92..b52a503bc 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, @@ -103,7 +104,7 @@ import { RetryInfo, TaskIdentifiers } from "./operations/common.js"; * for all previous versions must be written, which should be * avoided. */ -export const TALER_DB_NAME = "taler-wallet-main-v9"; +export const TALER_WALLET_MAIN_DB_NAME = "taler-wallet-main-v9"; /** * Name of the metadata database. This database is used @@ -111,7 +112,12 @@ export const TALER_DB_NAME = "taler-wallet-main-v9"; * * (Minor migrations are handled via upgrade transactions.) */ -export const TALER_META_DB_NAME = "taler-wallet-meta"; +export const TALER_WALLET_META_DB_NAME = "taler-wallet-meta"; + +/** + * Stored backups, mainly created when manually importing a backup. + */ +export const TALER_WALLET_STORED_BACKUPS_DB_NAME = "taler-wallet-stored-backups"; export const CURRENT_DB_CONFIG_KEY = "currentMainDbName"; @@ -566,10 +572,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 +621,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 +634,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 @@ -2431,7 +2458,7 @@ export const WalletStoresV1 = { ), exchanges: describeStore( "exchanges", - describeContents<ExchangeRecord>({ + describeContents<ExchangeEntryRecord>({ keyPath: "baseUrl", }), {}, @@ -2725,11 +2752,10 @@ export type WalletDbReadOnlyTransaction< Stores extends StoreNames<typeof WalletStoresV1> & string, > = DbReadOnlyTransaction<typeof WalletStoresV1, Stores>; -export type WalletReadWriteTransaction< +export type WalletDbReadWriteTransaction< Stores extends StoreNames<typeof WalletStoresV1> & string, > = DbReadWriteTransaction<typeof WalletStoresV1, Stores>; - /** * An applied migration. */ @@ -2760,45 +2786,144 @@ export const walletMetadataStore = { ), }; -export function exportDb(db: IDBDatabase): Promise<any> { - const dump = { - name: db.name, - stores: {} as { [s: string]: any }, - version: db.version, +export interface StoredBackupMeta { + name: string; +} + +export const StoredBackupStores = { + backupMeta: describeStore( + "backupMeta", + describeContents<StoredBackupMeta>({ keyPath: "name" }), + {}, + ), + backupData: describeStore("backupData", describeContents<any>({}), {}), +}; + +export interface DbDumpRecord { + /** + * Key, serialized with structuredEncapsulated. + * + * Only present for out-of-line keys (i.e. no key path). + */ + 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 async function exportSingleDb( + idb: IDBFactory, + dbName: string, +): Promise<DbDumpDatabase> { + const myDb = await openDatabase( + idb, + dbName, + undefined, + () => { + // May not happen, since we're not requesting a specific version + throw Error("unexpected version change"); + }, + () => { + logger.info("unexpected onupgradeneeded"); + }, + ); + + const singleDbDump: DbDumpDatabase = { + version: myDb.version, + stores: {}, }; return new Promise((resolve, reject) => { - const tx = db.transaction(Array.from(db.objectStoreNames)); + const tx = myDb.transaction(Array.from(myDb.objectStoreNames)); tx.addEventListener("complete", () => { - resolve(dump); + myDb.close(); + resolve(singleDbDump); }); // 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(); + for (let i = 0; i < myDb.objectStoreNames.length; i++) { + const name = myDb.objectStoreNames[i]; + 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, + }; + } + singleDbDump.stores[name] = storeDump; + store.openCursor().addEventListener("success", (e: Event) => { + const cursor = (e.target as any).result; + if (cursor) { + const rec: DbDumpRecord = { + value: structuredEncapsulate(cursor.value), + }; + // Only store key if necessary, i.e. when + // the key is not stored as part of the object via + // a key path. + if (store.keyPath == null) { + rec.key = structuredEncapsulate(cursor.key); } - }); + cursor.continue(); + } + }); } }); } -export interface DatabaseDump { - name: string; - stores: { [s: string]: any }; - version: string; +export async function exportDb(idb: IDBFactory): Promise<DbDump> { + const dbDump: DbDump = { + databases: {}, + }; + + dbDump.databases[TALER_WALLET_META_DB_NAME] = await exportSingleDb( + idb, + TALER_WALLET_META_DB_NAME, + ); + dbDump.databases[TALER_WALLET_MAIN_DB_NAME] = await exportSingleDb( + idb, + TALER_WALLET_MAIN_DB_NAME, + ); + + return dbDump; } async function recoverFromDump( db: IDBDatabase, - dump: DatabaseDump, + dbDump: DbDumpDatabase, ): Promise<void> { return new Promise((resolve, reject) => { const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite"); @@ -2807,67 +2932,33 @@ async function recoverFromDump( }); for (let i = 0; i < db.objectStoreNames.length; i++) { const name = db.objectStoreNames[i]; - const storeDump = dump.stores[name]; + const storeDump = dbDump.stores[name]; if (!storeDump) continue; - Object.keys(storeDump).forEach(async (key) => { - const value = storeDump[key]; - if (!value) return; - tx.objectStore(name).put(value); - }); + for (let rec of storeDump.records) { + tx.objectStore(name).put(rec.value, rec.key); + } } tx.commit(); }); } -export async function importDb(db: IDBDatabase, object: any): Promise<void> { - if ("name" in object && "stores" in object && "version" in object) { - // looks like a database dump - const dump = object as DatabaseDump; - return recoverFromDump(db, dump); - } - - if ("databases" in object && "$types" in object) { - // looks like a IDBDatabase - const someDatabase = object.databases; - - if (TALER_META_DB_NAME in someDatabase) { - //looks like a taler database - const currentMainDbValue = - someDatabase[TALER_META_DB_NAME].objectStores.metaConfig.records[0] - .value.value; - - if (currentMainDbValue !== TALER_DB_NAME) { - console.log("not the current database version"); - } - - const talerDb = someDatabase[currentMainDbValue]; - - const objectStoreNames = Object.keys(talerDb.objectStores); - - const dump: DatabaseDump = { - name: talerDb.schema.databaseName, - version: talerDb.schema.databaseVersion, - stores: {}, - }; - - for (let i = 0; i < objectStoreNames.length; i++) { - const name = objectStoreNames[i]; - const storeDump = {} as { [s: string]: any }; - dump.stores[name] = storeDump; - talerDb.objectStores[name].records.map((r: any) => { - const pkey = r.primaryKey; - const key = - typeof pkey === "string" || typeof pkey === "number" - ? pkey - : pkey.join(","); - storeDump[key] = r.value; - }); - } +function checkDbDump(x: any): x is DbDump { + return "databases" in x; +} - return recoverFromDump(db, dump); +export async function importDb(db: IDBDatabase, dumpJson: any): Promise<void> { + const d = dumpJson; + if (checkDbDump(d)) { + const walletDb = d.databases[TALER_WALLET_MAIN_DB_NAME]; + if (!walletDb) { + throw Error( + `unable to import, main wallet database (${TALER_WALLET_MAIN_DB_NAME}) not found`, + ); } + await recoverFromDump(db, walletDb); + } else { + throw Error("unable to import, doesn't look like a valid DB dump"); } - throw Error("could not import database"); } export interface FixupDescription { @@ -3151,6 +3242,36 @@ function onMetaDbUpgradeNeeded( ); } +function onStoredBackupsDbUpgradeNeeded( + db: IDBDatabase, + oldVersion: number, + newVersion: number, + upgradeTransaction: IDBTransaction, +) { + upgradeFromStoreMap( + StoredBackupStores, + db, + oldVersion, + newVersion, + upgradeTransaction, + ); +} + +export async function openStoredBackupsDatabase( + idbFactory: IDBFactory, +): Promise<DbAccess<typeof StoredBackupStores>> { + const backupsDbHandle = await openDatabase( + idbFactory, + TALER_WALLET_STORED_BACKUPS_DB_NAME, + 1, + () => {}, + onStoredBackupsDbUpgradeNeeded, + ); + + const handle = new DbAccess(backupsDbHandle, StoredBackupStores); + return handle; +} + /** * Return a promise that resolves * to the taler wallet db. @@ -3164,7 +3285,7 @@ export async function openTalerDatabase( ): Promise<DbAccess<typeof WalletStoresV1>> { const metaDbHandle = await openDatabase( idbFactory, - TALER_META_DB_NAME, + TALER_WALLET_META_DB_NAME, 1, () => {}, onMetaDbUpgradeNeeded, @@ -3177,17 +3298,17 @@ export async function openTalerDatabase( .runReadWrite(async (tx) => { const dbVersionRecord = await tx.metaConfig.get(CURRENT_DB_CONFIG_KEY); if (!dbVersionRecord) { - currentMainVersion = TALER_DB_NAME; + currentMainVersion = TALER_WALLET_MAIN_DB_NAME; await tx.metaConfig.put({ key: CURRENT_DB_CONFIG_KEY, - value: TALER_DB_NAME, + value: TALER_WALLET_MAIN_DB_NAME, }); } else { currentMainVersion = dbVersionRecord.value; } }); - if (currentMainVersion !== TALER_DB_NAME) { + if (currentMainVersion !== TALER_WALLET_MAIN_DB_NAME) { switch (currentMainVersion) { case "taler-wallet-main-v2": case "taler-wallet-main-v3": @@ -3203,7 +3324,7 @@ export async function openTalerDatabase( .runReadWrite(async (tx) => { await tx.metaConfig.put({ key: CURRENT_DB_CONFIG_KEY, - value: TALER_DB_NAME, + value: TALER_WALLET_MAIN_DB_NAME, }); }); break; @@ -3216,7 +3337,7 @@ export async function openTalerDatabase( const mainDbHandle = await openDatabase( idbFactory, - TALER_DB_NAME, + TALER_WALLET_MAIN_DB_NAME, WALLET_DB_MINOR_VERSION, onVersionChange, onTalerDbUpgradeNeeded, @@ -3233,7 +3354,7 @@ export async function deleteTalerDatabase( idbFactory: IDBFactory, ): Promise<void> { return new Promise((resolve, reject) => { - const req = idbFactory.deleteDatabase(TALER_DB_NAME); + const req = idbFactory.deleteDatabase(TALER_WALLET_MAIN_DB_NAME); req.onerror = () => reject(req.error); req.onsuccess = () => resolve(); }); diff --git a/packages/taler-wallet-core/src/host-impl.node.ts b/packages/taler-wallet-core/src/host-impl.node.ts index 6a4f21d79..0b6539306 100644 --- a/packages/taler-wallet-core/src/host-impl.node.ts +++ b/packages/taler-wallet-core/src/host-impl.node.ts @@ -139,13 +139,6 @@ export async function createNativeWalletHost2( }); } - const myVersionChange = (): Promise<void> => { - logger.error("version change requested, should not happen"); - throw Error( - "BUG: wallet DB version change event can't happen with memory IDB", - ); - }; - let dbResp: MakeDbResult; if (args.persistentStoragePath &&args.persistentStoragePath.endsWith(".json")) { @@ -160,8 +153,6 @@ export async function createNativeWalletHost2( shimIndexedDB(dbResp.idbFactory); - const myDb = await openTalerDatabase(myIdbFactory, myVersionChange); - let workerFactory; const cryptoWorkerType = args.cryptoWorkerType ?? "node-worker-thread"; if (cryptoWorkerType === "sync") { @@ -189,7 +180,7 @@ export async function createNativeWalletHost2( const timer = new SetTimeoutTimerAPI(); const w = await Wallet.create( - myDb, + myIdbFactory, myHttpLib, timer, workerFactory, diff --git a/packages/taler-wallet-core/src/host-impl.qtart.ts b/packages/taler-wallet-core/src/host-impl.qtart.ts index 720f5affb..81dbe0acd 100644 --- a/packages/taler-wallet-core/src/host-impl.qtart.ts +++ b/packages/taler-wallet-core/src/host-impl.qtart.ts @@ -110,7 +110,7 @@ async function makeSqliteDb( return { ...myBackend.accessStats, primitiveStatements: numStmt, - } + }; }, idbFactory: myBridgeIdbFactory, }; @@ -167,12 +167,15 @@ export async function createNativeWalletHost2( let dbResp: MakeDbResult; - if (args.persistentStoragePath && args.persistentStoragePath.endsWith(".json")) { + if ( + args.persistentStoragePath && + args.persistentStoragePath.endsWith(".json") + ) { logger.info("using JSON file DB backend (slow!)"); dbResp = await makeFileDb(args); } else { logger.info("using sqlite3 DB backend (experimental!)"); - dbResp = await makeSqliteDb(args) + dbResp = await makeSqliteDb(args); } const myIdbFactory: IDBFactory = dbResp.idbFactory as any as IDBFactory; @@ -189,22 +192,13 @@ export async function createNativeWalletHost2( }); } - const myVersionChange = (): Promise<void> => { - logger.error("version change requested, should not happen"); - throw Error( - "BUG: wallet DB version change event can't happen with memory IDB", - ); - }; - - const myDb = await openTalerDatabase(myIdbFactory, myVersionChange); - let workerFactory; workerFactory = new SynchronousCryptoWorkerFactoryPlain(); const timer = new SetTimeoutTimerAPI(); const w = await Wallet.create( - myDb, + myIdbFactory, myHttpLib, timer, workerFactory, diff --git a/packages/taler-wallet-core/src/internal-wallet-state.ts b/packages/taler-wallet-core/src/internal-wallet-state.ts index 742af89a8..a189c9cb3 100644 --- a/packages/taler-wallet-core/src/internal-wallet-state.ts +++ b/packages/taler-wallet-core/src/internal-wallet-state.ts @@ -42,7 +42,7 @@ import { HttpRequestLibrary } from "@gnu-taler/taler-util/http"; import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js"; import { ExchangeDetailsRecord, - ExchangeRecord, + ExchangeEntryRecord, RefreshReasonDetails, WalletStoresV1, } from "./db.js"; @@ -54,6 +54,7 @@ import { } from "./util/query.js"; import { TimerGroup } from "./util/timer.js"; import { WalletConfig } from "./wallet-api-types.js"; +import { IDBFactory } from "@gnu-taler/idb-bridge"; export const EXCHANGE_COINS_LOCK = "exchange-coins-lock"; export const EXCHANGE_RESERVES_LOCK = "exchange-reserves-lock"; @@ -108,7 +109,7 @@ export interface ExchangeOperations { ): Promise<ExchangeDetailsRecord | undefined>; getExchangeTrust( ws: InternalWalletState, - exchangeInfo: ExchangeRecord, + exchangeInfo: ExchangeEntryRecord, ): Promise<TrustInfo>; updateExchangeFromUrl( ws: InternalWalletState, @@ -118,7 +119,7 @@ export interface ExchangeOperations { cancellationToken?: CancellationToken; }, ): Promise<{ - exchange: ExchangeRecord; + exchange: ExchangeEntryRecord; exchangeDetails: ExchangeDetailsRecord; }>; } @@ -203,6 +204,9 @@ export interface InternalWalletState { denomPubHash: string, ): Promise<DenominationInfo | undefined>; + ensureWalletDbOpen(): Promise<void>; + + idb: IDBFactory; db: DbAccess<typeof WalletStoresV1>; http: HttpRequestLibrary; diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts b/packages/taler-wallet-core/src/operations/backup/export.ts deleted file mode 100644 index c9446a05f..000000000 --- a/packages/taler-wallet-core/src/operations/backup/export.ts +++ /dev/null @@ -1,586 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems SA - - 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 <http://www.gnu.org/licenses/> - */ - -/** - * Implementation of wallet backups (export/import/upload) and sync - * server management. - * - * @author Florian Dold <dold@taler.net> - */ - -/** - * Imports. - */ -import { - AbsoluteTime, - Amounts, - BackupBackupProvider, - BackupBackupProviderTerms, - BackupCoin, - BackupCoinSource, - BackupCoinSourceType, - BackupDenomination, - BackupExchange, - BackupExchangeDetails, - BackupExchangeSignKey, - BackupExchangeWireFee, - BackupOperationStatus, - BackupPayInfo, - BackupProposalStatus, - BackupPurchase, - BackupRecoupGroup, - BackupRefreshGroup, - BackupRefreshOldCoin, - BackupRefreshSession, - BackupRefundItem, - BackupRefundState, - BackupTip, - BackupWgInfo, - BackupWgType, - BackupWithdrawalGroup, - BACKUP_VERSION_MAJOR, - BACKUP_VERSION_MINOR, - canonicalizeBaseUrl, - canonicalJson, - CoinStatus, - encodeCrock, - getRandomBytes, - hash, - Logger, - stringToBytes, - WalletBackupContentV1, - TalerPreciseTimestamp, -} from "@gnu-taler/taler-util"; -import { - CoinSourceType, - ConfigRecordKey, - DenominationRecord, - PurchaseStatus, - RefreshCoinStatus, - WithdrawalGroupStatus, - WithdrawalRecordType, -} from "../../db.js"; -import { InternalWalletState } from "../../internal-wallet-state.js"; -import { assertUnreachable } from "../../util/assertUnreachable.js"; -import { checkDbInvariant } from "../../util/invariants.js"; -import { getWalletBackupState, provideBackupState } from "./state.js"; - -const logger = new Logger("backup/export.ts"); - -export async function exportBackup( - ws: InternalWalletState, -): Promise<WalletBackupContentV1> { - await provideBackupState(ws); - return ws.db - .mktx((x) => [ - x.config, - x.exchanges, - x.exchangeDetails, - x.exchangeSignKeys, - x.coins, - x.contractTerms, - x.denominations, - x.purchases, - x.refreshGroups, - x.backupProviders, - x.rewards, - x.recoupGroups, - x.withdrawalGroups, - ]) - .runReadWrite(async (tx) => { - const bs = await getWalletBackupState(ws, tx); - - const backupExchangeDetails: BackupExchangeDetails[] = []; - const backupExchanges: BackupExchange[] = []; - const backupCoinsByDenom: { [dph: string]: BackupCoin[] } = {}; - const backupDenominationsByExchange: { - [url: string]: BackupDenomination[]; - } = {}; - const backupPurchases: BackupPurchase[] = []; - const backupRefreshGroups: BackupRefreshGroup[] = []; - const backupBackupProviders: BackupBackupProvider[] = []; - const backupTips: BackupTip[] = []; - const backupRecoupGroups: BackupRecoupGroup[] = []; - const backupWithdrawalGroups: BackupWithdrawalGroup[] = []; - - await tx.withdrawalGroups.iter().forEachAsync(async (wg) => { - let info: BackupWgInfo; - switch (wg.wgInfo.withdrawalType) { - case WithdrawalRecordType.BankIntegrated: - info = { - type: BackupWgType.BankIntegrated, - exchange_payto_uri: wg.wgInfo.bankInfo.exchangePaytoUri, - taler_withdraw_uri: wg.wgInfo.bankInfo.talerWithdrawUri, - confirm_url: wg.wgInfo.bankInfo.confirmUrl, - timestamp_bank_confirmed: - wg.wgInfo.bankInfo.timestampBankConfirmed, - timestamp_reserve_info_posted: - wg.wgInfo.bankInfo.timestampReserveInfoPosted, - }; - break; - case WithdrawalRecordType.BankManual: - info = { - type: BackupWgType.BankManual, - }; - break; - case WithdrawalRecordType.PeerPullCredit: - info = { - type: BackupWgType.PeerPullCredit, - contract_priv: wg.wgInfo.contractPriv, - contract_terms: wg.wgInfo.contractTerms, - }; - break; - case WithdrawalRecordType.PeerPushCredit: - info = { - type: BackupWgType.PeerPushCredit, - contract_terms: wg.wgInfo.contractTerms, - }; - break; - case WithdrawalRecordType.Recoup: - info = { - type: BackupWgType.Recoup, - }; - break; - default: - assertUnreachable(wg.wgInfo); - } - backupWithdrawalGroups.push({ - raw_withdrawal_amount: Amounts.stringify(wg.rawWithdrawalAmount), - info, - timestamp_created: wg.timestampStart, - timestamp_finish: wg.timestampFinish, - withdrawal_group_id: wg.withdrawalGroupId, - secret_seed: wg.secretSeed, - exchange_base_url: wg.exchangeBaseUrl, - instructed_amount: Amounts.stringify(wg.instructedAmount), - effective_withdrawal_amount: Amounts.stringify( - wg.effectiveWithdrawalAmount, - ), - reserve_priv: wg.reservePriv, - restrict_age: wg.restrictAge, - // FIXME: proper status conversion! - operation_status: - wg.status == WithdrawalGroupStatus.Finished - ? BackupOperationStatus.Finished - : BackupOperationStatus.Pending, - selected_denoms_uid: wg.denomSelUid, - selected_denoms: wg.denomsSel.selectedDenoms.map((x) => ({ - count: x.count, - denom_pub_hash: x.denomPubHash, - })), - }); - }); - - await tx.rewards.iter().forEach((tip) => { - backupTips.push({ - exchange_base_url: tip.exchangeBaseUrl, - merchant_base_url: tip.merchantBaseUrl, - merchant_tip_id: tip.merchantRewardId, - wallet_tip_id: tip.walletRewardId, - next_url: tip.next_url, - secret_seed: tip.secretSeed, - selected_denoms: tip.denomsSel.selectedDenoms.map((x) => ({ - count: x.count, - denom_pub_hash: x.denomPubHash, - })), - timestamp_finished: tip.pickedUpTimestamp, - timestamp_accepted: tip.acceptedTimestamp, - timestamp_created: tip.createdTimestamp, - timestamp_expiration: tip.rewardExpiration, - tip_amount_raw: Amounts.stringify(tip.rewardAmountRaw), - selected_denoms_uid: tip.denomSelUid, - }); - }); - - await tx.recoupGroups.iter().forEach((recoupGroup) => { - backupRecoupGroups.push({ - recoup_group_id: recoupGroup.recoupGroupId, - timestamp_created: recoupGroup.timestampStarted, - timestamp_finish: recoupGroup.timestampFinished, - coins: recoupGroup.coinPubs.map((x, i) => ({ - coin_pub: x, - recoup_finished: recoupGroup.recoupFinishedPerCoin[i], - })), - }); - }); - - await tx.backupProviders.iter().forEach((bp) => { - let terms: BackupBackupProviderTerms | undefined; - if (bp.terms) { - terms = { - annual_fee: Amounts.stringify(bp.terms.annualFee), - storage_limit_in_megabytes: bp.terms.storageLimitInMegabytes, - supported_protocol_version: bp.terms.supportedProtocolVersion, - }; - } - backupBackupProviders.push({ - terms, - base_url: canonicalizeBaseUrl(bp.baseUrl), - pay_proposal_ids: bp.paymentProposalIds, - uids: bp.uids, - }); - }); - - await tx.coins.iter().forEach((coin) => { - let bcs: BackupCoinSource; - switch (coin.coinSource.type) { - case CoinSourceType.Refresh: - bcs = { - type: BackupCoinSourceType.Refresh, - old_coin_pub: coin.coinSource.oldCoinPub, - refresh_group_id: coin.coinSource.refreshGroupId, - }; - break; - case CoinSourceType.Reward: - bcs = { - type: BackupCoinSourceType.Reward, - coin_index: coin.coinSource.coinIndex, - wallet_tip_id: coin.coinSource.walletRewardId, - }; - break; - case CoinSourceType.Withdraw: - bcs = { - type: BackupCoinSourceType.Withdraw, - coin_index: coin.coinSource.coinIndex, - reserve_pub: coin.coinSource.reservePub, - withdrawal_group_id: coin.coinSource.withdrawalGroupId, - }; - break; - } - - const coins = (backupCoinsByDenom[coin.denomPubHash] ??= []); - coins.push({ - blinding_key: coin.blindingKey, - coin_priv: coin.coinPriv, - coin_source: bcs, - fresh: coin.status === CoinStatus.Fresh, - spend_allocation: coin.spendAllocation - ? { - amount: coin.spendAllocation.amount, - id: coin.spendAllocation.id, - } - : undefined, - denom_sig: coin.denomSig, - }); - }); - - await tx.denominations.iter().forEach((denom) => { - const backupDenoms = (backupDenominationsByExchange[ - denom.exchangeBaseUrl - ] ??= []); - backupDenoms.push({ - coins: backupCoinsByDenom[denom.denomPubHash] ?? [], - denom_pub: denom.denomPub, - fee_deposit: Amounts.stringify(denom.fees.feeDeposit), - fee_refresh: Amounts.stringify(denom.fees.feeRefresh), - fee_refund: Amounts.stringify(denom.fees.feeRefund), - fee_withdraw: Amounts.stringify(denom.fees.feeWithdraw), - is_offered: denom.isOffered, - is_revoked: denom.isRevoked, - master_sig: denom.masterSig, - stamp_expire_deposit: denom.stampExpireDeposit, - stamp_expire_legal: denom.stampExpireLegal, - stamp_expire_withdraw: denom.stampExpireWithdraw, - stamp_start: denom.stampStart, - value: Amounts.stringify(DenominationRecord.getValue(denom)), - list_issue_date: denom.listIssueDate, - }); - }); - - await tx.exchanges.iter().forEachAsync(async (ex) => { - const dp = ex.detailsPointer; - if (!dp) { - return; - } - backupExchanges.push({ - base_url: ex.baseUrl, - currency: dp.currency, - master_public_key: dp.masterPublicKey, - update_clock: dp.updateClock, - }); - }); - - await tx.exchangeDetails.iter().forEachAsync(async (ex) => { - // Only back up permanently added exchanges. - - const wi = ex.wireInfo; - const wireFees: BackupExchangeWireFee[] = []; - - Object.keys(wi.feesForType).forEach((x) => { - for (const f of wi.feesForType[x]) { - wireFees.push({ - wire_type: x, - closing_fee: Amounts.stringify(f.closingFee), - end_stamp: f.endStamp, - sig: f.sig, - start_stamp: f.startStamp, - wire_fee: Amounts.stringify(f.wireFee), - }); - } - }); - checkDbInvariant(ex.rowId != null); - const exchangeSk = - await tx.exchangeSignKeys.indexes.byExchangeDetailsRowId.getAll( - ex.rowId, - ); - let signingKeys: BackupExchangeSignKey[] = exchangeSk.map((x) => ({ - key: x.signkeyPub, - master_sig: x.masterSig, - stamp_end: x.stampEnd, - stamp_expire: x.stampExpire, - stamp_start: x.stampStart, - })); - - backupExchangeDetails.push({ - base_url: ex.exchangeBaseUrl, - reserve_closing_delay: ex.reserveClosingDelay, - accounts: ex.wireInfo.accounts.map((x) => ({ - payto_uri: x.payto_uri, - master_sig: x.master_sig, - })), - auditors: ex.auditors.map((x) => ({ - auditor_pub: x.auditor_pub, - auditor_url: x.auditor_url, - denomination_keys: x.denomination_keys, - })), - master_public_key: ex.masterPublicKey, - currency: ex.currency, - protocol_version: ex.protocolVersionRange, - wire_fees: wireFees, - signing_keys: signingKeys, - global_fees: ex.globalFees.map((x) => ({ - accountFee: Amounts.stringify(x.accountFee), - historyFee: Amounts.stringify(x.historyFee), - purseFee: Amounts.stringify(x.purseFee), - endDate: x.endDate, - historyTimeout: x.historyTimeout, - signature: x.signature, - purseLimit: x.purseLimit, - purseTimeout: x.purseTimeout, - startDate: x.startDate, - })), - tos_accepted_etag: ex.tosAccepted?.etag, - tos_accepted_timestamp: ex.tosAccepted?.timestamp, - denominations: - backupDenominationsByExchange[ex.exchangeBaseUrl] ?? [], - }); - }); - - const purchaseProposalIdSet = new Set<string>(); - - await tx.purchases.iter().forEachAsync(async (purch) => { - const refunds: BackupRefundItem[] = []; - purchaseProposalIdSet.add(purch.proposalId); - // for (const refundKey of Object.keys(purch.refunds)) { - // const ri = purch.refunds[refundKey]; - // const common = { - // coin_pub: ri.coinPub, - // execution_time: ri.executionTime, - // obtained_time: ri.obtainedTime, - // refund_amount: Amounts.stringify(ri.refundAmount), - // rtransaction_id: ri.rtransactionId, - // total_refresh_cost_bound: Amounts.stringify( - // ri.totalRefreshCostBound, - // ), - // }; - // switch (ri.type) { - // case RefundState.Applied: - // refunds.push({ type: BackupRefundState.Applied, ...common }); - // break; - // case RefundState.Failed: - // refunds.push({ type: BackupRefundState.Failed, ...common }); - // break; - // case RefundState.Pending: - // refunds.push({ type: BackupRefundState.Pending, ...common }); - // break; - // } - // } - - let propStatus: BackupProposalStatus; - switch (purch.purchaseStatus) { - case PurchaseStatus.Done: - case PurchaseStatus.PendingQueryingAutoRefund: - case PurchaseStatus.PendingQueryingRefund: - propStatus = BackupProposalStatus.Paid; - break; - case PurchaseStatus.PendingPayingReplay: - case PurchaseStatus.PendingDownloadingProposal: - case PurchaseStatus.DialogProposed: - case PurchaseStatus.PendingPaying: - propStatus = BackupProposalStatus.Proposed; - break; - case PurchaseStatus.DialogShared: - propStatus = BackupProposalStatus.Shared; - break; - case PurchaseStatus.FailedClaim: - case PurchaseStatus.AbortedIncompletePayment: - propStatus = BackupProposalStatus.PermanentlyFailed; - break; - case PurchaseStatus.AbortingWithRefund: - case PurchaseStatus.AbortedProposalRefused: - propStatus = BackupProposalStatus.Refused; - break; - case PurchaseStatus.RepurchaseDetected: - propStatus = BackupProposalStatus.Repurchase; - break; - default: { - const error = purch.purchaseStatus; - throw Error(`purchase status ${error} is not handled`); - } - } - - const payInfo = purch.payInfo; - let backupPayInfo: BackupPayInfo | undefined = undefined; - if (payInfo) { - backupPayInfo = { - pay_coins: payInfo.payCoinSelection.coinPubs.map((x, i) => ({ - coin_pub: x, - contribution: Amounts.stringify( - payInfo.payCoinSelection.coinContributions[i], - ), - })), - total_pay_cost: Amounts.stringify(payInfo.totalPayCost), - pay_coins_uid: payInfo.payCoinSelectionUid, - }; - } - - let contractTermsRaw = undefined; - if (purch.download) { - const contractTermsRecord = await tx.contractTerms.get( - purch.download.contractTermsHash, - ); - if (contractTermsRecord) { - contractTermsRaw = contractTermsRecord.contractTermsRaw; - } - } - - backupPurchases.push({ - contract_terms_raw: contractTermsRaw, - auto_refund_deadline: purch.autoRefundDeadline, - merchant_pay_sig: purch.merchantPaySig, - pos_confirmation: purch.posConfirmation, - pay_info: backupPayInfo, - proposal_id: purch.proposalId, - refunds, - timestamp_accepted: purch.timestampAccept, - timestamp_first_successful_pay: purch.timestampFirstSuccessfulPay, - nonce_priv: purch.noncePriv, - merchant_sig: purch.download?.contractTermsMerchantSig, - claim_token: purch.claimToken, - merchant_base_url: purch.merchantBaseUrl, - order_id: purch.orderId, - proposal_status: propStatus, - repurchase_proposal_id: purch.repurchaseProposalId, - download_session_id: purch.downloadSessionId, - timestamp_proposed: purch.timestamp, - shared: purch.shared, - }); - }); - - await tx.refreshGroups.iter().forEach((rg) => { - const oldCoins: BackupRefreshOldCoin[] = []; - - for (let i = 0; i < rg.oldCoinPubs.length; i++) { - let refreshSession: BackupRefreshSession | undefined; - const s = rg.refreshSessionPerCoin[i]; - if (s) { - refreshSession = { - new_denoms: s.newDenoms.map((x) => ({ - count: x.count, - denom_pub_hash: x.denomPubHash, - })), - session_secret_seed: s.sessionSecretSeed, - noreveal_index: s.norevealIndex, - }; - } - oldCoins.push({ - coin_pub: rg.oldCoinPubs[i], - estimated_output_amount: Amounts.stringify( - rg.estimatedOutputPerCoin[i], - ), - finished: rg.statusPerCoin[i] === RefreshCoinStatus.Finished, - input_amount: Amounts.stringify(rg.inputPerCoin[i]), - refresh_session: refreshSession, - }); - } - - backupRefreshGroups.push({ - reason: rg.reason as any, - refresh_group_id: rg.refreshGroupId, - timestamp_created: rg.timestampCreated, - timestamp_finish: rg.timestampFinished, - old_coins: oldCoins, - }); - }); - - const ts = TalerPreciseTimestamp.now(); - - if (!bs.lastBackupTimestamp) { - bs.lastBackupTimestamp = ts; - } - - const backupBlob: WalletBackupContentV1 = { - schema_id: "gnu-taler-wallet-backup-content", - schema_version: BACKUP_VERSION_MAJOR, - minor_version: BACKUP_VERSION_MINOR, - exchanges: backupExchanges, - exchange_details: backupExchangeDetails, - wallet_root_pub: bs.walletRootPub, - backup_providers: backupBackupProviders, - current_device_id: bs.deviceId, - purchases: backupPurchases, - recoup_groups: backupRecoupGroups, - refresh_groups: backupRefreshGroups, - tips: backupTips, - timestamp: bs.lastBackupTimestamp, - trusted_auditors: {}, - trusted_exchanges: {}, - intern_table: {}, - error_reports: [], - tombstones: [], - // FIXME! - withdrawal_groups: backupWithdrawalGroups, - }; - - // If the backup changed, we change our nonce and timestamp. - - let h = encodeCrock(hash(stringToBytes(canonicalJson(backupBlob)))); - if (h !== bs.lastBackupPlainHash) { - logger.trace( - `plain backup hash changed (from ${bs.lastBackupPlainHash}to ${h})`, - ); - bs.lastBackupTimestamp = ts; - backupBlob.timestamp = ts; - bs.lastBackupPlainHash = encodeCrock( - hash(stringToBytes(canonicalJson(backupBlob))), - ); - bs.lastBackupNonce = encodeCrock(getRandomBytes(32)); - logger.trace( - `setting timestamp to ${AbsoluteTime.toIsoString( - AbsoluteTime.fromPreciseTimestamp(ts), - )} and nonce to ${bs.lastBackupNonce}`, - ); - await tx.config.put({ - key: ConfigRecordKey.WalletBackupState, - value: bs, - }); - } else { - logger.trace("backup hash did not change"); - } - - return backupBlob; - }); -} diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts deleted file mode 100644 index a53b624e8..000000000 --- a/packages/taler-wallet-core/src/operations/backup/import.ts +++ /dev/null @@ -1,875 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems SA - - 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 <http://www.gnu.org/licenses/> - */ - -import { - AgeRestriction, - AmountJson, - Amounts, - BackupCoin, - BackupCoinSourceType, - BackupDenomSel, - BackupPayInfo, - BackupProposalStatus, - BackupRefreshReason, - BackupRefundState, - BackupWgType, - codecForMerchantContractTerms, - CoinStatus, - DenomKeyType, - DenomSelectionState, - j2s, - Logger, - PayCoinSelection, - RefreshReason, - TalerProtocolTimestamp, - TalerPreciseTimestamp, - WalletBackupContentV1, - WireInfo, -} from "@gnu-taler/taler-util"; -import { - CoinRecord, - CoinSource, - CoinSourceType, - DenominationRecord, - DenominationVerificationStatus, - ProposalDownloadInfo, - PurchaseStatus, - PurchasePayInfo, - RefreshCoinStatus, - RefreshSessionRecord, - WalletContractData, - WalletStoresV1, - WgInfo, - WithdrawalGroupStatus, - WithdrawalRecordType, - RefreshOperationStatus, - RewardRecordStatus, -} from "../../db.js"; -import { InternalWalletState } from "../../internal-wallet-state.js"; -import { assertUnreachable } from "../../util/assertUnreachable.js"; -import { checkLogicInvariant } from "../../util/invariants.js"; -import { GetReadOnlyAccess, GetReadWriteAccess } from "../../util/query.js"; -import { - constructTombstone, - makeCoinAvailable, - TombstoneTag, -} from "../common.js"; -import { getExchangeDetails } from "../exchanges.js"; -import { extractContractData } from "../pay-merchant.js"; -import { provideBackupState } from "./state.js"; - -const logger = new Logger("operations/backup/import.ts"); - -function checkBackupInvariant(b: boolean, m?: string): asserts b { - if (!b) { - if (m) { - throw Error(`BUG: backup invariant failed (${m})`); - } else { - throw Error("BUG: backup invariant failed"); - } - } -} - -/** - * Re-compute information about the coin selection for a payment. - */ -async function recoverPayCoinSelection( - tx: GetReadWriteAccess<{ - exchanges: typeof WalletStoresV1.exchanges; - exchangeDetails: typeof WalletStoresV1.exchangeDetails; - coins: typeof WalletStoresV1.coins; - denominations: typeof WalletStoresV1.denominations; - }>, - contractData: WalletContractData, - payInfo: BackupPayInfo, -): Promise<PayCoinSelection> { - const coinPubs: string[] = payInfo.pay_coins.map((x) => x.coin_pub); - const coinContributions: AmountJson[] = payInfo.pay_coins.map((x) => - Amounts.parseOrThrow(x.contribution), - ); - - const coveredExchanges: Set<string> = new Set(); - - let totalWireFee: AmountJson = Amounts.zeroOfAmount(contractData.amount); - let totalDepositFees: AmountJson = Amounts.zeroOfAmount(contractData.amount); - - for (const coinPub of coinPubs) { - const coinRecord = await tx.coins.get(coinPub); - checkBackupInvariant(!!coinRecord); - const denom = await tx.denominations.get([ - coinRecord.exchangeBaseUrl, - coinRecord.denomPubHash, - ]); - checkBackupInvariant(!!denom); - totalDepositFees = Amounts.add( - totalDepositFees, - denom.fees.feeDeposit, - ).amount; - - if (!coveredExchanges.has(coinRecord.exchangeBaseUrl)) { - const exchangeDetails = await getExchangeDetails( - tx, - coinRecord.exchangeBaseUrl, - ); - checkBackupInvariant(!!exchangeDetails); - let wireFee: AmountJson | undefined; - const feesForType = exchangeDetails.wireInfo.feesForType; - checkBackupInvariant(!!feesForType); - for (const fee of feesForType[contractData.wireMethod] || []) { - if ( - fee.startStamp <= contractData.timestamp && - fee.endStamp >= contractData.timestamp - ) { - wireFee = Amounts.parseOrThrow(fee.wireFee); - break; - } - } - if (wireFee) { - totalWireFee = Amounts.add(totalWireFee, wireFee).amount; - } - coveredExchanges.add(coinRecord.exchangeBaseUrl); - } - } - - let customerWireFee: AmountJson; - - const amortizedWireFee = Amounts.divide( - totalWireFee, - contractData.wireFeeAmortization, - ); - if (Amounts.cmp(contractData.maxWireFee, amortizedWireFee) < 0) { - customerWireFee = amortizedWireFee; - } else { - customerWireFee = Amounts.zeroOfAmount(contractData.amount); - } - - const customerDepositFees = Amounts.sub( - totalDepositFees, - contractData.maxDepositFee, - ).amount; - - return { - coinPubs, - coinContributions: coinContributions.map((x) => Amounts.stringify(x)), - paymentAmount: Amounts.stringify(contractData.amount), - customerWireFees: Amounts.stringify(customerWireFee), - customerDepositFees: Amounts.stringify(customerDepositFees), - }; -} - -async function getDenomSelStateFromBackup( - tx: GetReadOnlyAccess<{ denominations: typeof WalletStoresV1.denominations }>, - currency: string, - exchangeBaseUrl: string, - sel: BackupDenomSel, -): Promise<DenomSelectionState> { - const selectedDenoms: { - denomPubHash: string; - count: number; - }[] = []; - let totalCoinValue = Amounts.zeroOfCurrency(currency); - let totalWithdrawCost = Amounts.zeroOfCurrency(currency); - for (const s of sel) { - const d = await tx.denominations.get([exchangeBaseUrl, s.denom_pub_hash]); - checkBackupInvariant(!!d); - totalCoinValue = Amounts.add( - totalCoinValue, - DenominationRecord.getValue(d), - ).amount; - totalWithdrawCost = Amounts.add( - totalWithdrawCost, - DenominationRecord.getValue(d), - d.fees.feeWithdraw, - ).amount; - } - return { - selectedDenoms, - totalCoinValue: Amounts.stringify(totalCoinValue), - totalWithdrawCost: Amounts.stringify(totalWithdrawCost), - }; -} - -export interface CompletedCoin { - coinPub: string; - coinEvHash: string; -} - -/** - * Precomputed cryptographic material for a backup import. - * - * We separate this data from the backup blob as we want the backup - * blob to be small, and we can't compute it during the database transaction, - * as the async crypto worker communication would auto-close the database transaction. - */ -export interface BackupCryptoPrecomputedData { - rsaDenomPubToHash: Record<string, string>; - coinPrivToCompletedCoin: Record<string, CompletedCoin>; - proposalNoncePrivToPub: { [priv: string]: string }; - proposalIdToContractTermsHash: { [proposalId: string]: string }; - reservePrivToPub: Record<string, string>; -} - -export async function importCoin( - ws: InternalWalletState, - tx: GetReadWriteAccess<{ - coins: typeof WalletStoresV1.coins; - coinAvailability: typeof WalletStoresV1.coinAvailability; - denominations: typeof WalletStoresV1.denominations; - }>, - cryptoComp: BackupCryptoPrecomputedData, - args: { - backupCoin: BackupCoin; - exchangeBaseUrl: string; - denomPubHash: string; - }, -): Promise<void> { - const { backupCoin, exchangeBaseUrl, denomPubHash } = args; - const compCoin = cryptoComp.coinPrivToCompletedCoin[backupCoin.coin_priv]; - checkLogicInvariant(!!compCoin); - const existingCoin = await tx.coins.get(compCoin.coinPub); - if (!existingCoin) { - let coinSource: CoinSource; - switch (backupCoin.coin_source.type) { - case BackupCoinSourceType.Refresh: - coinSource = { - type: CoinSourceType.Refresh, - oldCoinPub: backupCoin.coin_source.old_coin_pub, - refreshGroupId: backupCoin.coin_source.refresh_group_id, - }; - break; - case BackupCoinSourceType.Reward: - coinSource = { - type: CoinSourceType.Reward, - coinIndex: backupCoin.coin_source.coin_index, - walletRewardId: backupCoin.coin_source.wallet_tip_id, - }; - break; - case BackupCoinSourceType.Withdraw: - coinSource = { - type: CoinSourceType.Withdraw, - coinIndex: backupCoin.coin_source.coin_index, - reservePub: backupCoin.coin_source.reserve_pub, - withdrawalGroupId: backupCoin.coin_source.withdrawal_group_id, - }; - break; - } - const coinRecord: CoinRecord = { - blindingKey: backupCoin.blinding_key, - coinEvHash: compCoin.coinEvHash, - coinPriv: backupCoin.coin_priv, - denomSig: backupCoin.denom_sig, - coinPub: compCoin.coinPub, - exchangeBaseUrl, - denomPubHash, - status: backupCoin.fresh ? CoinStatus.Fresh : CoinStatus.Dormant, - coinSource, - // FIXME! - maxAge: AgeRestriction.AGE_UNRESTRICTED, - // FIXME! - ageCommitmentProof: undefined, - // FIXME! - spendAllocation: undefined, - }; - if (coinRecord.status === CoinStatus.Fresh) { - await makeCoinAvailable(ws, tx, coinRecord); - } else { - await tx.coins.put(coinRecord); - } - } -} - -export async function importBackup( - ws: InternalWalletState, - backupBlobArg: any, - cryptoComp: BackupCryptoPrecomputedData, -): Promise<void> { - await provideBackupState(ws); - - logger.info(`importing backup ${j2s(backupBlobArg)}`); - - return ws.db - .mktx((x) => [ - x.config, - x.exchangeDetails, - x.exchanges, - x.coins, - x.coinAvailability, - x.denominations, - x.purchases, - x.refreshGroups, - x.backupProviders, - x.rewards, - x.recoupGroups, - x.withdrawalGroups, - x.tombstones, - x.depositGroups, - ]) - .runReadWrite(async (tx) => { - // FIXME: validate schema! - const backupBlob = backupBlobArg as WalletBackupContentV1; - - // FIXME: validate version - - for (const tombstone of backupBlob.tombstones) { - await tx.tombstones.put({ - id: tombstone, - }); - } - - const tombstoneSet = new Set( - (await tx.tombstones.iter().toArray()).map((x) => x.id), - ); - - // FIXME: Validate that the "details pointer" is correct - - for (const backupExchange of backupBlob.exchanges) { - const existingExchange = await tx.exchanges.get( - backupExchange.base_url, - ); - if (existingExchange) { - continue; - } - await tx.exchanges.put({ - baseUrl: backupExchange.base_url, - detailsPointer: { - currency: backupExchange.currency, - masterPublicKey: backupExchange.master_public_key, - updateClock: backupExchange.update_clock, - }, - permanent: true, - lastUpdate: undefined, - nextUpdate: TalerPreciseTimestamp.now(), - nextRefreshCheck: TalerPreciseTimestamp.now(), - lastKeysEtag: undefined, - lastWireEtag: undefined, - }); - } - - for (const backupExchangeDetails of backupBlob.exchange_details) { - const existingExchangeDetails = - await tx.exchangeDetails.indexes.byPointer.get([ - backupExchangeDetails.base_url, - backupExchangeDetails.currency, - backupExchangeDetails.master_public_key, - ]); - - if (!existingExchangeDetails) { - const wireInfo: WireInfo = { - accounts: backupExchangeDetails.accounts.map((x) => ({ - master_sig: x.master_sig, - payto_uri: x.payto_uri, - })), - feesForType: {}, - }; - for (const fee of backupExchangeDetails.wire_fees) { - const w = (wireInfo.feesForType[fee.wire_type] ??= []); - w.push({ - closingFee: Amounts.stringify(fee.closing_fee), - endStamp: fee.end_stamp, - sig: fee.sig, - startStamp: fee.start_stamp, - wireFee: Amounts.stringify(fee.wire_fee), - }); - } - let tosAccepted = undefined; - if ( - backupExchangeDetails.tos_accepted_etag && - backupExchangeDetails.tos_accepted_timestamp - ) { - tosAccepted = { - etag: backupExchangeDetails.tos_accepted_etag, - timestamp: backupExchangeDetails.tos_accepted_timestamp, - }; - } - await tx.exchangeDetails.put({ - exchangeBaseUrl: backupExchangeDetails.base_url, - wireInfo, - currency: backupExchangeDetails.currency, - auditors: backupExchangeDetails.auditors.map((x) => ({ - auditor_pub: x.auditor_pub, - auditor_url: x.auditor_url, - denomination_keys: x.denomination_keys, - })), - masterPublicKey: backupExchangeDetails.master_public_key, - protocolVersionRange: backupExchangeDetails.protocol_version, - reserveClosingDelay: backupExchangeDetails.reserve_closing_delay, - tosCurrentEtag: backupExchangeDetails.tos_accepted_etag || "", - tosAccepted, - globalFees: backupExchangeDetails.global_fees.map((x) => ({ - accountFee: Amounts.stringify(x.accountFee), - historyFee: Amounts.stringify(x.historyFee), - purseFee: Amounts.stringify(x.purseFee), - endDate: x.endDate, - historyTimeout: x.historyTimeout, - signature: x.signature, - purseLimit: x.purseLimit, - purseTimeout: x.purseTimeout, - startDate: x.startDate, - })), - }); - } - - for (const backupDenomination of backupExchangeDetails.denominations) { - if (backupDenomination.denom_pub.cipher !== DenomKeyType.Rsa) { - throw Error("unsupported cipher"); - } - const denomPubHash = - cryptoComp.rsaDenomPubToHash[ - backupDenomination.denom_pub.rsa_public_key - ]; - checkLogicInvariant(!!denomPubHash); - const existingDenom = await tx.denominations.get([ - backupExchangeDetails.base_url, - denomPubHash, - ]); - if (!existingDenom) { - const value = Amounts.parseOrThrow(backupDenomination.value); - - await tx.denominations.put({ - denomPub: backupDenomination.denom_pub, - denomPubHash: denomPubHash, - exchangeBaseUrl: backupExchangeDetails.base_url, - exchangeMasterPub: backupExchangeDetails.master_public_key, - fees: { - feeDeposit: Amounts.stringify(backupDenomination.fee_deposit), - feeRefresh: Amounts.stringify(backupDenomination.fee_refresh), - feeRefund: Amounts.stringify(backupDenomination.fee_refund), - feeWithdraw: Amounts.stringify(backupDenomination.fee_withdraw), - }, - isOffered: backupDenomination.is_offered, - isRevoked: backupDenomination.is_revoked, - masterSig: backupDenomination.master_sig, - stampExpireDeposit: backupDenomination.stamp_expire_deposit, - stampExpireLegal: backupDenomination.stamp_expire_legal, - stampExpireWithdraw: backupDenomination.stamp_expire_withdraw, - stampStart: backupDenomination.stamp_start, - verificationStatus: DenominationVerificationStatus.VerifiedGood, - currency: value.currency, - amountFrac: value.fraction, - amountVal: value.value, - listIssueDate: backupDenomination.list_issue_date, - }); - } - for (const backupCoin of backupDenomination.coins) { - await importCoin(ws, tx, cryptoComp, { - backupCoin, - denomPubHash, - exchangeBaseUrl: backupExchangeDetails.base_url, - }); - } - } - } - - for (const backupWg of backupBlob.withdrawal_groups) { - const reservePub = cryptoComp.reservePrivToPub[backupWg.reserve_priv]; - checkLogicInvariant(!!reservePub); - const ts = constructTombstone({ - tag: TombstoneTag.DeleteReserve, - reservePub, - }); - if (tombstoneSet.has(ts)) { - continue; - } - const existingWg = await tx.withdrawalGroups.get( - backupWg.withdrawal_group_id, - ); - if (existingWg) { - continue; - } - let wgInfo: WgInfo; - switch (backupWg.info.type) { - case BackupWgType.BankIntegrated: - wgInfo = { - withdrawalType: WithdrawalRecordType.BankIntegrated, - bankInfo: { - exchangePaytoUri: backupWg.info.exchange_payto_uri, - talerWithdrawUri: backupWg.info.taler_withdraw_uri, - confirmUrl: backupWg.info.confirm_url, - timestampBankConfirmed: backupWg.info.timestamp_bank_confirmed, - timestampReserveInfoPosted: - backupWg.info.timestamp_reserve_info_posted, - }, - }; - break; - case BackupWgType.BankManual: - wgInfo = { - withdrawalType: WithdrawalRecordType.BankManual, - }; - break; - case BackupWgType.PeerPullCredit: - wgInfo = { - withdrawalType: WithdrawalRecordType.PeerPullCredit, - contractTerms: backupWg.info.contract_terms, - contractPriv: backupWg.info.contract_priv, - }; - break; - case BackupWgType.PeerPushCredit: - wgInfo = { - withdrawalType: WithdrawalRecordType.PeerPushCredit, - contractTerms: backupWg.info.contract_terms, - }; - break; - case BackupWgType.Recoup: - wgInfo = { - withdrawalType: WithdrawalRecordType.Recoup, - }; - break; - default: - assertUnreachable(backupWg.info); - } - const instructedAmount = Amounts.parseOrThrow( - backupWg.instructed_amount, - ); - await tx.withdrawalGroups.put({ - withdrawalGroupId: backupWg.withdrawal_group_id, - exchangeBaseUrl: backupWg.exchange_base_url, - instructedAmount: Amounts.stringify(instructedAmount), - secretSeed: backupWg.secret_seed, - denomsSel: await getDenomSelStateFromBackup( - tx, - instructedAmount.currency, - backupWg.exchange_base_url, - backupWg.selected_denoms, - ), - denomSelUid: backupWg.selected_denoms_uid, - rawWithdrawalAmount: Amounts.stringify( - backupWg.raw_withdrawal_amount, - ), - effectiveWithdrawalAmount: Amounts.stringify( - backupWg.effective_withdrawal_amount, - ), - reservePriv: backupWg.reserve_priv, - reservePub, - status: backupWg.timestamp_finish - ? WithdrawalGroupStatus.Finished - : WithdrawalGroupStatus.PendingQueryingStatus, // FIXME! - timestampStart: backupWg.timestamp_created, - wgInfo, - restrictAge: backupWg.restrict_age, - senderWire: undefined, // FIXME! - timestampFinish: backupWg.timestamp_finish, - }); - } - - for (const backupPurchase of backupBlob.purchases) { - const ts = constructTombstone({ - tag: TombstoneTag.DeletePayment, - proposalId: backupPurchase.proposal_id, - }); - if (tombstoneSet.has(ts)) { - continue; - } - const existingPurchase = await tx.purchases.get( - backupPurchase.proposal_id, - ); - let proposalStatus: PurchaseStatus; - switch (backupPurchase.proposal_status) { - case BackupProposalStatus.Paid: - proposalStatus = PurchaseStatus.Done; - break; - case BackupProposalStatus.Shared: - proposalStatus = PurchaseStatus.DialogShared; - break; - case BackupProposalStatus.Proposed: - proposalStatus = PurchaseStatus.DialogProposed; - break; - case BackupProposalStatus.PermanentlyFailed: - proposalStatus = PurchaseStatus.AbortedIncompletePayment; - break; - case BackupProposalStatus.Refused: - proposalStatus = PurchaseStatus.AbortedProposalRefused; - break; - case BackupProposalStatus.Repurchase: - proposalStatus = PurchaseStatus.RepurchaseDetected; - break; - default: { - const error: never = backupPurchase.proposal_status; - throw Error(`backup status ${error} is not handled`); - } - } - if (!existingPurchase) { - //const refunds: { [refundKey: string]: WalletRefundItem } = {}; - // for (const backupRefund of backupPurchase.refunds) { - // const key = `${backupRefund.coin_pub}-${backupRefund.rtransaction_id}`; - // const coin = await tx.coins.get(backupRefund.coin_pub); - // checkBackupInvariant(!!coin); - // const denom = await tx.denominations.get([ - // coin.exchangeBaseUrl, - // coin.denomPubHash, - // ]); - // checkBackupInvariant(!!denom); - // const common = { - // coinPub: backupRefund.coin_pub, - // executionTime: backupRefund.execution_time, - // obtainedTime: backupRefund.obtained_time, - // refundAmount: Amounts.stringify(backupRefund.refund_amount), - // refundFee: Amounts.stringify(denom.fees.feeRefund), - // rtransactionId: backupRefund.rtransaction_id, - // totalRefreshCostBound: Amounts.stringify( - // backupRefund.total_refresh_cost_bound, - // ), - // }; - // switch (backupRefund.type) { - // case BackupRefundState.Applied: - // refunds[key] = { - // type: RefundState.Applied, - // ...common, - // }; - // break; - // case BackupRefundState.Failed: - // refunds[key] = { - // type: RefundState.Failed, - // ...common, - // }; - // break; - // case BackupRefundState.Pending: - // refunds[key] = { - // type: RefundState.Pending, - // ...common, - // }; - // break; - // } - // } - const parsedContractTerms = codecForMerchantContractTerms().decode( - backupPurchase.contract_terms_raw, - ); - const amount = Amounts.parseOrThrow(parsedContractTerms.amount); - const contractTermsHash = - cryptoComp.proposalIdToContractTermsHash[ - backupPurchase.proposal_id - ]; - let maxWireFee: AmountJson; - if (parsedContractTerms.max_wire_fee) { - maxWireFee = Amounts.parseOrThrow(parsedContractTerms.max_wire_fee); - } else { - maxWireFee = Amounts.zeroOfCurrency(amount.currency); - } - const download: ProposalDownloadInfo = { - contractTermsHash, - contractTermsMerchantSig: backupPurchase.merchant_sig!, - currency: amount.currency, - fulfillmentUrl: backupPurchase.contract_terms_raw.fulfillment_url, - }; - - const contractData = extractContractData( - backupPurchase.contract_terms_raw, - contractTermsHash, - download.contractTermsMerchantSig, - ); - - let payInfo: PurchasePayInfo | undefined = undefined; - if (backupPurchase.pay_info) { - payInfo = { - payCoinSelection: await recoverPayCoinSelection( - tx, - contractData, - backupPurchase.pay_info, - ), - payCoinSelectionUid: backupPurchase.pay_info.pay_coins_uid, - totalPayCost: Amounts.stringify( - backupPurchase.pay_info.total_pay_cost, - ), - }; - } - - await tx.purchases.put({ - proposalId: backupPurchase.proposal_id, - noncePriv: backupPurchase.nonce_priv, - noncePub: - cryptoComp.proposalNoncePrivToPub[backupPurchase.nonce_priv], - autoRefundDeadline: TalerProtocolTimestamp.never(), - timestampAccept: backupPurchase.timestamp_accepted, - timestampFirstSuccessfulPay: - backupPurchase.timestamp_first_successful_pay, - timestampLastRefundStatus: undefined, - merchantPaySig: backupPurchase.merchant_pay_sig, - posConfirmation: backupPurchase.pos_confirmation, - lastSessionId: undefined, - download, - //refunds, - claimToken: backupPurchase.claim_token, - downloadSessionId: backupPurchase.download_session_id, - merchantBaseUrl: backupPurchase.merchant_base_url, - orderId: backupPurchase.order_id, - payInfo, - refundAmountAwaiting: undefined, - repurchaseProposalId: backupPurchase.repurchase_proposal_id, - purchaseStatus: proposalStatus, - timestamp: backupPurchase.timestamp_proposed, - shared: backupPurchase.shared, - }); - } - } - - for (const backupRefreshGroup of backupBlob.refresh_groups) { - const ts = constructTombstone({ - tag: TombstoneTag.DeleteRefreshGroup, - refreshGroupId: backupRefreshGroup.refresh_group_id, - }); - if (tombstoneSet.has(ts)) { - continue; - } - const existingRg = await tx.refreshGroups.get( - backupRefreshGroup.refresh_group_id, - ); - if (!existingRg) { - let reason: RefreshReason; - switch (backupRefreshGroup.reason) { - case BackupRefreshReason.AbortPay: - reason = RefreshReason.AbortPay; - break; - case BackupRefreshReason.BackupRestored: - reason = RefreshReason.BackupRestored; - break; - case BackupRefreshReason.Manual: - reason = RefreshReason.Manual; - break; - case BackupRefreshReason.Pay: - reason = RefreshReason.PayMerchant; - break; - case BackupRefreshReason.Recoup: - reason = RefreshReason.Recoup; - break; - case BackupRefreshReason.Refund: - reason = RefreshReason.Refund; - break; - case BackupRefreshReason.Scheduled: - reason = RefreshReason.Scheduled; - break; - } - const refreshSessionPerCoin: (RefreshSessionRecord | undefined)[] = - []; - for (const oldCoin of backupRefreshGroup.old_coins) { - const c = await tx.coins.get(oldCoin.coin_pub); - checkBackupInvariant(!!c); - const d = await tx.denominations.get([ - c.exchangeBaseUrl, - c.denomPubHash, - ]); - checkBackupInvariant(!!d); - - if (oldCoin.refresh_session) { - const denomSel = await getDenomSelStateFromBackup( - tx, - d.currency, - c.exchangeBaseUrl, - oldCoin.refresh_session.new_denoms, - ); - refreshSessionPerCoin.push({ - sessionSecretSeed: oldCoin.refresh_session.session_secret_seed, - norevealIndex: oldCoin.refresh_session.noreveal_index, - newDenoms: oldCoin.refresh_session.new_denoms.map((x) => ({ - count: x.count, - denomPubHash: x.denom_pub_hash, - })), - amountRefreshOutput: Amounts.stringify(denomSel.totalCoinValue), - }); - } else { - refreshSessionPerCoin.push(undefined); - } - } - await tx.refreshGroups.put({ - timestampFinished: backupRefreshGroup.timestamp_finish, - timestampCreated: backupRefreshGroup.timestamp_created, - refreshGroupId: backupRefreshGroup.refresh_group_id, - currency: Amounts.currencyOf( - backupRefreshGroup.old_coins[0].input_amount, - ), - reason, - lastErrorPerCoin: {}, - oldCoinPubs: backupRefreshGroup.old_coins.map((x) => x.coin_pub), - statusPerCoin: backupRefreshGroup.old_coins.map((x) => - x.finished - ? RefreshCoinStatus.Finished - : RefreshCoinStatus.Pending, - ), - operationStatus: backupRefreshGroup.timestamp_finish - ? RefreshOperationStatus.Finished - : RefreshOperationStatus.Pending, - inputPerCoin: backupRefreshGroup.old_coins.map( - (x) => x.input_amount, - ), - estimatedOutputPerCoin: backupRefreshGroup.old_coins.map( - (x) => x.estimated_output_amount, - ), - refreshSessionPerCoin, - }); - } - } - - for (const backupTip of backupBlob.tips) { - const ts = constructTombstone({ - tag: TombstoneTag.DeleteReward, - walletTipId: backupTip.wallet_tip_id, - }); - if (tombstoneSet.has(ts)) { - continue; - } - const existingTip = await tx.rewards.get(backupTip.wallet_tip_id); - if (!existingTip) { - const tipAmountRaw = Amounts.parseOrThrow(backupTip.tip_amount_raw); - const denomsSel = await getDenomSelStateFromBackup( - tx, - tipAmountRaw.currency, - backupTip.exchange_base_url, - backupTip.selected_denoms, - ); - await tx.rewards.put({ - acceptedTimestamp: backupTip.timestamp_accepted, - createdTimestamp: backupTip.timestamp_created, - denomsSel, - next_url: backupTip.next_url, - exchangeBaseUrl: backupTip.exchange_base_url, - merchantBaseUrl: backupTip.exchange_base_url, - merchantRewardId: backupTip.merchant_tip_id, - pickedUpTimestamp: backupTip.timestamp_finished, - secretSeed: backupTip.secret_seed, - rewardAmountEffective: Amounts.stringify(denomsSel.totalCoinValue), - rewardAmountRaw: Amounts.stringify(tipAmountRaw), - rewardExpiration: backupTip.timestamp_expiration, - walletRewardId: backupTip.wallet_tip_id, - denomSelUid: backupTip.selected_denoms_uid, - status: RewardRecordStatus.Done, // FIXME! - }); - } - } - - // We now process tombstones. - // The import code above should already prevent - // importing things that are tombstoned, - // but we do tombstone processing last just to be sure. - - for (const tombstone of tombstoneSet) { - const [type, ...rest] = tombstone.split(":"); - if (type === TombstoneTag.DeleteDepositGroup) { - await tx.depositGroups.delete(rest[0]); - } else if (type === TombstoneTag.DeletePayment) { - await tx.purchases.delete(rest[0]); - } else if (type === TombstoneTag.DeleteRefreshGroup) { - await tx.refreshGroups.delete(rest[0]); - } else if (type === TombstoneTag.DeleteRefund) { - // Nothing required, will just prevent display - // in the transactions list - } else if (type === TombstoneTag.DeleteReward) { - await tx.rewards.delete(rest[0]); - } else if (type === TombstoneTag.DeleteWithdrawalGroup) { - await tx.withdrawalGroups.delete(rest[0]); - } else { - logger.warn(`unable to process tombstone of type '${type}'`); - } - } - }); -} diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts b/packages/taler-wallet-core/src/operations/backup/index.ts index e35765165..a5e8dbd42 100644 --- a/packages/taler-wallet-core/src/operations/backup/index.ts +++ b/packages/taler-wallet-core/src/operations/backup/index.ts @@ -43,7 +43,6 @@ import { TalerErrorDetail, TalerPreciseTimestamp, URL, - WalletBackupContentV1, buildCodecForObject, buildCodecForUnion, bytesToString, @@ -99,9 +98,8 @@ import { TaskIdentifiers, } from "../common.js"; import { checkPaymentByProposalId, preparePayForUri } from "../pay-merchant.js"; -import { exportBackup } from "./export.js"; -import { BackupCryptoPrecomputedData, importBackup } from "./import.js"; -import { getWalletBackupState, provideBackupState } from "./state.js"; +import { WalletStoresV1 } from "../../db.js"; +import { GetReadOnlyAccess } from "../../util/query.js"; const logger = new Logger("operations/backup.ts"); @@ -131,7 +129,7 @@ const magic = "TLRWBK01"; */ export async function encryptBackup( config: WalletBackupConfState, - blob: WalletBackupContentV1, + blob: any, ): Promise<Uint8Array> { const chunks: Uint8Array[] = []; chunks.push(stringToBytes(magic)); @@ -150,64 +148,6 @@ export async function encryptBackup( return concatArrays(chunks); } -/** - * Compute cryptographic values for a backup blob. - * - * FIXME: Take data that we already know from the DB. - * FIXME: Move computations into crypto worker. - */ -async function computeBackupCryptoData( - cryptoApi: TalerCryptoInterface, - backupContent: WalletBackupContentV1, -): Promise<BackupCryptoPrecomputedData> { - const cryptoData: BackupCryptoPrecomputedData = { - coinPrivToCompletedCoin: {}, - rsaDenomPubToHash: {}, - proposalIdToContractTermsHash: {}, - proposalNoncePrivToPub: {}, - reservePrivToPub: {}, - }; - for (const backupExchangeDetails of backupContent.exchange_details) { - for (const backupDenom of backupExchangeDetails.denominations) { - if (backupDenom.denom_pub.cipher !== DenomKeyType.Rsa) { - throw Error("unsupported cipher"); - } - for (const backupCoin of backupDenom.coins) { - const coinPub = encodeCrock( - eddsaGetPublic(decodeCrock(backupCoin.coin_priv)), - ); - const blindedCoin = rsaBlind( - hash(decodeCrock(backupCoin.coin_priv)), - decodeCrock(backupCoin.blinding_key), - decodeCrock(backupDenom.denom_pub.rsa_public_key), - ); - cryptoData.coinPrivToCompletedCoin[backupCoin.coin_priv] = { - coinEvHash: encodeCrock(hash(blindedCoin)), - coinPub, - }; - } - cryptoData.rsaDenomPubToHash[backupDenom.denom_pub.rsa_public_key] = - encodeCrock(hashDenomPub(backupDenom.denom_pub)); - } - } - for (const backupWg of backupContent.withdrawal_groups) { - cryptoData.reservePrivToPub[backupWg.reserve_priv] = encodeCrock( - eddsaGetPublic(decodeCrock(backupWg.reserve_priv)), - ); - } - for (const purch of backupContent.purchases) { - if (!purch.contract_terms_raw) continue; - const { h: contractTermsHash } = await cryptoApi.hashString({ - str: canonicalJson(purch.contract_terms_raw), - }); - const noncePub = encodeCrock(eddsaGetPublic(decodeCrock(purch.nonce_priv))); - cryptoData.proposalNoncePrivToPub[purch.nonce_priv] = noncePub; - cryptoData.proposalIdToContractTermsHash[purch.proposal_id] = - contractTermsHash; - } - return cryptoData; -} - function deriveAccountKeyPair( bc: WalletBackupConfState, providerUrl: string, @@ -262,7 +202,9 @@ async function runBackupCycleForProvider( return TaskRunResult.finished(); } - const backupJson = await exportBackup(ws); + //const backupJson = await exportBackup(ws); + // FIXME: re-implement backup + const backupJson = {}; const backupConfig = await provideBackupState(ws); const encBackup = await encryptBackup(backupConfig, backupJson); const currentBackupHash = hash(encBackup); @@ -441,9 +383,9 @@ async function runBackupCycleForProvider( logger.info("conflicting backup found"); const backupEnc = new Uint8Array(await resp.bytes()); const backupConfig = await provideBackupState(ws); - const blob = await decryptBackup(backupConfig, backupEnc); - const cryptoData = await computeBackupCryptoData(ws.cryptoApi, blob); - await importBackup(ws, blob, cryptoData); + // const blob = await decryptBackup(backupConfig, backupEnc); + // FIXME: Re-implement backup import with merging + // await importBackup(ws, blob, cryptoData); await ws.db .mktx((x) => [x.backupProviders, x.operationRetries]) .runReadWrite(async (tx) => { @@ -789,18 +731,6 @@ export interface BackupInfo { providers: ProviderInfo[]; } -export async function importBackupPlain( - ws: InternalWalletState, - blob: any, -): Promise<void> { - // FIXME: parse - const backup: WalletBackupContentV1 = blob; - - const cryptoData = await computeBackupCryptoData(ws.cryptoApi, backup); - - await importBackup(ws, blob, cryptoData); -} - export enum ProviderPaymentType { Unpaid = "unpaid", Pending = "pending", @@ -1036,23 +966,10 @@ export async function loadBackupRecovery( } } -export async function exportBackupEncrypted( - ws: InternalWalletState, -): Promise<Uint8Array> { - await provideBackupState(ws); - const blob = await exportBackup(ws); - const bs = await ws.db - .mktx((x) => [x.config]) - .runReadOnly(async (tx) => { - return await getWalletBackupState(ws, tx); - }); - return encryptBackup(bs, blob); -} - export async function decryptBackup( backupConfig: WalletBackupConfState, data: Uint8Array, -): Promise<WalletBackupContentV1> { +): Promise<any> { const rMagic = bytesToString(data.slice(0, 8)); if (rMagic != magic) { throw Error("invalid backup file (magic tag mismatch)"); @@ -1068,12 +985,85 @@ export async function decryptBackup( return JSON.parse(bytesToString(gunzipSync(dataCompressed))); } -export async function importBackupEncrypted( +export async function provideBackupState( ws: InternalWalletState, - data: Uint8Array, +): Promise<WalletBackupConfState> { + const bs: ConfigRecord | undefined = await ws.db + .mktx((stores) => [stores.config]) + .runReadOnly(async (tx) => { + return await tx.config.get(ConfigRecordKey.WalletBackupState); + }); + if (bs) { + checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState); + return bs.value; + } + // We need to generate the key outside of the transaction + // due to how IndexedDB works. + const k = await ws.cryptoApi.createEddsaKeypair({}); + const d = getRandomBytes(5); + // FIXME: device ID should be configured when wallet is initialized + // and be based on hostname + const deviceId = `wallet-core-${encodeCrock(d)}`; + return await ws.db + .mktx((x) => [x.config]) + .runReadWrite(async (tx) => { + let backupStateEntry: ConfigRecord | undefined = await tx.config.get( + ConfigRecordKey.WalletBackupState, + ); + if (!backupStateEntry) { + backupStateEntry = { + key: ConfigRecordKey.WalletBackupState, + value: { + deviceId, + walletRootPub: k.pub, + walletRootPriv: k.priv, + lastBackupPlainHash: undefined, + }, + }; + await tx.config.put(backupStateEntry); + } + checkDbInvariant( + backupStateEntry.key === ConfigRecordKey.WalletBackupState, + ); + return backupStateEntry.value; + }); +} + +export async function getWalletBackupState( + ws: InternalWalletState, + tx: GetReadOnlyAccess<{ config: typeof WalletStoresV1.config }>, +): Promise<WalletBackupConfState> { + const bs = await tx.config.get(ConfigRecordKey.WalletBackupState); + checkDbInvariant(!!bs, "wallet backup state should be in DB"); + checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState); + return bs.value; +} + +export async function setWalletDeviceId( + ws: InternalWalletState, + deviceId: string, ): Promise<void> { - const backupConfig = await provideBackupState(ws); - const blob = await decryptBackup(backupConfig, data); - const cryptoData = await computeBackupCryptoData(ws.cryptoApi, blob); - await importBackup(ws, blob, cryptoData); + await provideBackupState(ws); + await ws.db + .mktx((x) => [x.config]) + .runReadWrite(async (tx) => { + let backupStateEntry: ConfigRecord | undefined = await tx.config.get( + ConfigRecordKey.WalletBackupState, + ); + if ( + !backupStateEntry || + backupStateEntry.key !== ConfigRecordKey.WalletBackupState + ) { + return; + } + backupStateEntry.value.deviceId = deviceId; + await tx.config.put(backupStateEntry); + }); +} + +export async function getWalletDeviceId( + ws: InternalWalletState, +): Promise<string> { + const bs = await provideBackupState(ws); + return bs.deviceId; } diff --git a/packages/taler-wallet-core/src/operations/backup/state.ts b/packages/taler-wallet-core/src/operations/backup/state.ts index fa632f44c..d02ead783 100644 --- a/packages/taler-wallet-core/src/operations/backup/state.ts +++ b/packages/taler-wallet-core/src/operations/backup/state.ts @@ -14,96 +14,4 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { encodeCrock, getRandomBytes } from "@gnu-taler/taler-util"; -import { - ConfigRecord, - ConfigRecordKey, - WalletBackupConfState, - WalletStoresV1, -} from "../../db.js"; -import { checkDbInvariant } from "../../util/invariants.js"; -import { GetReadOnlyAccess } from "../../util/query.js"; -import { InternalWalletState } from "../../internal-wallet-state.js"; -export async function provideBackupState( - ws: InternalWalletState, -): Promise<WalletBackupConfState> { - const bs: ConfigRecord | undefined = await ws.db - .mktx((stores) => [stores.config]) - .runReadOnly(async (tx) => { - return await tx.config.get(ConfigRecordKey.WalletBackupState); - }); - if (bs) { - checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState); - return bs.value; - } - // We need to generate the key outside of the transaction - // due to how IndexedDB works. - const k = await ws.cryptoApi.createEddsaKeypair({}); - const d = getRandomBytes(5); - // FIXME: device ID should be configured when wallet is initialized - // and be based on hostname - const deviceId = `wallet-core-${encodeCrock(d)}`; - return await ws.db - .mktx((x) => [x.config]) - .runReadWrite(async (tx) => { - let backupStateEntry: ConfigRecord | undefined = await tx.config.get( - ConfigRecordKey.WalletBackupState, - ); - if (!backupStateEntry) { - backupStateEntry = { - key: ConfigRecordKey.WalletBackupState, - value: { - deviceId, - walletRootPub: k.pub, - walletRootPriv: k.priv, - lastBackupPlainHash: undefined, - }, - }; - await tx.config.put(backupStateEntry); - } - checkDbInvariant( - backupStateEntry.key === ConfigRecordKey.WalletBackupState, - ); - return backupStateEntry.value; - }); -} - -export async function getWalletBackupState( - ws: InternalWalletState, - tx: GetReadOnlyAccess<{ config: typeof WalletStoresV1.config }>, -): Promise<WalletBackupConfState> { - const bs = await tx.config.get(ConfigRecordKey.WalletBackupState); - checkDbInvariant(!!bs, "wallet backup state should be in DB"); - checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState); - return bs.value; -} - -export async function setWalletDeviceId( - ws: InternalWalletState, - deviceId: string, -): Promise<void> { - await provideBackupState(ws); - await ws.db - .mktx((x) => [x.config]) - .runReadWrite(async (tx) => { - let backupStateEntry: ConfigRecord | undefined = await tx.config.get( - ConfigRecordKey.WalletBackupState, - ); - if ( - !backupStateEntry || - backupStateEntry.key !== ConfigRecordKey.WalletBackupState - ) { - return; - } - backupStateEntry.value.deviceId = deviceId; - await tx.config.put(backupStateEntry); - }); -} - -export async function getWalletDeviceId( - ws: InternalWalletState, -): Promise<string> { - const bs = await provideBackupState(ws); - return bs.deviceId; -} diff --git a/packages/taler-wallet-core/src/operations/common.ts b/packages/taler-wallet-core/src/operations/common.ts index 7a8b78b53..e96beb5b2 100644 --- a/packages/taler-wallet-core/src/operations/common.ts +++ b/packages/taler-wallet-core/src/operations/common.ts @@ -30,6 +30,7 @@ import { ExchangeEntryStatus, ExchangeListItem, ExchangeTosStatus, + ExchangeUpdateStatus, getErrorDetailFromException, j2s, Logger, @@ -47,7 +48,7 @@ import { WalletStoresV1, CoinRecord, ExchangeDetailsRecord, - ExchangeRecord, + ExchangeEntryRecord, BackupProviderRecord, DepositGroupRecord, PeerPullPaymentIncomingRecord, @@ -59,6 +60,8 @@ import { RefreshGroupRecord, RewardRecord, WithdrawalGroupRecord, + ExchangeEntryDbUpdateStatus, + ExchangeEntryDbRecordStatus, } from "../db.js"; import { makeErrorDetail, TalerError } from "@gnu-taler/taler-util"; import { InternalWalletState } from "../internal-wallet-state.js"; @@ -529,16 +532,16 @@ export function getExchangeTosStatus( exchangeDetails: ExchangeDetailsRecord, ): ExchangeTosStatus { if (!exchangeDetails.tosAccepted) { - return ExchangeTosStatus.New; + return ExchangeTosStatus.Proposed; } if (exchangeDetails.tosAccepted?.etag == exchangeDetails.tosCurrentEtag) { return ExchangeTosStatus.Accepted; } - return ExchangeTosStatus.Changed; + return ExchangeTosStatus.Proposed; } export function makeExchangeListItem( - r: ExchangeRecord, + r: ExchangeEntryRecord, exchangeDetails: ExchangeDetailsRecord | undefined, lastError: TalerErrorDetail | undefined, ): ExchangeListItem { @@ -547,30 +550,57 @@ export function makeExchangeListItem( error: lastError, } : undefined; - if (!exchangeDetails) { - return { - exchangeBaseUrl: r.baseUrl, - currency: undefined, - tosStatus: ExchangeTosStatus.Unknown, - paytoUris: [], - exchangeStatus: ExchangeEntryStatus.Unknown, - permanent: r.permanent, - ageRestrictionOptions: [], - lastUpdateErrorInfo, - }; + + let exchangeUpdateStatus: ExchangeUpdateStatus; + switch (r.updateStatus) { + case ExchangeEntryDbUpdateStatus.Failed: + exchangeUpdateStatus = ExchangeUpdateStatus.Failed; + break; + case ExchangeEntryDbUpdateStatus.Initial: + exchangeUpdateStatus = ExchangeUpdateStatus.Initial; + break; + case ExchangeEntryDbUpdateStatus.InitialUpdate: + exchangeUpdateStatus = ExchangeUpdateStatus.InitialUpdate; + break; + case ExchangeEntryDbUpdateStatus.OutdatedUpdate: + exchangeUpdateStatus = ExchangeUpdateStatus.OutdatedUpdate; + break; + case ExchangeEntryDbUpdateStatus.Ready: + exchangeUpdateStatus = ExchangeUpdateStatus.Ready; + break; + case ExchangeEntryDbUpdateStatus.ReadyUpdate: + exchangeUpdateStatus = ExchangeUpdateStatus.ReadyUpdate; + break; + case ExchangeEntryDbUpdateStatus.Suspended: + exchangeUpdateStatus = ExchangeUpdateStatus.Suspended; + break; + } + + let exchangeEntryStatus: ExchangeEntryStatus; + switch (r.entryStatus) { + case ExchangeEntryDbRecordStatus.Ephemeral: + exchangeEntryStatus = ExchangeEntryStatus.Ephemeral; + break; + case ExchangeEntryDbRecordStatus.Preset: + exchangeEntryStatus = ExchangeEntryStatus.Preset; + break; + case ExchangeEntryDbRecordStatus.Used: + exchangeEntryStatus = ExchangeEntryStatus.Used; + break; } - let exchangeStatus; - exchangeStatus = ExchangeEntryStatus.Ok; + return { exchangeBaseUrl: r.baseUrl, - currency: exchangeDetails.currency, - tosStatus: getExchangeTosStatus(exchangeDetails), - paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri), - exchangeStatus, - permanent: r.permanent, - ageRestrictionOptions: exchangeDetails.ageMask + currency: exchangeDetails?.currency, + exchangeUpdateStatus, + exchangeEntryStatus, + tosStatus: exchangeDetails + ? getExchangeTosStatus(exchangeDetails) + : ExchangeTosStatus.Pending, + ageRestrictionOptions: exchangeDetails?.ageMask ? AgeRestriction.getAgeGroupsFromMask(exchangeDetails.ageMask) : [], + paytoUris: exchangeDetails?.wireInfo.accounts.map((x) => x.payto_uri) ?? [], lastUpdateErrorInfo, }; } @@ -892,13 +922,13 @@ export namespace TaskIdentifiers { export function forWithdrawal(wg: WithdrawalGroupRecord): TaskId { return `${PendingTaskType.Withdraw}:${wg.withdrawalGroupId}` as TaskId; } - export function forExchangeUpdate(exch: ExchangeRecord): TaskId { + export function forExchangeUpdate(exch: ExchangeEntryRecord): TaskId { return `${PendingTaskType.ExchangeUpdate}:${exch.baseUrl}` as TaskId; } export function forExchangeUpdateFromUrl(exchBaseUrl: string): TaskId { return `${PendingTaskType.ExchangeUpdate}:${exchBaseUrl}` as TaskId; } - export function forExchangeCheckRefresh(exch: ExchangeRecord): TaskId { + export function forExchangeCheckRefresh(exch: ExchangeEntryRecord): TaskId { return `${PendingTaskType.ExchangeCheckRefresh}:${exch.baseUrl}` as TaskId; } export function forTipPickup(tipRecord: RewardRecord): TaskId { diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts index c6b46e360..311a71a6e 100644 --- a/packages/taler-wallet-core/src/operations/exchanges.ts +++ b/packages/taler-wallet-core/src/operations/exchanges.ts @@ -32,6 +32,7 @@ import { encodeCrock, ExchangeAuditor, ExchangeDenomination, + ExchangeEntryStatus, ExchangeGlobalFees, ExchangeSignKeyJson, ExchangeWireJson, @@ -66,10 +67,15 @@ import { DenominationRecord, DenominationVerificationStatus, ExchangeDetailsRecord, - ExchangeRecord, + ExchangeEntryRecord, WalletStoresV1, } from "../db.js"; -import { isWithdrawableDenom } from "../index.js"; +import { + ExchangeEntryDbRecordStatus, + ExchangeEntryDbUpdateStatus, + isWithdrawableDenom, + WalletDbReadWriteTransaction, +} from "../index.js"; import { InternalWalletState, TrustInfo } from "../internal-wallet-state.js"; import { checkDbInvariant } from "../util/invariants.js"; import { @@ -326,6 +332,26 @@ export async function downloadExchangeInfo( }; } +export async function addPresetExchangeEntry( + tx: WalletDbReadWriteTransaction<"exchanges">, + exchangeBaseUrl: string, +): Promise<void> { + let exchange = await tx.exchanges.get(exchangeBaseUrl); + if (!exchange) { + const r: ExchangeEntryRecord = { + entryStatus: ExchangeEntryDbRecordStatus.Preset, + updateStatus: ExchangeEntryDbUpdateStatus.Initial, + baseUrl: exchangeBaseUrl, + detailsPointer: undefined, + lastUpdate: undefined, + lastKeysEtag: undefined, + nextRefreshCheckStampMs: AbsoluteTime.getStampMsNever(), + nextUpdateStampMs: AbsoluteTime.getStampMsNever(), + }; + await tx.exchanges.put(r); + } +} + export async function provideExchangeRecordInTx( ws: InternalWalletState, tx: GetReadWriteAccess<{ @@ -335,20 +361,20 @@ export async function provideExchangeRecordInTx( baseUrl: string, now: AbsoluteTime, ): Promise<{ - exchange: ExchangeRecord; + exchange: ExchangeEntryRecord; exchangeDetails: ExchangeDetailsRecord | undefined; }> { let exchange = await tx.exchanges.get(baseUrl); if (!exchange) { - const r: ExchangeRecord = { - permanent: true, + const r: ExchangeEntryRecord = { + entryStatus: ExchangeEntryDbRecordStatus.Ephemeral, + updateStatus: ExchangeEntryDbUpdateStatus.InitialUpdate, baseUrl: baseUrl, detailsPointer: undefined, lastUpdate: undefined, - nextUpdate: AbsoluteTime.toPreciseTimestamp(now), - nextRefreshCheck: AbsoluteTime.toPreciseTimestamp(now), + nextUpdateStampMs: AbsoluteTime.getStampMsNever(), + nextRefreshCheckStampMs: AbsoluteTime.getStampMsNever(), lastKeysEtag: undefined, - lastWireEtag: undefined, }; await tx.exchanges.put(r); exchange = r; @@ -534,6 +560,10 @@ export async function downloadTosFromAcceptedFormat( ); } +/** + * FIXME: Split this into two parts: (a) triggering the exchange + * to be updated and (b) waiting for the update to finish. + */ export async function updateExchangeFromUrl( ws: InternalWalletState, baseUrl: string, @@ -543,7 +573,7 @@ export async function updateExchangeFromUrl( cancellationToken?: CancellationToken; } = {}, ): Promise<{ - exchange: ExchangeRecord; + exchange: ExchangeEntryRecord; exchangeDetails: ExchangeDetailsRecord; }> { const canonUrl = canonicalizeBaseUrl(baseUrl); @@ -613,7 +643,7 @@ export async function updateExchangeFromUrlHandler( !forceNow && exchangeDetails !== undefined && !AbsoluteTime.isExpired( - AbsoluteTime.fromPreciseTimestamp(exchange.nextUpdate), + AbsoluteTime.fromStampMs(exchange.nextUpdateStampMs), ) ) { logger.trace("using existing exchange info"); @@ -755,11 +785,11 @@ export async function updateExchangeFromUrlHandler( newDetails.rowId = existingDetails.rowId; } r.lastUpdate = TalerPreciseTimestamp.now(); - r.nextUpdate = AbsoluteTime.toPreciseTimestamp( + r.nextUpdateStampMs = AbsoluteTime.toStampMs( AbsoluteTime.fromProtocolTimestamp(keysInfo.expiry), ); // New denominations might be available. - r.nextRefreshCheck = TalerPreciseTimestamp.now(); + r.nextRefreshCheckStampMs = AbsoluteTime.getStampMsNow(); if (detailsPointerChanged) { r.detailsPointer = { currency: newDetails.currency, @@ -948,7 +978,7 @@ export async function getExchangePaytoUri( */ export async function getExchangeTrust( ws: InternalWalletState, - exchangeInfo: ExchangeRecord, + exchangeInfo: ExchangeEntryRecord, ): Promise<TrustInfo> { let isTrusted = false; let isAudited = false; diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts index 6c6546f83..e37e45c16 100644 --- a/packages/taler-wallet-core/src/operations/pending.ts +++ b/packages/taler-wallet-core/src/operations/pending.ts @@ -45,6 +45,7 @@ import { PeerPushPaymentIncomingRecord, RefundGroupRecord, RefundGroupStatus, + ExchangeEntryDbUpdateStatus, } from "../db.js"; import { PendingOperationsResponse, @@ -81,19 +82,25 @@ async function gatherExchangePending( ws: InternalWalletState, tx: GetReadOnlyAccess<{ exchanges: typeof WalletStoresV1.exchanges; - exchangeDetails: typeof WalletStoresV1.exchangeDetails; operationRetries: typeof WalletStoresV1.operationRetries; }>, now: AbsoluteTime, resp: PendingOperationsResponse, ): Promise<void> { - // FIXME: We should do a range query here based on the update time. + // FIXME: We should do a range query here based on the update time + // and/or the entry state. await tx.exchanges.iter().forEachAsync(async (exch) => { + switch (exch.updateStatus) { + case ExchangeEntryDbUpdateStatus.Initial: + case ExchangeEntryDbUpdateStatus.Suspended: + case ExchangeEntryDbUpdateStatus.Failed: + return; + } const opTag = TaskIdentifiers.forExchangeUpdate(exch); let opr = await tx.operationRetries.get(opTag); const timestampDue = opr?.retryInfo.nextRetry ?? - AbsoluteTime.fromPreciseTimestamp(exch.nextUpdate); + AbsoluteTime.fromStampMs(exch.nextUpdateStampMs); resp.pendingOperations.push({ type: PendingTaskType.ExchangeUpdate, ...getPendingCommon(ws, opTag, timestampDue), @@ -108,7 +115,7 @@ async function gatherExchangePending( resp.pendingOperations.push({ type: PendingTaskType.ExchangeCheckRefresh, ...getPendingCommon(ws, opTag, timestampDue), - timestampDue: AbsoluteTime.fromPreciseTimestamp(exch.nextRefreshCheck), + timestampDue: AbsoluteTime.fromStampMs(exch.nextRefreshCheckStampMs), givesLifeness: false, exchangeBaseUrl: exch.baseUrl, }); @@ -184,8 +191,9 @@ export async function iterRecordsForWithdrawal( WithdrawalGroupStatus.PendingRegisteringBank, WithdrawalGroupStatus.PendingAml, ); - withdrawalGroupRecords = - await tx.withdrawalGroups.indexes.byStatus.getAll(range); + withdrawalGroupRecords = await tx.withdrawalGroups.indexes.byStatus.getAll( + range, + ); } else { withdrawalGroupRecords = await tx.withdrawalGroups.indexes.byStatus.getAll(); @@ -344,12 +352,8 @@ export async function iterRecordsForRefund( f: (r: RefundGroupRecord) => Promise<void>, ): Promise<void> { if (filter.onlyState === "nonfinal") { - const keyRange = GlobalIDB.KeyRange.only( - RefundGroupStatus.Pending - ); - await tx.refundGroups.indexes.byStatus - .iter(keyRange) - .forEachAsync(f); + const keyRange = GlobalIDB.KeyRange.only(RefundGroupStatus.Pending); + await tx.refundGroups.indexes.byStatus.iter(keyRange).forEachAsync(f); } else { await tx.refundGroups.iter().forEachAsync(f); } diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts index 72d1a2725..fb356f0fc 100644 --- a/packages/taler-wallet-core/src/operations/refresh.ts +++ b/packages/taler-wallet-core/src/operations/refresh.ts @@ -1190,14 +1190,14 @@ export async function autoRefresh( `created refresh group for auto-refresh (${res.refreshGroupId})`, ); } -// logger.trace( -// `current wallet time: ${AbsoluteTime.toIsoString(AbsoluteTime.now())}`, -// ); + // logger.trace( + // `current wallet time: ${AbsoluteTime.toIsoString(AbsoluteTime.now())}`, + // ); logger.trace( `next refresh check at ${AbsoluteTime.toIsoString(minCheckThreshold)}`, ); - exchange.nextRefreshCheck = - AbsoluteTime.toPreciseTimestamp(minCheckThreshold); + exchange.nextRefreshCheckStampMs = + AbsoluteTime.toStampMs(minCheckThreshold); await tx.exchanges.put(exchange); }); return TaskRunResult.finished(); diff --git a/packages/taler-wallet-core/src/operations/reward.ts b/packages/taler-wallet-core/src/operations/reward.ts index 69c888d7a..6f9d3ce85 100644 --- a/packages/taler-wallet-core/src/operations/reward.ts +++ b/packages/taler-wallet-core/src/operations/reward.ts @@ -150,14 +150,14 @@ export async function prepareTip( .mktx((x) => [x.rewards]) .runReadOnly(async (tx) => { return tx.rewards.indexes.byMerchantTipIdAndBaseUrl.get([ - res.merchantTipId, + res.merchantRewardId, res.merchantBaseUrl, ]); }); if (!tipRecord) { const tipStatusUrl = new URL( - `tips/${res.merchantTipId}`, + `rewards/${res.merchantRewardId}`, res.merchantBaseUrl, ); logger.trace("checking tip status from", tipStatusUrl.href); @@ -204,7 +204,7 @@ export async function prepareTip( next_url: tipPickupStatus.next_url, merchantBaseUrl: res.merchantBaseUrl, createdTimestamp: TalerPreciseTimestamp.now(), - merchantRewardId: res.merchantTipId, + merchantRewardId: res.merchantRewardId, rewardAmountEffective: Amounts.stringify(selectedDenoms.totalCoinValue), denomsSel: selectedDenoms, pickedUpTimestamp: undefined, diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 44817b389..a3d95fb5c 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -132,6 +132,8 @@ import { } from "../util/coinSelection.js"; import { ExchangeDetailsRecord, + ExchangeEntryDbRecordStatus, + ExchangeEntryDbUpdateStatus, PendingTaskType, isWithdrawableDenom, } from "../index.js"; @@ -2346,10 +2348,6 @@ export async function internalPerformCreateWithdrawalGroup( }>, prep: PrepareCreateWithdrawalGroupResult, ): Promise<PerformCreateWithdrawalGroupResult> { - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Withdrawal, - withdrawalGroupId: prep.withdrawalGroup.withdrawalGroupId, - }); const { withdrawalGroup } = prep; if (!prep.creationInfo) { return { withdrawalGroup, transitionInfo: undefined }; @@ -2366,6 +2364,7 @@ export async function internalPerformCreateWithdrawalGroup( const exchange = await tx.exchanges.get(withdrawalGroup.exchangeBaseUrl); if (exchange) { exchange.lastWithdrawal = TalerPreciseTimestamp.now(); + exchange.entryStatus = ExchangeEntryDbRecordStatus.Used; await tx.exchanges.put(exchange); } diff --git a/packages/taler-wallet-core/src/util/query.ts b/packages/taler-wallet-core/src/util/query.ts index 71f80f8aa..1c3ff6a2a 100644 --- a/packages/taler-wallet-core/src/util/query.ts +++ b/packages/taler-wallet-core/src/util/query.ts @@ -239,7 +239,7 @@ class ResultStream<T> { export function openDatabase( idbFactory: IDBFactory, databaseName: string, - databaseVersion: number, + databaseVersion: number | undefined, onVersionChange: () => void, onUpgradeNeeded: ( db: IDBDatabase, @@ -257,7 +257,7 @@ export function openDatabase( req.onsuccess = (e) => { req.result.onversionchange = (evt: IDBVersionChangeEvent) => { logger.info( - `handling live db version change from ${evt.oldVersion} to ${evt.newVersion}`, + `handling versionchange on ${databaseName} from ${evt.oldVersion} to ${evt.newVersion}`, ); req.result.close(); onVersionChange(); @@ -274,6 +274,9 @@ export function openDatabase( if (!transaction) { throw Error("no transaction handle available in upgrade handler"); } + logger.info( + `handling upgradeneeded event on ${databaseName} from ${e.oldVersion} to ${e.newVersion}`, + ); onUpgradeNeeded(db, e.oldVersion, newVersion, transaction); }; }); @@ -376,8 +379,8 @@ export interface InsertResponse { export interface StoreReadWriteAccessor<RecordType, IndexMap> { get(key: IDBValidKey): Promise<RecordType | undefined>; iter(query?: IDBValidKey): ResultStream<RecordType>; - put(r: RecordType): Promise<InsertResponse>; - add(r: RecordType): Promise<InsertResponse>; + put(r: RecordType, key?: IDBValidKey): Promise<InsertResponse>; + add(r: RecordType, key?: IDBValidKey): Promise<InsertResponse>; delete(key: IDBValidKey): Promise<void>; indexes: GetIndexReadWriteAccess<RecordType, IndexMap>; } @@ -652,15 +655,15 @@ function makeWriteContext( const req = tx.objectStore(storeName).openCursor(query); return new ResultStream<any>(req); }, - async add(r) { - const req = tx.objectStore(storeName).add(r); + async add(r, k) { + const req = tx.objectStore(storeName).add(r, k); const key = await requestToPromise(req); return { key: key, }; }, - async put(r) { - const req = tx.objectStore(storeName).put(r); + async put(r, k) { + const req = tx.objectStore(storeName).put(r, k); const key = await requestToPromise(req); return { key: key, diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts index 06ccdf6f3..4d9d40c43 100644 --- a/packages/taler-wallet-core/src/wallet-api-types.ts +++ b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -106,7 +106,6 @@ import { UserAttentionsResponse, ValidateIbanRequest, ValidateIbanResponse, - WalletBackupContentV1, WalletCoreVersion, WalletCurrencyInfo, WithdrawFakebankRequest, @@ -116,6 +115,10 @@ import { SharePaymentResult, GetCurrencyInfoRequest, GetCurrencyInfoResponse, + StoredBackupList, + CreateStoredBackupResponse, + RecoverStoredBackupRequest, + DeleteStoredBackupRequest, } from "@gnu-taler/taler-util"; import { AuditorTrustRecord, WalletContractData } from "./db.js"; import { @@ -195,7 +198,6 @@ export enum WalletApiOperation { GenerateDepositGroupTxId = "generateDepositGroupTxId", CreateDepositGroup = "createDepositGroup", SetWalletDeviceId = "setWalletDeviceId", - ExportBackupPlain = "exportBackupPlain", WithdrawFakebank = "withdrawFakebank", ImportDb = "importDb", ExportDb = "exportDb", @@ -214,6 +216,10 @@ export enum WalletApiOperation { TestingWaitTransactionsFinal = "testingWaitTransactionsFinal", TestingWaitRefreshesFinal = "testingWaitRefreshesFinal", GetScopedCurrencyInfo = "getScopedCurrencyInfo", + ListStoredBackups = "listStoredBackups", + CreateStoredBackup = "createStoredBackup", + DeleteStoredBackup = "deleteStoredBackup", + RecoverStoredBackup = "recoverStoredBackup", } // group: Initialization @@ -713,13 +719,28 @@ export type SetWalletDeviceIdOp = { response: EmptyObject; }; -/** - * Export a backup JSON, mostly useful for testing. - */ -export type ExportBackupPlainOp = { - op: WalletApiOperation.ExportBackupPlain; +export type ListStoredBackupsOp = { + op: WalletApiOperation.ListStoredBackups; + request: EmptyObject; + response: StoredBackupList; +}; + +export type CreateStoredBackupsOp = { + op: WalletApiOperation.CreateStoredBackup; request: EmptyObject; - response: WalletBackupContentV1; + response: CreateStoredBackupResponse; +}; + +export type RecoverStoredBackupsOp = { + op: WalletApiOperation.RecoverStoredBackup; + request: RecoverStoredBackupRequest; + response: EmptyObject; +}; + +export type DeleteStoredBackupOp = { + op: WalletApiOperation.DeleteStoredBackup; + request: DeleteStoredBackupRequest; + response: EmptyObject; }; // group: Peer Payments @@ -1062,7 +1083,6 @@ export type WalletOperations = { [WalletApiOperation.GenerateDepositGroupTxId]: GenerateDepositGroupTxIdOp; [WalletApiOperation.CreateDepositGroup]: CreateDepositGroupOp; [WalletApiOperation.SetWalletDeviceId]: SetWalletDeviceIdOp; - [WalletApiOperation.ExportBackupPlain]: ExportBackupPlainOp; [WalletApiOperation.ExportBackupRecovery]: ExportBackupRecoveryOp; [WalletApiOperation.ImportBackupRecovery]: ImportBackupRecoveryOp; [WalletApiOperation.RunBackupCycle]: RunBackupCycleOp; @@ -1092,6 +1112,10 @@ export type WalletOperations = { [WalletApiOperation.TestingWaitTransactionsFinal]: TestingWaitTransactionsFinal; [WalletApiOperation.TestingWaitRefreshesFinal]: TestingWaitRefreshesFinal; [WalletApiOperation.GetScopedCurrencyInfo]: GetScopedCurrencyInfoOp; + [WalletApiOperation.CreateStoredBackup]: CreateStoredBackupsOp; + [WalletApiOperation.ListStoredBackups]: ListStoredBackupsOp; + [WalletApiOperation.DeleteStoredBackup]: DeleteStoredBackupOp; + [WalletApiOperation.RecoverStoredBackup]: RecoverStoredBackupsOp; }; export type WalletCoreRequestType< diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 194894e52..626409dd6 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -120,6 +120,7 @@ import { codecForSharePaymentRequest, GetCurrencyInfoResponse, codecForGetCurrencyInfoRequest, + CreateStoredBackupResponse, } from "@gnu-taler/taler-util"; import { HttpRequestLibrary, @@ -139,6 +140,8 @@ import { clearDatabase, exportDb, importDb, + openStoredBackupsDatabase, + openTalerDatabase, } from "./db.js"; import { DevExperimentHttpLib, applyDevExperiment } from "./dev-experiments.js"; import { @@ -157,7 +160,6 @@ import { getUserAttentionsUnreadCount, markAttentionRequestAsRead, } from "./operations/attention.js"; -import { exportBackup } from "./operations/backup/export.js"; import { addBackupProvider, codecForAddBackupProviderRequest, @@ -165,13 +167,12 @@ import { codecForRunBackupCycle, getBackupInfo, getBackupRecovery, - importBackupPlain, loadBackupRecovery, processBackupForProvider, removeBackupProvider, runBackupCycle, + setWalletDeviceId, } from "./operations/backup/index.js"; -import { setWalletDeviceId } from "./operations/backup/state.js"; import { getBalanceDetail, getBalances } from "./operations/balance.js"; import { TaskIdentifiers, @@ -189,6 +190,7 @@ import { } from "./operations/deposits.js"; import { acceptExchangeTermsOfService, + addPresetExchangeEntry, downloadTosFromAcceptedFormat, getExchangeDetails, getExchangeRequestTimeout, @@ -314,6 +316,7 @@ import { getMaxPeerPushAmount, convertWithdrawalAmount, } from "./util/instructedAmountConversion.js"; +import { IDBFactory } from "@gnu-taler/idb-bridge"; const logger = new Logger("wallet.ts"); @@ -340,9 +343,8 @@ async function callOperationHandler( return await processRecoupGroup(ws, pending.recoupGroupId); case PendingTaskType.ExchangeCheckRefresh: return await autoRefresh(ws, pending.exchangeBaseUrl); - case PendingTaskType.Deposit: { + case PendingTaskType.Deposit: return await processDepositGroup(ws, pending.depositGroupId); - } case PendingTaskType.Backup: return await processBackupForProvider(ws, pending.backupProviderBaseUrl); case PendingTaskType.PeerPushDebit: @@ -533,6 +535,7 @@ async function fillDefaults(ws: InternalWalletState): Promise<void> { await tx.auditorTrust.put(c); } for (const baseUrl of ws.config.builtin.exchanges) { + await addPresetExchangeEntry(tx, baseUrl); const now = AbsoluteTime.now(); provideExchangeRecordInTx(ws, tx, baseUrl, now); } @@ -1021,6 +1024,23 @@ export async function getClientFromWalletState( return client; } +async function createStoredBackup( + ws: InternalWalletState, +): Promise<CreateStoredBackupResponse> { + const backup = await exportDb(ws.idb); + const backupsDb = await openStoredBackupsDatabase(ws.idb); + const name = `backup-${new Date().getTime()}`; + await backupsDb.mktxAll().runReadWrite(async (tx) => { + await tx.backupMeta.add({ + name, + }); + await tx.backupData.add(backup, name); + }); + return { + name, + }; +} + /** * Implementation of the "wallet-core" API. */ @@ -1037,6 +1057,14 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>( // FIXME: Can we make this more type-safe by using the request/response type // definitions we already have? switch (operation) { + case WalletApiOperation.CreateStoredBackup: + return createStoredBackup(ws); + case WalletApiOperation.DeleteStoredBackup: + return {}; + case WalletApiOperation.ListStoredBackups: + return {}; + case WalletApiOperation.RecoverStoredBackup: + return {}; case WalletApiOperation.InitWallet: { logger.trace("initializing wallet"); ws.initCalled = true; @@ -1378,9 +1406,6 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>( const req = codecForAcceptTipRequest().decode(payload); return await acceptTip(ws, req.walletRewardId); } - case WalletApiOperation.ExportBackupPlain: { - return exportBackup(ws); - } case WalletApiOperation.AddBackupProvider: { const req = codecForAddBackupProviderRequest().decode(payload); return await addBackupProvider(ws, req); @@ -1531,13 +1556,11 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>( await clearDatabase(ws.db.idbHandle()); return {}; case WalletApiOperation.Recycle: { - const backup = await exportBackup(ws); - await clearDatabase(ws.db.idbHandle()); - await importBackupPlain(ws, backup); + throw Error("not implemented"); return {}; } case WalletApiOperation.ExportDb: { - const dbDump = await exportDb(ws.db.idbHandle()); + const dbDump = await exportDb(ws.idb); return dbDump; } case WalletApiOperation.ImportDb: { @@ -1616,7 +1639,7 @@ export function getVersion(ws: InternalWalletState): WalletCoreVersion { /** * Handle a request to the wallet-core API. */ -export async function handleCoreApiRequest( +async function handleCoreApiRequest( ws: InternalWalletState, operation: string, id: string, @@ -1652,14 +1675,14 @@ export class Wallet { private _client: WalletCoreApiClient | undefined; private constructor( - db: DbAccess<typeof WalletStoresV1>, + idb: IDBFactory, http: HttpRequestLibrary, timer: TimerAPI, cryptoWorkerFactory: CryptoWorkerFactory, config?: WalletConfigParameter, ) { this.ws = new InternalWalletStateImpl( - db, + idb, http, timer, cryptoWorkerFactory, @@ -1675,21 +1698,20 @@ export class Wallet { } static async create( - db: DbAccess<typeof WalletStoresV1>, + idb: IDBFactory, http: HttpRequestLibrary, timer: TimerAPI, cryptoWorkerFactory: CryptoWorkerFactory, config?: WalletConfigParameter, ): Promise<Wallet> { - const w = new Wallet(db, http, timer, cryptoWorkerFactory, config); + const w = new Wallet(idb, http, timer, cryptoWorkerFactory, config); w._client = await getClientFromWalletState(w.ws); return w; } public static defaultConfig: Readonly<WalletConfig> = { builtin: { - //exchanges: ["https://exchange.demo.taler.net/"], - exchanges: [], + exchanges: ["https://exchange.demo.taler.net/"], auditors: [ { currency: "KUDOS", @@ -1724,19 +1746,22 @@ export class Wallet { this.ws.stop(); } - runPending(): Promise<void> { + async runPending(): Promise<void> { + await this.ws.ensureWalletDbOpen(); return runPending(this.ws); } - runTaskLoop(opts?: RetryLoopOpts): Promise<TaskLoopResult> { + async runTaskLoop(opts?: RetryLoopOpts): Promise<TaskLoopResult> { + await this.ws.ensureWalletDbOpen(); return runTaskLoop(this.ws, opts); } - handleCoreApiRequest( + async handleCoreApiRequest( operation: string, id: string, payload: unknown, ): Promise<CoreApiResponse> { + await this.ws.ensureWalletDbOpen(); return handleCoreApiRequest(this.ws, operation, id, payload); } } @@ -1800,12 +1825,17 @@ class InternalWalletStateImpl implements InternalWalletState { config: Readonly<WalletConfig>; + private _db: DbAccess<typeof WalletStoresV1> | undefined = undefined; + + get db(): DbAccess<typeof WalletStoresV1> { + if (!this._db) { + throw Error("db not initialized"); + } + return this._db; + } + constructor( - // FIXME: Make this a getter and make - // the actual value nullable. - // Check if we are in a DB migration / garbage collection - // and throw an error in that case. - public db: DbAccess<typeof WalletStoresV1>, + public idb: IDBFactory, public http: HttpRequestLibrary, public timer: TimerAPI, cryptoWorkerFactory: CryptoWorkerFactory, @@ -1820,6 +1850,17 @@ class InternalWalletStateImpl implements InternalWalletState { } } + async ensureWalletDbOpen(): Promise<void> { + if (this._db) { + return; + } + const myVersionChange = async (): Promise<void> => { + logger.info("version change requested for Taler DB"); + }; + const myDb = await openTalerDatabase(this.idb, myVersionChange); + this._db = myDb; + } + async getTransactionState( ws: InternalWalletState, tx: GetReadOnlyAccess<typeof WalletStoresV1>, |