diff options
-rw-r--r-- | src/db.ts | 119 | ||||
-rw-r--r-- | src/headless/helpers.ts | 9 | ||||
-rw-r--r-- | src/operations/balance.ts | 5 | ||||
-rw-r--r-- | src/operations/exchanges.ts | 34 | ||||
-rw-r--r-- | src/operations/history.ts | 5 | ||||
-rw-r--r-- | src/operations/pay.ts | 103 | ||||
-rw-r--r-- | src/operations/payback.ts | 16 | ||||
-rw-r--r-- | src/operations/pending.ts | 5 | ||||
-rw-r--r-- | src/operations/refresh.ts | 68 | ||||
-rw-r--r-- | src/operations/reserves.ts | 38 | ||||
-rw-r--r-- | src/operations/return.ts | 25 | ||||
-rw-r--r-- | src/operations/state.ts | 3 | ||||
-rw-r--r-- | src/operations/tip.ts | 22 | ||||
-rw-r--r-- | src/operations/withdraw.ts | 53 | ||||
-rw-r--r-- | src/util/query.ts | 302 | ||||
-rw-r--r-- | src/wallet.ts | 46 | ||||
-rw-r--r-- | src/webex/wxBackend.ts | 21 |
17 files changed, 389 insertions, 485 deletions
@@ -1,118 +1,27 @@ import { Stores, WALLET_DB_VERSION } from "./types/dbTypes"; -import { Store, Index } from "./util/query"; +import { openDatabase, Database } from "./util/query"; -const DB_NAME = "taler"; +const TALER_DB_NAME = "taler"; /** * Return a promise that resolves * to the taler wallet db. */ -export function openDatabase( +export function openTalerDatabase( idbFactory: IDBFactory, onVersionChange: () => void, onUpgradeUnsupported: (oldVersion: number, newVersion: number) => void, ): Promise<IDBDatabase> { - return new Promise<IDBDatabase>((resolve, reject) => { - const req = idbFactory.open(DB_NAME, WALLET_DB_VERSION); - req.onerror = e => { - console.log("taler database error", e); - reject(new Error("database error")); - }; - req.onsuccess = e => { - req.result.onversionchange = (evt: IDBVersionChangeEvent) => { - console.log( - `handling live db version change from ${evt.oldVersion} to ${evt.newVersion}`, - ); - req.result.close(); - onVersionChange(); - }; - resolve(req.result); - }; - req.onupgradeneeded = e => { - const db = req.result; - console.log( - `DB: upgrade needed: oldVersion=${e.oldVersion}, newVersion=${e.newVersion}`, - ); - switch (e.oldVersion) { - case 0: // DB does not exist yet - for (const n in Stores) { - if ((Stores as any)[n] instanceof Store) { - const si: Store<any> = (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<any, any> = (si as any)[indexName]; - s.createIndex(ii.indexName, ii.keyPath, ii.options); - } - } - } - } - break; - default: - if (e.oldVersion !== WALLET_DB_VERSION) { - onUpgradeUnsupported(e.oldVersion, WALLET_DB_VERSION); - throw Error("incompatible DB"); - } - break; - } - }; - }); + return openDatabase( + idbFactory, + TALER_DB_NAME, + WALLET_DB_VERSION, + Stores, + onVersionChange, + onUpgradeUnsupported, + ); } -export function exportDatabase(db: IDBDatabase): Promise<any> { - const dump = { - name: db.name, - stores: {} as { [s: string]: any }, - version: db.version, - }; - - return new Promise((resolve, reject) => { - const tx = db.transaction(Array.from(db.objectStoreNames)); - tx.addEventListener("complete", () => { - resolve(dump); - }); - // 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(); - } - }); - } - }); -} - -export function importDatabase(db: IDBDatabase, dump: any): Promise<void> { - console.log("importing db", dump); - return new Promise<void>((resolve, reject) => { - const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite"); - if (dump.stores) { - for (const storeName in dump.stores) { - const objects = []; - const dumpStore = dump.stores[storeName]; - for (const key in dumpStore) { - objects.push(dumpStore[key]); - } - console.log(`importing ${objects.length} records into ${storeName}`); - const store = tx.objectStore(storeName); - for (const obj of objects) { - store.put(obj); - } - } - } - tx.addEventListener("complete", () => { - resolve(); - }); - }); -} - -export function deleteDatabase(idbFactory: IDBFactory) { - idbFactory.deleteDatabase(DB_NAME); -} +export function deleteTalerDatabase(idbFactory: IDBFactory) { + Database.deleteDatabase(idbFactory, TALER_DB_NAME); +}
\ No newline at end of file diff --git a/src/headless/helpers.ts b/src/headless/helpers.ts index 2c0824a7d..33304cd03 100644 --- a/src/headless/helpers.ts +++ b/src/headless/helpers.ts @@ -23,7 +23,7 @@ */ import { Wallet } from "../wallet"; import { MemoryBackend, BridgeIDBFactory, shimIndexedDB } from "idb-bridge"; -import { openDatabase } from "../db"; +import { openTalerDatabase } from "../db"; import Axios, { AxiosPromise, AxiosResponse } from "axios"; import { HttpRequestLibrary, @@ -39,6 +39,7 @@ import { NodeThreadCryptoWorkerFactory } from "../crypto/workers/nodeThreadWorke import { SynchronousCryptoWorkerFactory } from "../crypto/workers/synchronousWorker"; import { RequestThrottler } from "../util/RequestThrottler"; import { WalletNotification, NotificationType } from "../types/notifications"; +import { Database } from "../util/query"; const logger = new Logger("helpers.ts"); @@ -191,7 +192,7 @@ export async function getDefaultNodeWallet( shimIndexedDB(myBridgeIdbFactory); - const myDb = await openDatabase( + const myDb = await openTalerDatabase( myIdbFactory, myVersionChange, myUnsupportedUpgrade, @@ -202,7 +203,9 @@ export async function getDefaultNodeWallet( const worker = new NodeThreadCryptoWorkerFactory(); - const w = new Wallet(myDb, myHttpLib, worker); + const dbWrap = new Database(myDb); + + const w = new Wallet(dbWrap, myHttpLib, worker); if (args.notifyHandler) { w.addNotificationListener(args.notifyHandler); } diff --git a/src/operations/balance.ts b/src/operations/balance.ts index 8c8a2a9cf..f5a51abec 100644 --- a/src/operations/balance.ts +++ b/src/operations/balance.ts @@ -18,7 +18,7 @@ * Imports. */ import { WalletBalance, WalletBalanceEntry } from "../types/walletTypes"; -import { runWithReadTransaction } from "../util/query"; +import { Database } from "../util/query"; import { InternalWalletState } from "./state"; import { Stores, TipRecord, CoinStatus } from "../types/dbTypes"; import * as Amounts from "../util/amounts"; @@ -73,8 +73,7 @@ export async function getBalances( byExchange: {}, }; - await runWithReadTransaction( - ws.db, + await ws.db.runWithReadTransaction( [Stores.coins, Stores.refresh, Stores.reserves, Stores.purchases, Stores.withdrawalSession], async tx => { await tx.iter(Stores.coins).forEach(c => { diff --git a/src/operations/exchanges.ts b/src/operations/exchanges.ts index 836bce6e4..6c4c1aa0c 100644 --- a/src/operations/exchanges.ts +++ b/src/operations/exchanges.ts @@ -32,10 +32,7 @@ import { extractTalerStampOrThrow, } from "../util/helpers"; import { - oneShotGet, - oneShotPut, - runWithWriteTransaction, - oneShotMutate, + Database } from "../util/query"; import * as Amounts from "../util/amounts"; import { parsePaytoUri } from "../util/payto"; @@ -81,7 +78,7 @@ async function setExchangeError( exchange.lastError = err; return exchange; }; - await oneShotMutate(ws.db, Stores.exchanges, baseUrl, mut); + await ws.db.mutate( Stores.exchanges, baseUrl, mut); } /** @@ -94,8 +91,7 @@ async function updateExchangeWithKeys( ws: InternalWalletState, baseUrl: string, ): Promise<void> { - const existingExchangeRecord = await oneShotGet( - ws.db, + const existingExchangeRecord = await ws.db.get( Stores.exchanges, baseUrl, ); @@ -180,8 +176,7 @@ async function updateExchangeWithKeys( ), ); - await runWithWriteTransaction( - ws.db, + await ws.db.runWithWriteTransaction( [Stores.exchanges, Stores.denominations], async tx => { const r = await tx.get(Stores.exchanges, baseUrl); @@ -222,7 +217,7 @@ async function updateExchangeWithTermsOfService( ws: InternalWalletState, exchangeBaseUrl: string, ) { - const exchange = await oneShotGet(ws.db, Stores.exchanges, exchangeBaseUrl); + const exchange = await ws.db.get(Stores.exchanges, exchangeBaseUrl); if (!exchange) { return; } @@ -243,7 +238,7 @@ async function updateExchangeWithTermsOfService( const tosText = await resp.text(); const tosEtag = resp.headers.get("etag") || undefined; - await runWithWriteTransaction(ws.db, [Stores.exchanges], async tx => { + await ws.db.runWithWriteTransaction([Stores.exchanges], async tx => { const r = await tx.get(Stores.exchanges, exchangeBaseUrl); if (!r) { return; @@ -263,7 +258,7 @@ export async function acceptExchangeTermsOfService( exchangeBaseUrl: string, etag: string | undefined, ) { - await runWithWriteTransaction(ws.db, [Stores.exchanges], async tx => { + await ws.db.runWithWriteTransaction([Stores.exchanges], async tx => { const r = await tx.get(Stores.exchanges, exchangeBaseUrl); if (!r) { return; @@ -283,7 +278,7 @@ async function updateExchangeWithWireInfo( ws: InternalWalletState, exchangeBaseUrl: string, ) { - const exchange = await oneShotGet(ws.db, Stores.exchanges, exchangeBaseUrl); + const exchange = await ws.db.get(Stores.exchanges, exchangeBaseUrl); if (!exchange) { return; } @@ -349,7 +344,7 @@ async function updateExchangeWithWireInfo( feesForType[wireMethod] = feeList; } - await runWithWriteTransaction(ws.db, [Stores.exchanges], async tx => { + await ws.db.runWithWriteTransaction([Stores.exchanges], async tx => { const r = await tx.get(Stores.exchanges, exchangeBaseUrl); if (!r) { return; @@ -392,7 +387,7 @@ async function updateExchangeFromUrlImpl( const now = getTimestampNow(); baseUrl = canonicalizeBaseUrl(baseUrl); - const r = await oneShotGet(ws.db, Stores.exchanges, baseUrl); + const r = await ws.db.get(Stores.exchanges, baseUrl); if (!r) { const newExchangeRecord: ExchangeRecord = { baseUrl: baseUrl, @@ -407,9 +402,9 @@ async function updateExchangeFromUrlImpl( termsOfServiceLastEtag: undefined, termsOfServiceText: undefined, }; - await oneShotPut(ws.db, Stores.exchanges, newExchangeRecord); + await ws.db.put(Stores.exchanges, newExchangeRecord); } else { - await runWithWriteTransaction(ws.db, [Stores.exchanges], async t => { + await ws.db.runWithWriteTransaction([Stores.exchanges], async t => { const rec = await t.get(Stores.exchanges, baseUrl); if (!rec) { return; @@ -431,7 +426,7 @@ async function updateExchangeFromUrlImpl( await updateExchangeWithWireInfo(ws, baseUrl); await updateExchangeWithTermsOfService(ws, baseUrl); - const updatedExchange = await oneShotGet(ws.db, Stores.exchanges, baseUrl); + const updatedExchange = await ws.db.get(Stores.exchanges, baseUrl); if (!updatedExchange) { // This should practically never happen @@ -453,8 +448,7 @@ export async function getExchangeTrust( if (!exchangeDetails) { throw Error(`exchange ${exchangeInfo.baseUrl} details not available`); } - const currencyRecord = await oneShotGet( - ws.db, + const currencyRecord = await ws.db.get( Stores.currencies, exchangeDetails.currency, ); diff --git a/src/operations/history.ts b/src/operations/history.ts index 9c4bb6a90..b8d756cc6 100644 --- a/src/operations/history.ts +++ b/src/operations/history.ts @@ -17,7 +17,7 @@ /** * Imports. */ -import { oneShotIter, runWithReadTransaction } from "../util/query"; +import { Database } from "../util/query"; import { InternalWalletState } from "./state"; import { Stores, TipRecord } from "../types/dbTypes"; import * as Amounts from "../util/amounts"; @@ -38,8 +38,7 @@ export async function getHistory( // This works as timestamps are guaranteed to be monotonically // increasing even - await runWithReadTransaction( - ws.db, + await ws.db.runWithReadTransaction( [ Stores.currencies, Stores.coins, diff --git a/src/operations/pay.ts b/src/operations/pay.ts index 08d227927..27f0e4404 100644 --- a/src/operations/pay.ts +++ b/src/operations/pay.ts @@ -36,13 +36,7 @@ import { OperationError, } from "../types/walletTypes"; import { - oneShotIter, - oneShotIterIndex, - oneShotGet, - runWithWriteTransaction, - oneShotPut, - oneShotGetIndexed, - oneShotMutate, + Database } from "../util/query"; import { Stores, @@ -202,7 +196,7 @@ async function getCoinsForPayment( let remainingAmount = paymentAmount; - const exchanges = await oneShotIter(ws.db, Stores.exchanges).toArray(); + const exchanges = await ws.db.iter(Stores.exchanges).toArray(); for (const exchange of exchanges) { let isOkay: boolean = false; @@ -242,14 +236,12 @@ async function getCoinsForPayment( continue; } - const coins = await oneShotIterIndex( - ws.db, + const coins = await ws.db.iterIndex( Stores.coins.exchangeBaseUrlIndex, exchange.baseUrl, ).toArray(); - const denoms = await oneShotIterIndex( - ws.db, + const denoms = await ws.db.iterIndex( Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl, ).toArray(); @@ -260,7 +252,7 @@ async function getCoinsForPayment( // Denomination of the first coin, we assume that all other // coins have the same currency - const firstDenom = await oneShotGet(ws.db, Stores.denominations, [ + const firstDenom = await ws.db.get(Stores.denominations, [ exchange.baseUrl, coins[0].denomPub, ]); @@ -270,7 +262,7 @@ async function getCoinsForPayment( const currency = firstDenom.value.currency; const cds: CoinWithDenom[] = []; for (const coin of coins) { - const denom = await oneShotGet(ws.db, Stores.denominations, [ + const denom = await ws.db.get(Stores.denominations, [ exchange.baseUrl, coin.denomPub, ]); @@ -377,8 +369,7 @@ async function recordConfirmPay( paymentSubmitPending: true, }; - await runWithWriteTransaction( - ws.db, + await ws.db.runWithWriteTransaction( [Stores.coins, Stores.purchases, Stores.proposals], async tx => { const p = await tx.get(Stores.proposals, proposal.proposalId); @@ -417,7 +408,7 @@ export async function abortFailedPayment( ws: InternalWalletState, proposalId: string, ): Promise<void> { - const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId); + const purchase = await ws.db.get(Stores.purchases, proposalId); if (!purchase) { throw Error("Purchase not found, unable to abort with refund"); } @@ -434,7 +425,7 @@ export async function abortFailedPayment( // From now on, we can't retry payment anymore, // so mark this in the DB in case the /pay abort // does not complete on the first try. - await oneShotPut(ws.db, Stores.purchases, purchase); + await ws.db.put(Stores.purchases, purchase); let resp; @@ -457,7 +448,7 @@ export async function abortFailedPayment( const refundResponse = MerchantRefundResponse.checked(await resp.json()); await acceptRefundResponse(ws, purchase.proposalId, refundResponse); - await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => { + await ws.db.runWithWriteTransaction([Stores.purchases], async tx => { const p = await tx.get(Stores.purchases, proposalId); if (!p) { return; @@ -472,7 +463,7 @@ async function incrementProposalRetry( proposalId: string, err: OperationError | undefined, ): Promise<void> { - await runWithWriteTransaction(ws.db, [Stores.proposals], async tx => { + await ws.db.runWithWriteTransaction([Stores.proposals], async tx => { const pr = await tx.get(Stores.proposals, proposalId); if (!pr) { return; @@ -494,7 +485,7 @@ async function incrementPurchasePayRetry( err: OperationError | undefined, ): Promise<void> { console.log("incrementing purchase pay retry with error", err); - await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => { + await ws.db.runWithWriteTransaction([Stores.purchases], async tx => { const pr = await tx.get(Stores.purchases, proposalId); if (!pr) { return; @@ -516,7 +507,7 @@ async function incrementPurchaseQueryRefundRetry( err: OperationError | undefined, ): Promise<void> { console.log("incrementing purchase refund query retry with error", err); - await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => { + await ws.db.runWithWriteTransaction([Stores.purchases], async tx => { const pr = await tx.get(Stores.purchases, proposalId); if (!pr) { return; @@ -538,7 +529,7 @@ async function incrementPurchaseApplyRefundRetry( err: OperationError | undefined, ): Promise<void> { console.log("incrementing purchase refund apply retry with error", err); - await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => { + await ws.db.runWithWriteTransaction([Stores.purchases], async tx => { const pr = await tx.get(Stores.purchases, proposalId); if (!pr) { return; @@ -571,7 +562,7 @@ async function resetDownloadProposalRetry( ws: InternalWalletState, proposalId: string, ) { - await oneShotMutate(ws.db, Stores.proposals, proposalId, x => { + await ws.db.mutate(Stores.proposals, proposalId, x => { if (x.retryInfo.active) { x.retryInfo = initRetryInfo(); } @@ -587,7 +578,7 @@ async function processDownloadProposalImpl( if (forceNow) { await resetDownloadProposalRetry(ws, proposalId); } - const proposal = await oneShotGet(ws.db, Stores.proposals, proposalId); + const proposal = await ws.db.get(Stores.proposals, proposalId); if (!proposal) { return; } @@ -621,8 +612,7 @@ async function processDownloadProposalImpl( const fulfillmentUrl = proposalResp.contract_terms.fulfillment_url; - await runWithWriteTransaction( - ws.db, + await ws.db.runWithWriteTransaction( [Stores.proposals, Stores.purchases], async tx => { const p = await tx.get(Stores.proposals, proposalId); @@ -677,8 +667,7 @@ async function startDownloadProposal( orderId: string, sessionId: string | undefined, ): Promise<string> { - const oldProposal = await oneShotGetIndexed( - ws.db, + const oldProposal = await ws.db.getIndexed( Stores.proposals.urlAndOrderIdIndex, [merchantBaseUrl, orderId], ); @@ -705,7 +694,7 @@ async function startDownloadProposal( downloadSessionId: sessionId, }; - await runWithWriteTransaction(ws.db, [Stores.proposals], async (tx) => { + await ws.db.runWithWriteTransaction([Stores.proposals], async (tx) => { const existingRecord = await tx.getIndexed(Stores.proposals.urlAndOrderIdIndex, [ merchantBaseUrl, orderId, @@ -725,7 +714,7 @@ export async function submitPay( ws: InternalWalletState, proposalId: string, ): Promise<ConfirmPayResult> { - const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId); + const purchase = await ws.db.get(Stores.purchases, proposalId); if (!purchase) { throw Error("Purchase not found: " + proposalId); } @@ -788,7 +777,7 @@ export async function submitPay( const modifiedCoins: CoinRecord[] = []; for (const pc of purchase.payReq.coins) { - const c = await oneShotGet(ws.db, Stores.coins, pc.coin_pub); + const c = await ws.db.get(Stores.coins, pc.coin_pub); if (!c) { console.error("coin not found"); throw Error("coin used in payment not found"); @@ -797,8 +786,7 @@ export async function submitPay( modifiedCoins.push(c); } - await runWithWriteTransaction( - ws.db, + await ws.db.runWithWriteTransaction( [Stores.coins, Stores.purchases], async tx => { for (let c of modifiedCoins) { @@ -849,7 +837,7 @@ export async function preparePay( uriResult.sessionId, ); - let proposal = await oneShotGet(ws.db, Stores.proposals, proposalId); + let proposal = await ws.db.get(Stores.proposals, proposalId); if (!proposal) { throw Error(`could not get proposal ${proposalId}`); } @@ -859,7 +847,7 @@ export async function preparePay( throw Error("invalid proposal state"); } console.log("using existing purchase for same product"); - proposal = await oneShotGet(ws.db, Stores.proposals, existingProposalId); + proposal = await ws.db.get(Stores.proposals, existingProposalId); if (!proposal) { throw Error("existing proposal is in wrong state"); } @@ -878,7 +866,7 @@ export async function preparePay( proposalId = proposal.proposalId; // First check if we already payed for it. - const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId); + const purchase = await ws.db.get(Stores.purchases, proposalId); if (!purchase) { const paymentAmount = Amounts.parseOrThrow(contractTerms.amount); @@ -966,7 +954,7 @@ async function getSpeculativePayData( const coinKeys = sp.payCoinInfo.updatedCoins.map(x => x.coinPub); const coins: CoinRecord[] = []; for (let coinKey of coinKeys) { - const cc = await oneShotGet(ws.db, Stores.coins, coinKey); + const cc = await ws.db.get(Stores.coins, coinKey); if (cc) { coins.push(cc); } @@ -997,7 +985,7 @@ export async function confirmPay( logger.trace( `executing confirmPay with proposalId ${proposalId} and sessionIdOverride ${sessionIdOverride}`, ); - const proposal = await oneShotGet(ws.db, Stores.proposals, proposalId); + const proposal = await ws.db.get(Stores.proposals, proposalId); if (!proposal) { throw Error(`proposal with id ${proposalId} not found`); @@ -1008,7 +996,7 @@ export async function confirmPay( throw Error("proposal is in invalid state"); } - let purchase = await oneShotGet(ws.db, Stores.purchases, d.contractTermsHash); + let purchase = await ws.db.get(Stores.purchases, d.contractTermsHash); if (purchase) { if ( @@ -1016,7 +1004,7 @@ export async function confirmPay( sessionIdOverride != purchase.lastSessionId ) { logger.trace(`changing session ID to ${sessionIdOverride}`); - await oneShotMutate(ws.db, Stores.purchases, purchase.proposalId, x => { + await ws.db.mutate(Stores.purchases, purchase.proposalId, x => { x.lastSessionId = sessionIdOverride; x.paymentSubmitPending = true; return x; @@ -1092,8 +1080,7 @@ export async function getFullRefundFees( if (refundPermissions.length === 0) { throw Error("no refunds given"); } - const coin0 = await oneShotGet( - ws.db, + const coin0 = await ws.db.get( Stores.coins, refundPermissions[0].coin_pub, ); @@ -1104,18 +1091,17 @@ export async function getFullRefundFees( Amounts.parseOrThrow(refundPermissions[0].refund_amount).currency, ); - const denoms = await oneShotIterIndex( - ws.db, + const denoms = await ws.db.iterIndex( Stores.denominations.exchangeBaseUrlIndex, coin0.exchangeBaseUrl, ).toArray(); for (const rp of refundPermissions) { - const coin = await oneShotGet(ws.db, Stores.coins, rp.coin_pub); + const coin = await ws.db.get(Stores.coins, rp.coin_pub); if (!coin) { throw Error("coin not found"); } - const denom = await oneShotGet(ws.db, Stores.denominations, [ + const denom = await ws.db.get(Stores.denominations, [ coin0.exchangeBaseUrl, coin.denomPub, ]); @@ -1147,7 +1133,7 @@ async function acceptRefundResponse( let numNewRefunds = 0; - await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => { + await ws.db.runWithWriteTransaction([Stores.purchases], async tx => { const p = await tx.get(Stores.purchases, proposalId); if (!p) { console.error("purchase not found, not adding refunds"); @@ -1215,8 +1201,7 @@ async function startRefundQuery( ws: InternalWalletState, proposalId: string, ): Promise<void> { - const success = await runWithWriteTransaction( - ws.db, + const success = await ws.db.runWithWriteTransaction( [Stores.purchases], async tx => { const p = await tx.get(Stores.purchases, proposalId); @@ -1259,8 +1244,7 @@ export async function applyRefund( throw Error("invalid refund URI"); } - const purchase = await oneShotGetIndexed( - ws.db, + const purchase = await ws.db.getIndexed( Stores.purchases.orderIdIndex, [parseResult.merchantBaseUrl, parseResult.orderId], ); @@ -1292,7 +1276,7 @@ async function resetPurchasePayRetry( ws: InternalWalletState, proposalId: string, ) { - await oneShotMutate(ws.db, Stores.purchases, proposalId, x => { + await ws.db.mutate(Stores.purchases, proposalId, x => { if (x.payRetryInfo.active) { x.payRetryInfo = initRetryInfo(); } @@ -1308,7 +1292,7 @@ async function processPurchasePayImpl( if (forceNow) { await resetPurchasePayRetry(ws, proposalId); } - const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId); + const purchase = await ws.db.get(Stores.purchases, proposalId); if (!purchase) { return; } @@ -1336,7 +1320,7 @@ async function resetPurchaseQueryRefundRetry( ws: InternalWalletState, proposalId: string, ) { - await oneShotMutate(ws.db, Stores.purchases, proposalId, x => { + await ws.db.mutate(Stores.purchases, proposalId, x => { if (x.refundStatusRetryInfo.active) { x.refundStatusRetryInfo = initRetryInfo(); } @@ -1352,7 +1336,7 @@ async function processPurchaseQueryRefundImpl( if (forceNow) { await resetPurchaseQueryRefundRetry(ws, proposalId); } - const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId); + const purchase = await ws.db.get(Stores.purchases, proposalId); if (!purchase) { return; } @@ -1398,7 +1382,7 @@ async function resetPurchaseApplyRefundRetry( ws: InternalWalletState, proposalId: string, ) { - await oneShotMutate(ws.db, Stores.purchases, proposalId, x => { + await ws.db.mutate(Stores.purchases, proposalId, x => { if (x.refundApplyRetryInfo.active) { x.refundApplyRetryInfo = initRetryInfo(); } @@ -1414,7 +1398,7 @@ async function processPurchaseApplyRefundImpl( if (forceNow) { await resetPurchaseApplyRefundRetry(ws, proposalId); } - const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId); + const purchase = await ws.db.get(Stores.purchases, proposalId); if (!purchase) { console.error("not submitting refunds, payment not found:"); return; @@ -1448,8 +1432,7 @@ async function processPurchaseApplyRefundImpl( let allRefundsProcessed = false; - await runWithWriteTransaction( - ws.db, + await ws.db.runWithWriteTransaction( [Stores.purchases, Stores.coins], async tx => { const p = await tx.get(Stores.purchases, proposalId); diff --git a/src/operations/payback.ts b/src/operations/payback.ts index 2d8a72839..51adb6ad3 100644 --- a/src/operations/payback.ts +++ b/src/operations/payback.ts @@ -18,10 +18,7 @@ * Imports. */ import { - oneShotIter, - runWithWriteTransaction, - oneShotGet, - oneShotPut, + Database } from "../util/query"; import { InternalWalletState } from "./state"; import { Stores, TipRecord, CoinStatus } from "../types/dbTypes"; @@ -37,7 +34,7 @@ export async function payback( ws: InternalWalletState, coinPub: string, ): Promise<void> { - let coin = await oneShotGet(ws.db, Stores.coins, coinPub); + let coin = await ws.db.get(Stores.coins, coinPub); if (!coin) { throw Error(`Coin ${coinPub} not found, can't request payback`); } @@ -45,7 +42,7 @@ export async function payback( if (!reservePub) { throw Error(`Can't request payback for a refreshed coin`); } - const reserve = await oneShotGet(ws.db, Stores.reserves, reservePub); + const reserve = await ws.db.get(Stores.reserves, reservePub); if (!reserve) { throw Error(`Reserve of coin ${coinPub} not found`); } @@ -58,8 +55,7 @@ export async function payback( // technically we might update reserve status before we get the response // from the reserve for the payback request. reserve.hasPayback = true; - await runWithWriteTransaction( - ws.db, + await ws.db.runWithWriteTransaction( [Stores.coins, Stores.reserves], async tx => { await tx.put(Stores.coins, coin!!); @@ -80,12 +76,12 @@ export async function payback( if (paybackConfirmation.reserve_pub !== coin.reservePub) { throw Error(`Coin's reserve doesn't match reserve on payback`); } - coin = await oneShotGet(ws.db, Stores.coins, coinPub); + coin = await ws.db.get(Stores.coins, coinPub); if (!coin) { throw Error(`Coin ${coinPub} not found, can't confirm payback`); } coin.status = CoinStatus.Dormant; - await oneShotPut(ws.db, Stores.coins, coin); + await ws.db.put(Stores.coins, coin); ws.notify({ type: NotificationType.PaybackFinished, }); diff --git a/src/operations/pending.ts b/src/operations/pending.ts index b9fc1d203..13859c64b 100644 --- a/src/operations/pending.ts +++ b/src/operations/pending.ts @@ -22,7 +22,7 @@ import { Timestamp, Duration, } from "../types/walletTypes"; -import { runWithReadTransaction, TransactionHandle } from "../util/query"; +import { Database, TransactionHandle } from "../util/query"; import { InternalWalletState } from "./state"; import { Stores, @@ -425,8 +425,7 @@ export async function getPendingOperations( pendingOperations: [], }; const now = getTimestampNow(); - await runWithReadTransaction( - ws.db, + await ws.db.runWithReadTransaction( [ Stores.exchanges, Stores.reserves, diff --git a/src/operations/refresh.ts b/src/operations/refresh.ts index 4e4449d96..4ffc3ea60 100644 --- a/src/operations/refresh.ts +++ b/src/operations/refresh.ts @@ -27,21 +27,12 @@ import { updateRetryInfoTimeout, } from "../types/dbTypes"; import { amountToPretty } from "../util/helpers"; -import { - oneShotGet, - oneShotMutate, - runWithWriteTransaction, - TransactionAbort, - oneShotIterIndex, -} from "../util/query"; +import { Database } from "../util/query"; import { InternalWalletState } from "./state"; import { Logger } from "../util/logging"; import { getWithdrawDenomList } from "./withdraw"; import { updateExchangeFromUrl } from "./exchanges"; -import { - getTimestampNow, - OperationError, -} from "../types/walletTypes"; +import { getTimestampNow, OperationError } from "../types/walletTypes"; import { guardOperationException } from "./errors"; import { NotificationType } from "../types/notifications"; @@ -84,11 +75,7 @@ async function refreshMelt( ws: InternalWalletState, refreshSessionId: string, ): Promise<void> { - const refreshSession = await oneShotGet( - ws.db, - Stores.refresh, - refreshSessionId, - ); + const refreshSession = await ws.db.get(Stores.refresh, refreshSessionId); if (!refreshSession) { return; } @@ -96,11 +83,7 @@ async function refreshMelt( return; } - const coin = await oneShotGet( - ws.db, - Stores.coins, - refreshSession.meltCoinPub, - ); + const coin = await ws.db.get(Stores.coins, refreshSession.meltCoinPub); if (!coin) { console.error("can't melt coin, it does not exist"); @@ -139,7 +122,7 @@ async function refreshMelt( refreshSession.norevealIndex = norevealIndex; - await oneShotMutate(ws.db, Stores.refresh, refreshSessionId, rs => { + await ws.db.mutate(Stores.refresh, refreshSessionId, rs => { if (rs.norevealIndex !== undefined) { return; } @@ -159,11 +142,7 @@ async function refreshReveal( ws: InternalWalletState, refreshSessionId: string, ): Promise<void> { - const refreshSession = await oneShotGet( - ws.db, - Stores.refresh, - refreshSessionId, - ); + const refreshSession = await ws.db.get(Stores.refresh, refreshSessionId); if (!refreshSession) { return; } @@ -179,8 +158,7 @@ async function refreshReveal( throw Error("refresh index error"); } - const meltCoinRecord = await oneShotGet( - ws.db, + const meltCoinRecord = await ws.db.get( Stores.coins, refreshSession.meltCoinPub, ); @@ -241,7 +219,7 @@ async function refreshReveal( const coins: CoinRecord[] = []; for (let i = 0; i < respJson.ev_sigs.length; i++) { - const denom = await oneShotGet(ws.db, Stores.denominations, [ + const denom = await ws.db.get(Stores.denominations, [ refreshSession.exchangeBaseUrl, refreshSession.newDenoms[i], ]); @@ -274,8 +252,7 @@ async function refreshReveal( coins.push(coin); } - await runWithWriteTransaction( - ws.db, + await ws.db.runWithWriteTransaction( [Stores.coins, Stores.refresh], async tx => { const rs = await tx.get(Stores.refresh, refreshSessionId); @@ -306,7 +283,7 @@ async function incrementRefreshRetry( refreshSessionId: string, err: OperationError | undefined, ): Promise<void> { - await runWithWriteTransaction(ws.db, [Stores.refresh], async tx => { + await ws.db.runWithWriteTransaction([Stores.refresh], async tx => { const r = await tx.get(Stores.refresh, refreshSessionId); if (!r) { return; @@ -341,7 +318,7 @@ async function resetRefreshSessionRetry( ws: InternalWalletState, refreshSessionId: string, ) { - await oneShotMutate(ws.db, Stores.refresh, refreshSessionId, (x) => { + await ws.db.mutate(Stores.refresh, refreshSessionId, x => { if (x.retryInfo.active) { x.retryInfo = initRetryInfo(); } @@ -357,11 +334,7 @@ async function processRefreshSessionImpl( if (forceNow) { await resetRefreshSessionRetry(ws, refreshSessionId); } - const refreshSession = await oneShotGet( - ws.db, - Stores.refresh, - refreshSessionId, - ); + const refreshSession = await ws.db.get(Stores.refresh, refreshSessionId); if (!refreshSession) { return; } @@ -380,7 +353,7 @@ export async function refresh( oldCoinPub: string, force: boolean = false, ): Promise<void> { - const coin = await oneShotGet(ws.db, Stores.coins, oldCoinPub); + const coin = await ws.db.get(Stores.coins, oldCoinPub); if (!coin) { console.warn("can't refresh, coin not in database"); return; @@ -402,7 +375,7 @@ export async function refresh( throw Error("db inconsistent: exchange of coin not found"); } - const oldDenom = await oneShotGet(ws.db, Stores.denominations, [ + const oldDenom = await ws.db.get(Stores.denominations, [ exchange.baseUrl, coin.denomPub, ]); @@ -411,11 +384,9 @@ export async function refresh( throw Error("db inconsistent: denomination for coin not found"); } - const availableDenoms: DenominationRecord[] = await oneShotIterIndex( - ws.db, - Stores.denominations.exchangeBaseUrlIndex, - exchange.baseUrl, - ).toArray(); + const availableDenoms: DenominationRecord[] = await ws.db + .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl) + .toArray(); const availableAmount = Amounts.sub(coin.currentAmount, oldDenom.feeRefresh) .amount; @@ -428,7 +399,7 @@ export async function refresh( availableAmount, )} too small`, ); - await oneShotMutate(ws.db, Stores.coins, oldCoinPub, x => { + await ws.db.mutate(Stores.coins, oldCoinPub, x => { if (x.status != coin.status) { // Concurrent modification? return; @@ -450,8 +421,7 @@ export async function refresh( // Store refresh session and subtract refreshed amount from // coin in the same transaction. - await runWithWriteTransaction( - ws.db, + await ws.db.runWithWriteTransaction( [Stores.refresh, Stores.coins], async tx => { const c = await tx.get(Stores.coins, coin.coinPub); diff --git a/src/operations/reserves.ts b/src/operations/reserves.ts index 5ad13a67a..215d5ba7d 100644 --- a/src/operations/reserves.ts +++ b/src/operations/reserves.ts @@ -33,10 +33,7 @@ import { updateRetryInfoTimeout, } from "../types/dbTypes"; import { - oneShotMutate, - oneShotPut, - oneShotGet, - runWithWriteTransaction, + Database, TransactionAbort, } from "../util/query"; import { Logger } from "../util/logging"; @@ -104,7 +101,7 @@ export async function createReserve( const rec = { paytoUri: senderWire, }; - await oneShotPut(ws.db, Stores.senderWires, rec); + await ws.db.put(Stores.senderWires, rec); } const exchangeInfo = await updateExchangeFromUrl(ws, req.exchange); @@ -114,8 +111,7 @@ export async function createReserve( throw Error("exchange not updated"); } const { isAudited, isTrusted } = await getExchangeTrust(ws, exchangeInfo); - let currencyRecord = await oneShotGet( - ws.db, + let currencyRecord = await ws.db.get( Stores.currencies, exchangeDetails.currency, ); @@ -137,8 +133,7 @@ export async function createReserve( const cr: CurrencyRecord = currencyRecord; - const resp = await runWithWriteTransaction( - ws.db, + const resp = await ws.db.runWithWriteTransaction( [Stores.currencies, Stores.reserves, Stores.bankWithdrawUris], async tx => { // Check if we have already created a reserve for that bankWithdrawStatusUrl @@ -212,7 +207,7 @@ async function registerReserveWithBank( ws: InternalWalletState, reservePub: string, ): Promise<void> { - let reserve = await oneShotGet(ws.db, Stores.reserves, reservePub); + let reserve = await ws.db.get(Stores.reserves, reservePub); switch (reserve?.reserveStatus) { case ReserveRecordStatus.WAIT_CONFIRM_BANK: case ReserveRecordStatus.REGISTERING_BANK: @@ -233,7 +228,7 @@ async function registerReserveWithBank( selected_exchange: reserve.exchangeWire, }); console.log("got response", bankResp); - await oneShotMutate(ws.db, Stores.reserves, reservePub, r => { + await ws.db.mutate(Stores.reserves, reservePub, r => { switch (r.reserveStatus) { case ReserveRecordStatus.REGISTERING_BANK: case ReserveRecordStatus.WAIT_CONFIRM_BANK: @@ -266,7 +261,7 @@ async function processReserveBankStatusImpl( ws: InternalWalletState, reservePub: string, ): Promise<void> { - let reserve = await oneShotGet(ws.db, Stores.reserves, reservePub); + let reserve = await ws.db.get(Stores.reserves, reservePub); switch (reserve?.reserveStatus) { case ReserveRecordStatus.WAIT_CONFIRM_BANK: case ReserveRecordStatus.REGISTERING_BANK: @@ -303,7 +298,7 @@ async function processReserveBankStatusImpl( } if (status.transfer_done) { - await oneShotMutate(ws.db, Stores.reserves, reservePub, r => { + await ws.db.mutate(Stores.reserves, reservePub, r => { switch (r.reserveStatus) { case ReserveRecordStatus.REGISTERING_BANK: case ReserveRecordStatus.WAIT_CONFIRM_BANK: @@ -319,7 +314,7 @@ async function processReserveBankStatusImpl( }); await processReserveImpl(ws, reservePub, true); } else { - await oneShotMutate(ws.db, Stores.reserves, reservePub, r => { + await ws.db.mutate(Stores.reserves, reservePub, r => { switch (r.reserveStatus) { case ReserveRecordStatus.WAIT_CONFIRM_BANK: break; @@ -339,7 +334,7 @@ async function incrementReserveRetry( reservePub: string, err: OperationError | undefined, ): Promise<void> { - await runWithWriteTransaction(ws.db, [Stores.reserves], async tx => { + await ws.db.runWithWriteTransaction([Stores.reserves], async tx => { const r = await tx.get(Stores.reserves, reservePub); if (!r) { return; @@ -363,7 +358,7 @@ async function updateReserve( ws: InternalWalletState, reservePub: string, ): Promise<void> { - const reserve = await oneShotGet(ws.db, Stores.reserves, reservePub); + const reserve = await ws.db.get(Stores.reserves, reservePub); if (!reserve) { throw Error("reserve not in db"); } @@ -400,7 +395,7 @@ async function updateReserve( } const reserveInfo = ReserveStatus.checked(await resp.json()); const balance = Amounts.parseOrThrow(reserveInfo.balance); - await oneShotMutate(ws.db, Stores.reserves, reserve.reservePub, r => { + await ws.db.mutate(Stores.reserves, reserve.reservePub, r => { if (r.reserveStatus !== ReserveRecordStatus.QUERYING_STATUS) { return; } @@ -442,7 +437,7 @@ async function processReserveImpl( reservePub: string, forceNow: boolean = false, ): Promise<void> { - const reserve = await oneShotGet(ws.db, Stores.reserves, reservePub); + const reserve = await ws.db.get(Stores.reserves, reservePub); if (!reserve) { console.log("not processing reserve: reserve does not exist"); return; @@ -488,7 +483,7 @@ export async function confirmReserve( req: ConfirmReserveRequest, ): Promise<void> { const now = getTimestampNow(); - await oneShotMutate(ws.db, Stores.reserves, req.reservePub, reserve => { + await ws.db.mutate(Stores.reserves, req.reservePub, reserve => { if (reserve.reserveStatus !== ReserveRecordStatus.UNCONFIRMED) { return; } @@ -515,7 +510,7 @@ async function depleteReserve( ws: InternalWalletState, reservePub: string, ): Promise<void> { - const reserve = await oneShotGet(ws.db, Stores.reserves, reservePub); + const reserve = await ws.db.get(Stores.reserves, reservePub); if (!reserve) { return; } @@ -600,8 +595,7 @@ async function depleteReserve( return r; } - const success = await runWithWriteTransaction( - ws.db, + const success = await ws.db.runWithWriteTransaction( [Stores.withdrawalSession, Stores.reserves], async tx => { const myReserve = await tx.get(Stores.reserves, reservePub); diff --git a/src/operations/return.ts b/src/operations/return.ts index 74885a735..01d2802d9 100644 --- a/src/operations/return.ts +++ b/src/operations/return.ts @@ -21,7 +21,7 @@ import { ReturnCoinsRequest, CoinWithDenom, } from "../types/walletTypes"; -import { runWithWriteTransaction, oneShotGet, oneShotIterIndex, oneShotPut } from "../util/query"; +import { Database } from "../util/query"; import { InternalWalletState } from "./state"; import { Stores, TipRecord, CoinStatus, CoinsReturnRecord, CoinRecord } from "../types/dbTypes"; import * as Amounts from "../util/amounts"; @@ -38,8 +38,7 @@ async function getCoinsForReturn( exchangeBaseUrl: string, amount: AmountJson, ): Promise<CoinWithDenom[] | undefined> { - const exchange = await oneShotGet( - ws.db, + const exchange = await ws.db.get( Stores.exchanges, exchangeBaseUrl, ); @@ -47,8 +46,7 @@ async function getCoinsForReturn( throw Error(`Exchange ${exchangeBaseUrl} not known to the wallet`); } - const coins: CoinRecord[] = await oneShotIterIndex( - ws.db, + const coins: CoinRecord[] = await ws.db.iterIndex( Stores.coins.exchangeBaseUrlIndex, exchange.baseUrl, ).toArray(); @@ -57,15 +55,14 @@ async function getCoinsForReturn( return []; } - const denoms = await oneShotIterIndex( - ws.db, + const denoms = await ws.db.iterIndex( Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl, ).toArray(); // Denomination of the first coin, we assume that all other // coins have the same currency - const firstDenom = await oneShotGet(ws.db, Stores.denominations, [ + const firstDenom = await ws.db.get(Stores.denominations, [ exchange.baseUrl, coins[0].denomPub, ]); @@ -76,7 +73,7 @@ async function getCoinsForReturn( const cds: CoinWithDenom[] = []; for (const coin of coins) { - const denom = await oneShotGet(ws.db, Stores.denominations, [ + const denom = await ws.db.get(Stores.denominations, [ exchange.baseUrl, coin.denomPub, ]); @@ -121,7 +118,7 @@ export async function returnCoins( return; } const stampSecNow = Math.floor(new Date().getTime() / 1000); - const exchange = await oneShotGet(ws.db, Stores.exchanges, req.exchange); + const exchange = await ws.db.get(Stores.exchanges, req.exchange); if (!exchange) { console.error(`Exchange ${req.exchange} not known to the wallet`); return; @@ -190,8 +187,7 @@ export async function returnCoins( wire: req.senderWire, }; - await runWithWriteTransaction( - ws.db, + await ws.db.runWithWriteTransaction( [Stores.coinsReturns, Stores.coins], async tx => { await tx.put(Stores.coinsReturns, coinsReturnRecord); @@ -248,8 +244,7 @@ async function depositReturnedCoins( // FIXME: verify signature // For every successful deposit, we replace the old record with an updated one - const currentCrr = await oneShotGet( - ws.db, + const currentCrr = await ws.db.get( Stores.coinsReturns, coinsReturnRecord.contractTermsHash, ); @@ -262,6 +257,6 @@ async function depositReturnedCoins( nc.depositedSig = respJson.sig; } } - await oneShotPut(ws.db, Stores.coinsReturns, currentCrr); + await ws.db.put(Stores.coinsReturns, currentCrr); } } diff --git a/src/operations/state.ts b/src/operations/state.ts index 47bf40de3..1e4b90360 100644 --- a/src/operations/state.ts +++ b/src/operations/state.ts @@ -25,6 +25,7 @@ import { AsyncOpMemoMap, AsyncOpMemoSingle } from "../util/asyncMemo"; import { Logger } from "../util/logging"; import { PendingOperationsResponse } from "../types/pending"; import { WalletNotification } from "../types/notifications"; +import { Database } from "../util/query"; type NotificationListener = (n: WalletNotification) => void; @@ -45,7 +46,7 @@ export class InternalWalletState { listeners: NotificationListener[] = []; constructor( - public db: IDBDatabase, + public db: Database, public http: HttpRequestLibrary, cryptoWorkerFactory: CryptoWorkerFactory, ) { diff --git a/src/operations/tip.ts b/src/operations/tip.ts index 0a710f67e..f723374f9 100644 --- a/src/operations/tip.ts +++ b/src/operations/tip.ts @@ -15,7 +15,7 @@ */ -import { oneShotGet, oneShotPut, oneShotMutate, runWithWriteTransaction } from "../util/query"; +import { Database } from "../util/query"; import { InternalWalletState } from "./state"; import { parseTipUri } from "../util/taleruri"; import { TipStatus, getTimestampNow, OperationError } from "../types/walletTypes"; @@ -53,7 +53,7 @@ export async function getTipStatus( let amount = Amounts.parseOrThrow(tipPickupStatus.amount); - let tipRecord = await oneShotGet(ws.db, Stores.tips, [ + let tipRecord = await ws.db.get(Stores.tips, [ res.merchantTipId, res.merchantOrigin, ]); @@ -87,7 +87,7 @@ export async function getTipStatus( retryInfo: initRetryInfo(), lastError: undefined, }; - await oneShotPut(ws.db, Stores.tips, tipRecord); + await ws.db.put(Stores.tips, tipRecord); } const tipStatus: TipStatus = { @@ -112,7 +112,7 @@ async function incrementTipRetry( refreshSessionId: string, err: OperationError | undefined, ): Promise<void> { - await runWithWriteTransaction(ws.db, [Stores.tips], async tx => { + await ws.db.runWithWriteTransaction([Stores.tips], async tx => { const t = await tx.get(Stores.tips, refreshSessionId); if (!t) { return; @@ -141,7 +141,7 @@ async function resetTipRetry( ws: InternalWalletState, tipId: string, ): Promise<void> { - await oneShotMutate(ws.db, Stores.tips, tipId, (x) => { + await ws.db.mutate(Stores.tips, tipId, (x) => { if (x.retryInfo.active) { x.retryInfo = initRetryInfo(); } @@ -157,7 +157,7 @@ async function processTipImpl( if (forceNow) { await resetTipRetry(ws, tipId); } - let tipRecord = await oneShotGet(ws.db, Stores.tips, tipId); + let tipRecord = await ws.db.get(Stores.tips, tipId); if (!tipRecord) { return; } @@ -179,7 +179,7 @@ async function processTipImpl( denomsForWithdraw.map(d => ws.cryptoApi.createTipPlanchet(d)), ); - await oneShotMutate(ws.db, Stores.tips, tipId, r => { + await ws.db.mutate(Stores.tips, tipId, r => { if (!r.planchets) { r.planchets = planchets; } @@ -187,7 +187,7 @@ async function processTipImpl( }); } - tipRecord = await oneShotGet(ws.db, Stores.tips, tipId); + tipRecord = await ws.db.get(Stores.tips, tipId); if (!tipRecord) { throw Error("tip not in database"); } @@ -267,7 +267,7 @@ async function processTipImpl( }; - await runWithWriteTransaction(ws.db, [Stores.tips, Stores.withdrawalSession], async (tx) => { + await ws.db.runWithWriteTransaction([Stores.tips, Stores.withdrawalSession], async (tx) => { const tr = await tx.get(Stores.tips, tipId); if (!tr) { return; @@ -291,14 +291,14 @@ export async function acceptTip( ws: InternalWalletState, tipId: string, ): Promise<void> { - const tipRecord = await oneShotGet(ws.db, Stores.tips, tipId); + const tipRecord = await ws.db.get(Stores.tips, tipId); if (!tipRecord) { console.log("tip not found"); return; } tipRecord.accepted = true; - await oneShotPut(ws.db, Stores.tips, tipRecord); + await ws.db.put(Stores.tips, tipRecord); await processTip(ws, tipId); return; diff --git a/src/operations/withdraw.ts b/src/operations/withdraw.ts index 4ecc321f8..a34eec5a1 100644 --- a/src/operations/withdraw.ts +++ b/src/operations/withdraw.ts @@ -39,12 +39,7 @@ import { InternalWalletState } from "./state"; import { parseWithdrawUri } from "../util/taleruri"; import { Logger } from "../util/logging"; import { - oneShotGet, - oneShotPut, - oneShotIterIndex, - oneShotGetIndexed, - runWithWriteTransaction, - oneShotMutate, + Database } from "../util/query"; import { updateExchangeFromUrl, @@ -167,8 +162,7 @@ async function getPossibleDenoms( ws: InternalWalletState, exchangeBaseUrl: string, ): Promise<DenominationRecord[]> { - return await oneShotIterIndex( - ws.db, + return await ws.db.iterIndex( Stores.denominations.exchangeBaseUrlIndex, exchangeBaseUrl, ).filter(d => { @@ -187,8 +181,7 @@ async function processPlanchet( withdrawalSessionId: string, coinIdx: number, ): Promise<void> { - const withdrawalSession = await oneShotGet( - ws.db, + const withdrawalSession = await ws.db.get( Stores.withdrawalSession, withdrawalSessionId, ); @@ -205,8 +198,7 @@ async function processPlanchet( console.log("processPlanchet: planchet not found"); return; } - const exchange = await oneShotGet( - ws.db, + const exchange = await ws.db.get( Stores.exchanges, withdrawalSession.exchangeBaseUrl, ); @@ -215,7 +207,7 @@ async function processPlanchet( return; } - const denom = await oneShotGet(ws.db, Stores.denominations, [ + const denom = await ws.db.get(Stores.denominations, [ withdrawalSession.exchangeBaseUrl, planchet.denomPub, ]); @@ -268,8 +260,7 @@ async function processPlanchet( let withdrawSessionFinished = false; let reserveDepleted = false; - const success = await runWithWriteTransaction( - ws.db, + const success = await ws.db.runWithWriteTransaction( [Stores.coins, Stores.withdrawalSession, Stores.reserves], async tx => { const ws = await tx.get(Stores.withdrawalSession, withdrawalSessionId); @@ -346,7 +337,7 @@ export async function getVerifiedWithdrawDenomList( exchangeBaseUrl: string, amount: AmountJson, ): Promise<DenominationRecord[]> { - const exchange = await oneShotGet(ws.db, Stores.exchanges, exchangeBaseUrl); + const exchange = await ws.db.get(Stores.exchanges, exchangeBaseUrl); if (!exchange) { console.log("exchange not found"); throw Error(`exchange ${exchangeBaseUrl} not found`); @@ -391,7 +382,7 @@ export async function getVerifiedWithdrawDenomList( denom.status = DenominationStatus.VerifiedGood; nextPossibleDenoms.push(denom); } - await oneShotPut(ws.db, Stores.denominations, denom); + await ws.db.put(Stores.denominations, denom); } else { nextPossibleDenoms.push(denom); } @@ -408,8 +399,7 @@ async function makePlanchet( withdrawalSessionId: string, coinIndex: number, ): Promise<void> { - const withdrawalSession = await oneShotGet( - ws.db, + const withdrawalSession = await ws.db.get( Stores.withdrawalSession, withdrawalSessionId, ); @@ -420,11 +410,11 @@ async function makePlanchet( if (src.type !== "reserve") { throw Error("invalid state"); } - const reserve = await oneShotGet(ws.db, Stores.reserves, src.reservePub); + const reserve = await ws.db.get(Stores.reserves, src.reservePub); if (!reserve) { return; } - const denom = await oneShotGet(ws.db, Stores.denominations, [ + const denom = await ws.db.get(Stores.denominations, [ withdrawalSession.exchangeBaseUrl, withdrawalSession.denoms[coinIndex], ]); @@ -450,7 +440,7 @@ async function makePlanchet( reservePub: r.reservePub, withdrawSig: r.withdrawSig, }; - await runWithWriteTransaction(ws.db, [Stores.withdrawalSession], async tx => { + await ws.db.runWithWriteTransaction([Stores.withdrawalSession], async tx => { const myWs = await tx.get(Stores.withdrawalSession, withdrawalSessionId); if (!myWs) { return; @@ -469,8 +459,7 @@ async function processWithdrawCoin( coinIndex: number, ) { logger.trace("starting withdraw for coin", coinIndex); - const withdrawalSession = await oneShotGet( - ws.db, + const withdrawalSession = await ws.db.get( Stores.withdrawalSession, withdrawalSessionId, ); @@ -479,8 +468,7 @@ async function processWithdrawCoin( return; } - const coin = await oneShotGetIndexed( - ws.db, + const coin = await ws.db.getIndexed( Stores.coins.byWithdrawalWithIdx, [withdrawalSessionId, coinIndex], ); @@ -505,7 +493,7 @@ async function incrementWithdrawalRetry( withdrawalSessionId: string, err: OperationError | undefined, ): Promise<void> { - await runWithWriteTransaction(ws.db, [Stores.withdrawalSession], async tx => { + await ws.db.runWithWriteTransaction([Stores.withdrawalSession], async tx => { const wsr = await tx.get(Stores.withdrawalSession, withdrawalSessionId); if (!wsr) { return; @@ -538,7 +526,7 @@ async function resetWithdrawSessionRetry( ws: InternalWalletState, withdrawalSessionId: string, ) { - await oneShotMutate(ws.db, Stores.withdrawalSession, withdrawalSessionId, (x) => { + await ws.db.mutate(Stores.withdrawalSession, withdrawalSessionId, (x) => { if (x.retryInfo.active) { x.retryInfo = initRetryInfo(); } @@ -555,8 +543,7 @@ async function processWithdrawSessionImpl( if (forceNow) { await resetWithdrawSessionRetry(ws, withdrawalSessionId); } - const withdrawalSession = await oneShotGet( - ws.db, + const withdrawalSession = await ws.db.get( Stores.withdrawalSession, withdrawalSessionId, ); @@ -615,15 +602,13 @@ export async function getExchangeWithdrawalInfo( } } - const possibleDenoms = await oneShotIterIndex( - ws.db, + const possibleDenoms = await ws.db.iterIndex( Stores.denominations.exchangeBaseUrlIndex, baseUrl, ).filter(d => d.isOffered); const trustedAuditorPubs = []; - const currencyRecord = await oneShotGet( - ws.db, + const currencyRecord = await ws.db.get( Stores.currencies, amount.currency, ); diff --git a/src/util/query.ts b/src/util/query.ts index e05656bb7..08a8fec02 100644 --- a/src/util/query.ts +++ b/src/util/query.ts @@ -26,22 +26,6 @@ import { openPromise } from "./promiseUtils"; /** - * Result of an inner join. - */ -export interface JoinResult<L, R> { - left: L; - right: R; -} - -/** - * Result of a left outer join. - */ -export interface JoinLeftResult<L, R> { - left: L; - right?: R; -} - -/** * Definition of an object store. */ export class Store<T> { @@ -95,46 +79,6 @@ function transactionToPromise(tx: IDBTransaction): Promise<void> { }); } -export async function oneShotGet<T>( - db: IDBDatabase, - store: Store<T>, - key: any, -): Promise<T | undefined> { - const tx = db.transaction([store.name], "readonly"); - const req = tx.objectStore(store.name).get(key); - const v = await requestToPromise(req); - await transactionToPromise(tx); - return v; -} - -export async function oneShotGetIndexed<S extends IDBValidKey, T>( - db: IDBDatabase, - index: Index<S, T>, - key: any, -): Promise<T | undefined> { - const tx = db.transaction([index.storeName], "readonly"); - const req = tx - .objectStore(index.storeName) - .index(index.indexName) - .get(key); - const v = await requestToPromise(req); - await transactionToPromise(tx); - return v; -} - -export async function oneShotPut<T>( - db: IDBDatabase, - store: Store<T>, - value: T, - key?: any, -): Promise<any> { - const tx = db.transaction([store.name], "readwrite"); - const req = tx.objectStore(store.name).put(value, key); - const v = await requestToPromise(req); - await transactionToPromise(tx); - return v; -} - function applyMutation<T>( req: IDBRequest, f: (x: T) => T | undefined, @@ -166,18 +110,6 @@ function applyMutation<T>( }); } -export async function oneShotMutate<T>( - db: IDBDatabase, - store: Store<T>, - key: any, - f: (x: T) => T | undefined, -): Promise<void> { - const tx = db.transaction([store.name], "readwrite"); - const req = tx.objectStore(store.name).openCursor(key); - await applyMutation(req, f); - await transactionToPromise(tx); -} - type CursorResult<T> = CursorEmptyResult<T> | CursorValueResult<T>; interface CursorEmptyResult<T> { @@ -294,28 +226,6 @@ class ResultStream<T> { } } -export function oneShotIter<T>( - db: IDBDatabase, - store: Store<T>, -): ResultStream<T> { - const tx = db.transaction([store.name], "readonly"); - const req = tx.objectStore(store.name).openCursor(); - return new ResultStream<T>(req); -} - -export function oneShotIterIndex<S extends IDBValidKey, T>( - db: IDBDatabase, - index: Index<S, T>, - query?: any, -): ResultStream<T> { - const tx = db.transaction([index.storeName], "readonly"); - const req = tx - .objectStore(index.storeName) - .index(index.indexName) - .openCursor(query); - return new ResultStream<T>(req); -} - export class TransactionHandle { constructor(private tx: IDBTransaction) {} @@ -361,22 +271,6 @@ export class TransactionHandle { } } -export function runWithReadTransaction<T>( - db: IDBDatabase, - stores: Store<any>[], - f: (t: TransactionHandle) => Promise<T>, -): Promise<T> { - return runWithTransaction<T>(db, stores, f, "readonly"); -} - -export function runWithWriteTransaction<T>( - db: IDBDatabase, - stores: Store<any>[], - f: (t: TransactionHandle) => Promise<T>, -): Promise<T> { - return runWithTransaction<T>(db, stores, f, "readwrite"); -} - function runWithTransaction<T>( db: IDBDatabase, stores: Store<any>[], @@ -471,6 +365,202 @@ export class Index<S extends IDBValidKey, T> { } /** + * Return a promise that resolves + * to the taler wallet db. + */ +export function openDatabase( + idbFactory: IDBFactory, + databaseName: string, + databaseVersion: number, + schema: any, + onVersionChange: () => void, + onUpgradeUnsupported: (oldVersion: number, newVersion: number) => void, +): Promise<IDBDatabase> { + return new Promise<IDBDatabase>((resolve, reject) => { + const req = idbFactory.open(databaseName, databaseVersion); + req.onerror = e => { + console.log("taler database error", e); + reject(new Error("database error")); + }; + req.onsuccess = e => { + req.result.onversionchange = (evt: IDBVersionChangeEvent) => { + console.log( + `handling live db version change from ${evt.oldVersion} to ${evt.newVersion}`, + ); + req.result.close(); + onVersionChange(); + }; + resolve(req.result); + }; + req.onupgradeneeded = e => { + const db = req.result; + console.log( + `DB: upgrade needed: oldVersion=${e.oldVersion}, newVersion=${e.newVersion}`, + ); + switch (e.oldVersion) { + case 0: // DB does not exist yet + for (const n in schema) { + if (schema[n] instanceof Store) { + const si: Store<any> = schema[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<any, any> = (si as any)[indexName]; + s.createIndex(ii.indexName, ii.keyPath, ii.options); + } + } + } + } + break; + default: + if (e.oldVersion !== databaseVersion) { + onUpgradeUnsupported(e.oldVersion, databaseVersion); + throw Error("incompatible DB"); + } + break; + } + }; + }); +} + +/** * Exception that should be thrown by client code to abort a transaction. */ export const TransactionAbort = Symbol("transaction_abort"); + +export class Database { + constructor(private db: IDBDatabase) {} + + static deleteDatabase(idbFactory: IDBFactory, dbName: string) { + idbFactory.deleteDatabase(dbName); + } + + async exportDatabase(): Promise<any> { + const db = this.db; + const dump = { + name: db.name, + stores: {} as { [s: string]: any }, + version: db.version, + }; + + return new Promise((resolve, reject) => { + const tx = db.transaction(Array.from(db.objectStoreNames)); + tx.addEventListener("complete", () => { + resolve(dump); + }); + // 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(); + } + }); + } + }); + } + + importDatabase(dump: any): Promise<void> { + const db = this.db; + console.log("importing db", dump); + return new Promise<void>((resolve, reject) => { + const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite"); + if (dump.stores) { + for (const storeName in dump.stores) { + const objects = []; + const dumpStore = dump.stores[storeName]; + for (const key in dumpStore) { + objects.push(dumpStore[key]); + } + console.log(`importing ${objects.length} records into ${storeName}`); + const store = tx.objectStore(storeName); + for (const obj of objects) { + store.put(obj); + } + } + } + tx.addEventListener("complete", () => { + resolve(); + }); + }); + } + + async get<T>(store: Store<T>, key: any): Promise<T | undefined> { + const tx = this.db.transaction([store.name], "readonly"); + const req = tx.objectStore(store.name).get(key); + const v = await requestToPromise(req); + await transactionToPromise(tx); + return v; + } + + async getIndexed<S extends IDBValidKey, T>( + index: Index<S, T>, + key: any, + ): Promise<T | undefined> { + const tx = this.db.transaction([index.storeName], "readonly"); + const req = tx + .objectStore(index.storeName) + .index(index.indexName) + .get(key); + const v = await requestToPromise(req); + await transactionToPromise(tx); + return v; + } + + async put<T>(store: Store<T>, value: T, key?: any): Promise<any> { + const tx = this.db.transaction([store.name], "readwrite"); + const req = tx.objectStore(store.name).put(value, key); + const v = await requestToPromise(req); + await transactionToPromise(tx); + return v; + } + + async mutate<T>( + store: Store<T>, + key: any, + f: (x: T) => T | undefined, + ): Promise<void> { + const tx = this.db.transaction([store.name], "readwrite"); + const req = tx.objectStore(store.name).openCursor(key); + await applyMutation(req, f); + await transactionToPromise(tx); + } + + iter<T>(store: Store<T>): ResultStream<T> { + const tx = this.db.transaction([store.name], "readonly"); + const req = tx.objectStore(store.name).openCursor(); + return new ResultStream<T>(req); + } + + iterIndex<S extends IDBValidKey, T>( + index: Index<S, T>, + query?: any, + ): ResultStream<T> { + const tx = this.db.transaction([index.storeName], "readonly"); + const req = tx + .objectStore(index.storeName) + .index(index.indexName) + .openCursor(query); + return new ResultStream<T>(req); + } + + async runWithReadTransaction<T>( + stores: Store<any>[], + f: (t: TransactionHandle) => Promise<T>, + ): Promise<T> { + return runWithTransaction<T>(this.db, stores, f, "readonly"); + } + + async runWithWriteTransaction<T>( + stores: Store<any>[], + f: (t: TransactionHandle) => Promise<T>, + ): Promise<T> { + return runWithTransaction<T>(this.db, stores, f, "readwrite"); + } +} diff --git a/src/wallet.ts b/src/wallet.ts index 1db458b38..e4088fab2 100644 --- a/src/wallet.ts +++ b/src/wallet.ts @@ -25,11 +25,7 @@ import { CryptoWorkerFactory } from "./crypto/workers/cryptoApi"; import { HttpRequestLibrary } from "./util/http"; import { - oneShotPut, - oneShotGet, - runWithWriteTransaction, - oneShotIter, - oneShotIterIndex, + Database } from "./util/query"; import { AmountJson } from "./util/amounts"; @@ -148,12 +144,12 @@ export class Wallet { private stopped: boolean = false; private memoRunRetryLoop = new AsyncOpMemoSingle<void>(); - get db(): IDBDatabase { + get db(): Database { return this.ws.db; } constructor( - db: IDBDatabase, + db: Database, http: HttpRequestLibrary, cryptoWorkerFactory: CryptoWorkerFactory, ) { @@ -345,8 +341,7 @@ export class Wallet { * already been applied. */ async fillDefaults() { - await runWithWriteTransaction( - this.db, + await this.db.runWithWriteTransaction( [Stores.config, Stores.currencies], async tx => { let applied = false; @@ -381,7 +376,7 @@ export class Wallet { */ async refreshDirtyCoins(): Promise<{ numRefreshed: number }> { let n = 0; - const coins = await oneShotIter(this.db, Stores.coins).toArray(); + const coins = await this.db.iter(Stores.coins).toArray(); for (let coin of coins) { if (coin.status == CoinStatus.Dirty) { try { @@ -512,7 +507,7 @@ export class Wallet { async findExchange( exchangeBaseUrl: string, ): Promise<ExchangeRecord | undefined> { - return await oneShotGet(this.db, Stores.exchanges, exchangeBaseUrl); + return await this.db.get(Stores.exchanges, exchangeBaseUrl); } /** @@ -540,8 +535,7 @@ export class Wallet { } async getDenoms(exchangeUrl: string): Promise<DenominationRecord[]> { - const denoms = await oneShotIterIndex( - this.db, + const denoms = await this.db.iterIndex( Stores.denominations.exchangeBaseUrlIndex, exchangeUrl, ).toArray(); @@ -549,37 +543,37 @@ export class Wallet { } async getProposal(proposalId: string): Promise<ProposalRecord | undefined> { - const proposal = await oneShotGet(this.db, Stores.proposals, proposalId); + const proposal = await this.db.get(Stores.proposals, proposalId); return proposal; } async getExchanges(): Promise<ExchangeRecord[]> { - return await oneShotIter(this.db, Stores.exchanges).toArray(); + return await this.db.iter(Stores.exchanges).toArray(); } async getCurrencies(): Promise<CurrencyRecord[]> { - return await oneShotIter(this.db, Stores.currencies).toArray(); + return await this.db.iter(Stores.currencies).toArray(); } async updateCurrency(currencyRecord: CurrencyRecord): Promise<void> { logger.trace("updating currency to", currencyRecord); - await oneShotPut(this.db, Stores.currencies, currencyRecord); + await this.db.put(Stores.currencies, currencyRecord); } async getReserves(exchangeBaseUrl: string): Promise<ReserveRecord[]> { - return await oneShotIter(this.db, Stores.reserves).filter( + return await this.db.iter(Stores.reserves).filter( r => r.exchangeBaseUrl === exchangeBaseUrl, ); } async getCoinsForExchange(exchangeBaseUrl: string): Promise<CoinRecord[]> { - return await oneShotIter(this.db, Stores.coins).filter( + return await this.db.iter(Stores.coins).filter( c => c.exchangeBaseUrl === exchangeBaseUrl, ); } async getCoins(): Promise<CoinRecord[]> { - return await oneShotIter(this.db, Stores.coins).toArray(); + return await this.db.iter(Stores.coins).toArray(); } async payback(coinPub: string): Promise<void> { @@ -587,7 +581,7 @@ export class Wallet { } async getPaybackReserves(): Promise<ReserveRecord[]> { - return await oneShotIter(this.db, Stores.reserves).filter( + return await this.db.iter(Stores.reserves).filter( r => r.hasPayback, ); } @@ -604,7 +598,7 @@ export class Wallet { async getSenderWireInfos(): Promise<SenderWireInfos> { const m: { [url: string]: Set<string> } = {}; - await oneShotIter(this.db, Stores.exchanges).forEach(x => { + await this.db.iter(Stores.exchanges).forEach(x => { const wi = x.wireInfo; if (!wi) { return; @@ -619,7 +613,7 @@ export class Wallet { }); const senderWiresSet: Set<string> = new Set(); - await oneShotIter(this.db, Stores.senderWires).forEach(x => { + await this.db.iter(Stores.senderWires).forEach(x => { senderWiresSet.add(x.paytoUri); }); @@ -649,7 +643,7 @@ export class Wallet { async getPurchase( contractTermsHash: string, ): Promise<PurchaseRecord | undefined> { - return oneShotGet(this.db, Stores.purchases, contractTermsHash); + return this.db.get(Stores.purchases, contractTermsHash); } async getFullRefundFees( @@ -683,7 +677,7 @@ export class Wallet { * confirmation from the bank.). */ public async handleNotifyReserve() { - const reserves = await oneShotIter(this.db, Stores.reserves).toArray(); + const reserves = await this.db.iter(Stores.reserves).toArray(); for (const r of reserves) { if (r.reserveStatus === ReserveRecordStatus.WAIT_CONFIRM_BANK) { try { @@ -718,7 +712,7 @@ export class Wallet { } async getPurchaseDetails(hc: string): Promise<PurchaseDetails> { - const purchase = await oneShotGet(this.db, Stores.purchases, hc); + const purchase = await this.db.get(Stores.purchases, hc); if (!purchase) { throw Error("unknown purchase"); } diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts index 547ec35e4..0a44dc193 100644 --- a/src/webex/wxBackend.ts +++ b/src/webex/wxBackend.ts @@ -24,7 +24,7 @@ * Imports. */ import { BrowserCryptoWorkerFactory } from "../crypto/workers/cryptoApi"; -import { deleteDatabase, exportDatabase, importDatabase, openDatabase } from "../db"; +import { deleteTalerDatabase, openTalerDatabase } from "../db"; import { WALLET_DB_VERSION } from "../types/dbTypes"; import { ConfirmReserveRequest, CreateReserveRequest, ReturnCoinsRequest, WalletDiagnostics } from "../types/walletTypes"; import { AmountJson } from "../util/amounts"; @@ -37,6 +37,7 @@ import { isFirefox } from "./compat"; import { MessageType } from "./messages"; import * as wxApi from "./wxApi"; import MessageSender = chrome.runtime.MessageSender; +import { Database } from "../util/query"; const NeedsWallet = Symbol("NeedsWallet"); @@ -67,25 +68,17 @@ async function handleMessage( } case "dump-db": { const db = needsWallet().db; - return exportDatabase(db); + return db.exportDatabase() } case "import-db": { const db = needsWallet().db; - return importDatabase(db, detail.dump); + return db.importDatabase(detail.dump); } case "ping": { return Promise.resolve(); } case "reset-db": { - if (currentWallet) { - const db = currentWallet.db; - const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite"); - // tslint:disable-next-line:prefer-for-of - for (let i = 0; i < db.objectStoreNames.length; i++) { - tx.objectStore(db.objectStoreNames[i]).clear(); - } - } - deleteDatabase(indexedDB); + deleteTalerDatabase(indexedDB); setBadgeText({ text: "" }); console.log("reset done"); if (!currentWallet) { @@ -417,7 +410,7 @@ async function reinitWallet() { setBadgeText({ text: "" }); const badge = new ChromeBadge(); try { - currentDatabase = await openDatabase( + currentDatabase = await openTalerDatabase( indexedDB, reinitWallet, handleUpgradeUnsupported, @@ -430,7 +423,7 @@ async function reinitWallet() { const http = new BrowserHttpLib(); console.log("setting wallet"); const wallet = new Wallet( - currentDatabase, + new Database(currentDatabase), http, new BrowserCryptoWorkerFactory(), ); |