diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index b94fd1547..ac3c79e20 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -1,6 +1,7 @@ import { Stores } from "./types/dbTypes"; import { openDatabase, Database, Store, Index } from "./util/query"; -import { IDBFactory, IDBDatabase } from "idb-bridge"; +import { IDBFactory, IDBDatabase, IDBObjectStore, IDBTransaction } from "idb-bridge"; +import { Logger } from './util/logging'; /** * Name of the Taler database. This is effectively the major @@ -17,7 +18,9 @@ const TALER_DB_NAME = "taler-wallet-prod-v1"; * backwards-compatible way or object stores and indices * are added. */ -export const WALLET_DB_MINOR_VERSION = 1; +export const WALLET_DB_MINOR_VERSION = 2; + +const logger = new Logger("db.ts"); /** * Return a promise that resolves @@ -31,24 +34,45 @@ export function openTalerDatabase( db: IDBDatabase, oldVersion: number, newVersion: number, + upgradeTransaction: IDBTransaction, ): void => { - switch (oldVersion) { - case 0: // DB does not exist yet - for (const n in Stores) { - if ((Stores as any)[n] instanceof Store) { - const si: Store = (Stores as any)[n]; - const s = db.createObjectStore(si.name, si.storeParams); - for (const indexName in si as any) { - if ((si as any)[indexName] instanceof Index) { - const ii: Index = (si as any)[indexName]; - s.createIndex(ii.indexName, ii.keyPath, ii.options); - } + if (oldVersion === 0) { + for (const n in Stores) { + if ((Stores as any)[n] instanceof Store) { + const si: Store = (Stores as any)[n]; + const s = db.createObjectStore(si.name, si.storeParams); + for (const indexName in si as any) { + if ((si as any)[indexName] instanceof Index) { + const ii: Index = (si as any)[indexName]; + s.createIndex(ii.indexName, ii.keyPath, ii.options); } } } - break; - default: - throw Error("unsupported existig DB version"); + } + return; + } + if (oldVersion === newVersion) { + return; + } + logger.info(`upgrading database from ${oldVersion} to ${newVersion}`); + for (const n in Stores) { + if ((Stores as any)[n] instanceof Store) { + const si: Store = (Stores as any)[n]; + let s: IDBObjectStore; + if ((si.storeParams?.versionAdded ?? 1) > oldVersion) { + s = db.createObjectStore(si.name, si.storeParams); + } else { + s = upgradeTransaction.objectStore(si.name); + } + for (const indexName in si as any) { + if ((si as any)[indexName] instanceof Index) { + const ii: Index = (si as any)[indexName]; + if ((ii.options?.versionAdded ?? 0) > oldVersion) { + s.createIndex(ii.indexName, ii.keyPath, ii.options); + } + } + } + } } }; diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts index 08f45eb89..3b8ac0269 100644 --- a/packages/taler-wallet-core/src/operations/tip.ts +++ b/packages/taler-wallet-core/src/operations/tip.ts @@ -46,6 +46,7 @@ import { Logger } from "../util/logging"; import { checkDbInvariant } from "../util/invariants"; import { TalerErrorCode } from "../TalerErrorCode"; import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries"; +import { j2s } from '../util/helpers'; const logger = new Logger("operations/tip.ts"); @@ -68,7 +69,7 @@ export async function prepareTip( merchantResp, codecForTipPickupGetResponse(), ); - logger.trace(`status ${tipPickupStatus}`); + logger.trace(`status ${j2s(tipPickupStatus)}`); const amount = Amounts.parseOrThrow(tipPickupStatus.tip_amount); @@ -80,6 +81,7 @@ export async function prepareTip( ]); if (!tipRecord) { + logger.trace("new tip, creating tip record"); await updateExchangeFromUrl(ws, tipPickupStatus.exchange_url); const withdrawDetails = await getExchangeWithdrawalInfo( ws, @@ -232,7 +234,7 @@ async function processTipImpl( })); const tipStatusUrl = new URL( - `/tips/${tipRecord.merchantTipId}/pickup`, + `tips/${tipRecord.merchantTipId}/pickup`, tipRecord.merchantBaseUrl, ); diff --git a/packages/taler-wallet-core/src/types/dbTypes.ts b/packages/taler-wallet-core/src/types/dbTypes.ts index 8f6c22f22..ed3a18ae1 100644 --- a/packages/taler-wallet-core/src/types/dbTypes.ts +++ b/packages/taler-wallet-core/src/types/dbTypes.ts @@ -361,7 +361,6 @@ export enum DenominationStatus { VerifiedBad, } - /** * Denomination record as stored in the wallet's database. */ @@ -640,7 +639,7 @@ export interface PlanchetRecord { /** * Public key of the reserve that this planchet * is being withdrawn from. - * + * * Can be the empty string (non-null/undefined for DB indexing) * if this is a tipping reserve. */ @@ -1532,7 +1531,6 @@ export enum ImportPayloadType { CoreSchema = "core-schema", } - class ExchangesStore extends Store { constructor() { super("exchanges", { keyPath: "baseUrl" }); @@ -1624,6 +1622,15 @@ class TipsStore extends Store { constructor() { super("tips", { keyPath: "walletTipId" }); } + // Added in version 2 + byMerchantTipIdAndBaseUrl = new Index<[string, string], TipRecord>( + this, + "tipsByMerchantTipIdAndOriginIndex", + ["merchantTipId", "merchantBaseUrl"], + { + versionAdded: 2, + } + ); } class WithdrawalGroupsStore extends Store { diff --git a/packages/taler-wallet-core/src/util/helpers.ts b/packages/taler-wallet-core/src/util/helpers.ts index ae4b0359e..570df441d 100644 --- a/packages/taler-wallet-core/src/util/helpers.ts +++ b/packages/taler-wallet-core/src/util/helpers.ts @@ -146,3 +146,7 @@ export function strcmp(s1: string, s2: string): number { } return 0; } + +export function j2s(x: any): string { + return JSON.stringify(x, undefined, 2); +} \ No newline at end of file diff --git a/packages/taler-wallet-core/src/util/query.ts b/packages/taler-wallet-core/src/util/query.ts index 6ebc3bbc4..f533c4cfd 100644 --- a/packages/taler-wallet-core/src/util/query.ts +++ b/packages/taler-wallet-core/src/util/query.ts @@ -44,14 +44,25 @@ const logger = new Logger("query.ts"); */ export const TransactionAbort = Symbol("transaction_abort"); +export interface StoreParams { + validator?: (v: T) => T; + autoIncrement?: boolean; + keyPath?: string | string[] | null; + + /** + * Database version that this store was added in, or + * undefined if added in the first version. + */ + versionAdded?: number; +} + /** * Definition of an object store. */ export class Store { constructor( public name: string, - public storeParams?: IDBObjectStoreParameters, - public validator?: (v: T) => T, + public storeParams?: StoreParams, ) {} } @@ -66,6 +77,12 @@ export interface IndexOptions { * Defaults to false. */ multiEntry?: boolean; + + /** + * Database version that this store was added in, or + * undefined if added in the first version. + */ + versionAdded?: number; } function requestToPromise(req: IDBRequest): Promise { @@ -425,6 +442,7 @@ export function openDatabase( db: IDBDatabase, oldVersion: number, newVersion: number, + upgradeTransaction: IDBTransaction, ) => void, ): Promise { return new Promise((resolve, reject) => { @@ -449,7 +467,11 @@ export function openDatabase( if (!newVersion) { throw Error("upgrade needed, but new version unknown"); } - onUpgradeNeeded(db, e.oldVersion, newVersion); + const transaction = req.transaction; + if (!transaction) { + throw Error("no transaction handle available in upgrade handler"); + } + onUpgradeNeeded(db, e.oldVersion, newVersion, transaction); }; }); } diff --git a/packages/taler-wallet-core/src/util/taleruri.ts b/packages/taler-wallet-core/src/util/taleruri.ts index 839d0b29f..ee055a32f 100644 --- a/packages/taler-wallet-core/src/util/taleruri.ts +++ b/packages/taler-wallet-core/src/util/taleruri.ts @@ -14,6 +14,7 @@ GNU Taler; see the file COPYING. If not, see */ +import { canonicalizeBaseUrl } from './helpers'; import { URLSearchParams } from "./url"; export interface PayUriResult { @@ -59,7 +60,7 @@ export function parseWithdrawUri(s: string): WithdrawUriResult | undefined { const p = [host, ...pathSegments].join("/"); return { - bankIntegrationApiBaseUrl: `${pi.innerProto}://${p}/`, + bankIntegrationApiBaseUrl: canonicalizeBaseUrl(`${pi.innerProto}://${p}/`), withdrawalOperationId: withdrawId, }; } @@ -155,7 +156,7 @@ export function parsePayUri(s: string): PayUriResult | undefined { const orderId = parts[parts.length - 2]; const pathSegments = parts.slice(1, parts.length - 2); const p = [host, ...pathSegments].join("/"); - const merchantBaseUrl = `${pi.innerProto}://${p}/`; + const merchantBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`); return { merchantBaseUrl, @@ -183,7 +184,7 @@ export function parseTipUri(s: string): TipUriResult | undefined { const tipId = parts[parts.length - 1]; const pathSegments = parts.slice(1, parts.length - 1); const p = [host, ...pathSegments].join("/"); - const merchantBaseUrl = `${pi.innerProto}://${p}/`; + const merchantBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`); return { merchantBaseUrl, @@ -210,7 +211,7 @@ export function parseRefundUri(s: string): RefundUriResult | undefined { const orderId = parts[parts.length - 2]; const pathSegments = parts.slice(1, parts.length - 2); const p = [host, ...pathSegments].join("/"); - const merchantBaseUrl = `${pi.innerProto}://${p}/`; + const merchantBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`); return { merchantBaseUrl, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f8b236858..5bca329ce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -233,7 +233,7 @@ importers: taler-wallet-core: 'workspace:*' tslib: ^2.0.1 typescript: ^3.9.7 -lockfileVersion: 5.1 +lockfileVersion: 5.2 packages: /@ava/typescript/1.1.1: dependencies: