database access refactor

This commit is contained in:
Florian Dold 2021-06-09 15:14:17 +02:00
parent 68dddc848f
commit 5c26461247
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
23 changed files with 3232 additions and 2902 deletions

View File

@ -642,7 +642,7 @@ reservesCli
}) })
.action(async (args) => { .action(async (args) => {
await withWallet(args, async (wallet) => { await withWallet(args, async (wallet) => {
const reserves = await wallet.getReserves(); const reserves = await wallet.getReservesForExchange();
console.log(JSON.stringify(reserves, undefined, 2)); console.log(JSON.stringify(reserves, undefined, 2));
}); });
}); });

View File

@ -1,16 +1,19 @@
import { import {
openDatabase, openDatabase,
Database, describeStore,
Store, describeContents,
Index, describeIndex,
AnyStoreMap, DbAccess,
StoreDescriptor,
StoreWithIndexes,
IndexDescriptor,
} from "./util/query"; } from "./util/query";
import { import {
IDBFactory, IDBFactory,
IDBDatabase, IDBDatabase,
IDBObjectStore, IDBObjectStore,
IDBTransaction, IDBTransaction,
IDBKeyPath, IDBObjectStoreParameters,
} from "@gnu-taler/idb-bridge"; } from "@gnu-taler/idb-bridge";
import { Logger } from "@gnu-taler/taler-util"; import { Logger } from "@gnu-taler/taler-util";
import { import {
@ -55,7 +58,7 @@ export const WALLET_DB_MINOR_VERSION = 1;
const logger = new Logger("db.ts"); const logger = new Logger("db.ts");
function upgradeFromStoreMap( function upgradeFromStoreMap(
storeMap: AnyStoreMap, storeMap: any,
db: IDBDatabase, db: IDBDatabase,
oldVersion: number, oldVersion: number,
newVersion: number, newVersion: number,
@ -63,15 +66,17 @@ function upgradeFromStoreMap(
): void { ): void {
if (oldVersion === 0) { if (oldVersion === 0) {
for (const n in storeMap) { for (const n in storeMap) {
if ((storeMap as any)[n] instanceof Store) { const swi: StoreWithIndexes<StoreDescriptor<unknown>, any> = storeMap[n];
const si: Store<string, any> = (storeMap as any)[n]; const storeDesc: StoreDescriptor<unknown> = swi.store;
const s = db.createObjectStore(si.name, si.storeParams); const s = db.createObjectStore(storeDesc.name, {
for (const indexName in si as any) { autoIncrement: storeDesc.autoIncrement,
if ((si as any)[indexName] instanceof Index) { keyPath: storeDesc.keyPath,
const ii: Index<string, string, any, any> = (si as any)[indexName]; });
s.createIndex(ii.indexName, ii.keyPath, ii.options); for (const indexName in swi.indexMap as any) {
} const indexDesc: IndexDescriptor = swi.indexMap[indexName];
} s.createIndex(indexDesc.name, indexDesc.keyPath, {
multiEntry: indexDesc.multiEntry,
});
} }
} }
return; return;
@ -80,30 +85,7 @@ function upgradeFromStoreMap(
return; return;
} }
logger.info(`upgrading database from ${oldVersion} to ${newVersion}`); logger.info(`upgrading database from ${oldVersion} to ${newVersion}`);
for (const n in Stores) { throw Error("upgrade not supported");
if ((Stores as any)[n] instanceof Store) {
const si: Store<string, any> = (Stores as any)[n];
let s: IDBObjectStore;
const storeVersionAdded = si.storeParams?.versionAdded ?? 1;
if (storeVersionAdded > 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<string, string, any, any> = (si as any)[indexName];
const indexVersionAdded = ii.options?.versionAdded ?? 0;
if (
indexVersionAdded > oldVersion ||
storeVersionAdded > oldVersion
) {
s.createIndex(ii.indexName, ii.keyPath, ii.options);
}
}
}
}
}
} }
function onTalerDbUpgradeNeeded( function onTalerDbUpgradeNeeded(
@ -112,7 +94,13 @@ function onTalerDbUpgradeNeeded(
newVersion: number, newVersion: number,
upgradeTransaction: IDBTransaction, upgradeTransaction: IDBTransaction,
) { ) {
upgradeFromStoreMap(Stores, db, oldVersion, newVersion, upgradeTransaction); upgradeFromStoreMap(
WalletStoresV1,
db,
oldVersion,
newVersion,
upgradeTransaction,
);
} }
function onMetaDbUpgradeNeeded( function onMetaDbUpgradeNeeded(
@ -122,7 +110,7 @@ function onMetaDbUpgradeNeeded(
upgradeTransaction: IDBTransaction, upgradeTransaction: IDBTransaction,
) { ) {
upgradeFromStoreMap( upgradeFromStoreMap(
MetaStores, walletMetadataStore,
db, db,
oldVersion, oldVersion,
newVersion, newVersion,
@ -137,7 +125,7 @@ function onMetaDbUpgradeNeeded(
export async function openTalerDatabase( export async function openTalerDatabase(
idbFactory: IDBFactory, idbFactory: IDBFactory,
onVersionChange: () => void, onVersionChange: () => void,
): Promise<Database<typeof Stores>> { ): Promise<DbAccess<typeof WalletStoresV1>> {
const metaDbHandle = await openDatabase( const metaDbHandle = await openDatabase(
idbFactory, idbFactory,
TALER_META_DB_NAME, TALER_META_DB_NAME,
@ -146,16 +134,17 @@ export async function openTalerDatabase(
onMetaDbUpgradeNeeded, onMetaDbUpgradeNeeded,
); );
const metaDb = new Database(metaDbHandle, MetaStores); const metaDb = new DbAccess(metaDbHandle, walletMetadataStore);
let currentMainVersion: string | undefined; let currentMainVersion: string | undefined;
await metaDb.runWithWriteTransaction([MetaStores.metaConfig], async (tx) => { await metaDb
const dbVersionRecord = await tx.get( .mktx((x) => ({
MetaStores.metaConfig, metaConfig: x.metaConfig,
CURRENT_DB_CONFIG_KEY, }))
); .runReadWrite(async (tx) => {
const dbVersionRecord = await tx.metaConfig.get(CURRENT_DB_CONFIG_KEY);
if (!dbVersionRecord) { if (!dbVersionRecord) {
currentMainVersion = TALER_DB_NAME; currentMainVersion = TALER_DB_NAME;
await tx.put(MetaStores.metaConfig, { await tx.metaConfig.put({
key: CURRENT_DB_CONFIG_KEY, key: CURRENT_DB_CONFIG_KEY,
value: TALER_DB_NAME, value: TALER_DB_NAME,
}); });
@ -177,11 +166,12 @@ export async function openTalerDatabase(
onTalerDbUpgradeNeeded, onTalerDbUpgradeNeeded,
); );
return new Database(mainDbHandle, Stores); return new DbAccess(mainDbHandle, WalletStoresV1);
} }
export function deleteTalerDatabase(idbFactory: IDBFactory): Promise<void> {
return Database.deleteDatabase(idbFactory, TALER_DB_NAME); export function deleteTalerDatabase(idbFactory: IDBFactory): void {
idbFactory.deleteDatabase(TALER_DB_NAME);
} }
export enum ReserveRecordStatus { export enum ReserveRecordStatus {
@ -1683,289 +1673,167 @@ export interface TombstoneRecord {
id: string; id: string;
} }
class ExchangesStore extends Store<"exchanges", ExchangeRecord> { export const WalletStoresV1 = {
constructor() { coins: describeStore(
super("exchanges", { keyPath: "baseUrl" }); describeContents<CoinRecord>("coins", {
} keyPath: "coinPub",
} }),
class ExchangeDetailsStore extends Store<
"exchangeDetails",
ExchangeDetailsRecord
> {
constructor() {
super("exchangeDetails", {
keyPath: ["exchangeBaseUrl", "currency", "masterPublicKey"],
});
}
}
class CoinsStore extends Store<"coins", CoinRecord> {
constructor() {
super("coins", { keyPath: "coinPub" });
}
exchangeBaseUrlIndex = new Index<
"coins",
"exchangeBaseUrl",
string,
CoinRecord
>(this, "exchangeBaseUrl", "exchangeBaseUrl");
denomPubHashIndex = new Index<
"coins",
"denomPubHashIndex",
string,
CoinRecord
>(this, "denomPubHashIndex", "denomPubHash");
coinEvHashIndex = new Index<"coins", "coinEvHashIndex", string, CoinRecord>(
this,
"coinEvHashIndex",
"coinEvHash",
);
}
class ProposalsStore extends Store<"proposals", ProposalRecord> {
constructor() {
super("proposals", { keyPath: "proposalId" });
}
urlAndOrderIdIndex = new Index<
"proposals",
"urlIndex",
string,
ProposalRecord
>(this, "urlIndex", ["merchantBaseUrl", "orderId"]);
}
class PurchasesStore extends Store<"purchases", PurchaseRecord> {
constructor() {
super("purchases", { keyPath: "proposalId" });
}
fulfillmentUrlIndex = new Index<
"purchases",
"fulfillmentUrlIndex",
string,
PurchaseRecord
>(this, "fulfillmentUrlIndex", "download.contractData.fulfillmentUrl");
orderIdIndex = new Index<"purchases", "orderIdIndex", string, PurchaseRecord>(
this,
"orderIdIndex",
["download.contractData.merchantBaseUrl", "download.contractData.orderId"],
);
}
class DenominationsStore extends Store<"denominations", DenominationRecord> {
constructor() {
// cast needed because of bug in type annotations
super("denominations", {
keyPath: (["exchangeBaseUrl", "denomPubHash"] as any) as IDBKeyPath,
});
}
exchangeBaseUrlIndex = new Index<
"denominations",
"exchangeBaseUrlIndex",
string,
DenominationRecord
>(this, "exchangeBaseUrlIndex", "exchangeBaseUrl");
}
class AuditorTrustStore extends Store<"auditorTrust", AuditorTrustRecord> {
constructor() {
super("auditorTrust", {
keyPath: ["currency", "auditorBaseUrl", "auditorPub"],
});
}
auditorPubIndex = new Index<
"auditorTrust",
"auditorPubIndex",
string,
AuditorTrustRecord
>(this, "auditorPubIndex", "auditorPub");
uidIndex = new Index<"auditorTrust", "uidIndex", string, AuditorTrustRecord>(
this,
"uidIndex",
"uids",
{ multiEntry: true },
);
}
class ExchangeTrustStore extends Store<"exchangeTrust", ExchangeTrustRecord> {
constructor() {
super("exchangeTrust", {
keyPath: ["currency", "exchangeBaseUrl", "exchangeMasterPub"],
});
}
exchangeMasterPubIndex = new Index<
"exchangeTrust",
"exchangeMasterPubIndex",
string,
ExchangeTrustRecord
>(this, "exchangeMasterPubIndex", "exchangeMasterPub");
uidIndex = new Index<
"exchangeTrust",
"uidIndex",
string,
ExchangeTrustRecord
>(this, "uidIndex", "uids", { multiEntry: true });
}
class ConfigStore extends Store<"config", ConfigRecord<any>> {
constructor() {
super("config", { keyPath: "key" });
}
}
class ReservesStore extends Store<"reserves", ReserveRecord> {
constructor() {
super("reserves", { keyPath: "reservePub" });
}
byInitialWithdrawalGroupId = new Index<
"reserves",
"initialWithdrawalGroupIdIndex",
string,
ReserveRecord
>(this, "initialWithdrawalGroupIdIndex", "initialWithdrawalGroupId");
}
class TipsStore extends Store<"tips", TipRecord> {
constructor() {
super("tips", { keyPath: "walletTipId" });
}
// Added in version 2
byMerchantTipIdAndBaseUrl = new Index<
"tips",
"tipsByMerchantTipIdAndOriginIndex",
[string, string],
TipRecord
>(this, "tipsByMerchantTipIdAndOriginIndex", [
"merchantTipId",
"merchantBaseUrl",
]);
}
class WithdrawalGroupsStore extends Store<
"withdrawals",
WithdrawalGroupRecord
> {
constructor() {
super("withdrawals", { keyPath: "withdrawalGroupId" });
}
byReservePub = new Index<
"withdrawals",
"withdrawalsByReserveIndex",
string,
WithdrawalGroupRecord
>(this, "withdrawalsByReserveIndex", "reservePub");
}
class PlanchetsStore extends Store<"planchets", PlanchetRecord> {
constructor() {
super("planchets", { keyPath: "coinPub" });
}
byGroupAndIndex = new Index<
"planchets",
"withdrawalGroupAndCoinIdxIndex",
string,
PlanchetRecord
>(this, "withdrawalGroupAndCoinIdxIndex", ["withdrawalGroupId", "coinIdx"]);
byGroup = new Index<
"planchets",
"withdrawalGroupIndex",
string,
PlanchetRecord
>(this, "withdrawalGroupIndex", "withdrawalGroupId");
coinEvHashIndex = new Index<
"planchets",
"coinEvHashIndex",
string,
PlanchetRecord
>(this, "coinEvHashIndex", "coinEvHash");
}
/**
* This store is effectively a materialized index for
* reserve records that are for a bank-integrated withdrawal.
*/
class BankWithdrawUrisStore extends Store<
"bankWithdrawUris",
BankWithdrawUriRecord
> {
constructor() {
super("bankWithdrawUris", { keyPath: "talerWithdrawUri" });
}
}
/**
*/
class BackupProvidersStore extends Store<
"backupProviders",
BackupProviderRecord
> {
constructor() {
super("backupProviders", { keyPath: "baseUrl" });
}
}
class DepositGroupsStore extends Store<"depositGroups", DepositGroupRecord> {
constructor() {
super("depositGroups", { keyPath: "depositGroupId" });
}
}
class TombstonesStore extends Store<"tombstones", TombstoneRecord> {
constructor() {
super("tombstones", { keyPath: "id" });
}
}
/**
* The stores and indices for the wallet database.
*/
export const Stores = {
coins: new CoinsStore(),
config: new ConfigStore(),
auditorTrustStore: new AuditorTrustStore(),
exchangeTrustStore: new ExchangeTrustStore(),
denominations: new DenominationsStore(),
exchanges: new ExchangesStore(),
exchangeDetails: new ExchangeDetailsStore(),
proposals: new ProposalsStore(),
refreshGroups: new Store<"refreshGroups", RefreshGroupRecord>(
"refreshGroups",
{ {
keyPath: "refreshGroupId", byBaseUrl: describeIndex("byBaseUrl", "exchangeBaseUrl"),
byDenomPubHash: describeIndex("byDenomPubHash", "denomPubHash"),
byCoinEvHash: describeIndex("byCoinEvHash", "coinEvHash"),
}, },
), ),
recoupGroups: new Store<"recoupGroups", RecoupGroupRecord>("recoupGroups", { config: describeStore(
describeContents<ConfigRecord<any>>("config", { keyPath: "key" }),
{},
),
auditorTrust: describeStore(
describeContents<AuditorTrustRecord>("auditorTrust", {
keyPath: ["currency", "auditorBaseUrl"],
}),
{
byAuditorPub: describeIndex("byAuditorPub", "auditorPub"),
byUid: describeIndex("byUid", "uids", {
multiEntry: true,
}),
},
),
exchangeTrust: describeStore(
describeContents<ExchangeTrustRecord>("exchangeTrust", {
keyPath: ["currency", "exchangeBaseUrl"],
}),
{
byExchangeMasterPub: describeIndex(
"byExchangeMasterPub",
"exchangeMasterPub",
),
},
),
denominations: describeStore(
describeContents<DenominationRecord>("denominations", {
keyPath: ["exchangeBaseUrl", "denomPubHash"],
}),
{
byExchangeBaseUrl: describeIndex("byExchangeBaseUrl", "exchangeBaseUrl"),
},
),
exchanges: describeStore(
describeContents<ExchangeRecord>("exchanges", {
keyPath: "baseUrl",
}),
{},
),
exchangeDetails: describeStore(
describeContents<ExchangeDetailsRecord>("exchangeDetails", {
keyPath: ["exchangeBaseUrl", "currency", "masterPublicKey"],
}),
{},
),
proposals: describeStore(
describeContents<ProposalRecord>("proposals", { keyPath: "proposalId" }),
{
byUrlAndOrderId: describeIndex("byUrlAndOrderId", [
"merchantBaseUrl",
"orderId",
]),
},
),
refreshGroups: describeStore(
describeContents<RefreshGroupRecord>("refreshGroups", {
keyPath: "refreshGroupId",
}),
{},
),
recoupGroups: describeStore(
describeContents<RecoupGroupRecord>("recoupGroups", {
keyPath: "recoupGroupId", keyPath: "recoupGroupId",
}), }),
reserves: new ReservesStore(), {},
purchases: new PurchasesStore(), ),
tips: new TipsStore(), reserves: describeStore(
withdrawalGroups: new WithdrawalGroupsStore(), describeContents<ReserveRecord>("reserves", { keyPath: "reservePub" }),
planchets: new PlanchetsStore(),
bankWithdrawUris: new BankWithdrawUrisStore(),
backupProviders: new BackupProvidersStore(),
depositGroups: new DepositGroupsStore(),
tombstones: new TombstonesStore(),
ghostDepositGroups: new Store<"ghostDepositGroups", GhostDepositGroupRecord>(
"ghostDepositGroups",
{ {
keyPath: "contractTermsHash", byInitialWithdrawalGroupId: describeIndex(
"byInitialWithdrawalGroupId",
"initialWithdrawalGroupId",
),
}, },
), ),
purchases: describeStore(
describeContents<PurchaseRecord>("purchases", { keyPath: "proposalId" }),
{
byFulfillmentUrl: describeIndex(
"byFulfillmentUrl",
"download.contractData.fulfillmentUrl",
),
byMerchantUrlAndOrderId: describeIndex("byOrderId", [
"download.contractData.merchantBaseUrl",
"download.contractData.orderId",
]),
},
),
tips: describeStore(
describeContents<TipRecord>("tips", { keyPath: "walletTipId" }),
{
byMerchantTipIdAndBaseUrl: describeIndex("byMerchantTipIdAndBaseUrl", [
"merchantTipId",
"merchantBaseUrl",
]),
},
),
withdrawalGroups: describeStore(
describeContents<WithdrawalGroupRecord>("withdrawalGroups", {
keyPath: "withdrawalGroupId",
}),
{
byReservePub: describeIndex("byReservePub", "reservePub"),
},
),
planchets: describeStore(
describeContents<PlanchetRecord>("planchets", { keyPath: "coinPub" }),
{
byGroupAndIndex: describeIndex("byGroupAndIndex", [
"withdrawalGroupId",
"coinIdx",
]),
byGroup: describeIndex("byGroup", "withdrawalGroupId"),
byCoinEvHash: describeIndex("byCoinEv", "coinEvHash"),
},
),
bankWithdrawUris: describeStore(
describeContents<BankWithdrawUriRecord>("bankWithdrawUris", {
keyPath: "talerWithdrawUri",
}),
{},
),
backupProviders: describeStore(
describeContents<BackupProviderRecord>("backupProviders", {
keyPath: "baseUrl",
}),
{},
),
depositGroups: describeStore(
describeContents<DepositGroupRecord>("depositGroups", {
keyPath: "depositGroupId",
}),
{},
),
tombstones: describeStore(
describeContents<TombstoneRecord>("tombstones", { keyPath: "id" }),
{},
),
ghostDepositGroups: describeStore(
describeContents<GhostDepositGroupRecord>("ghostDepositGroups", {
keyPath: "contractTermsHash",
}),
{},
),
}; };
export class MetaConfigStore extends Store<"metaConfig", ConfigRecord<any>> { export const walletMetadataStore = {
constructor() { metaConfig: describeStore(
super("metaConfig", { keyPath: "key" }); describeContents<ConfigRecord<any>>("metaConfig", { keyPath: "key" }),
} {},
} ),
export const MetaStores = {
metaConfig: new MetaConfigStore(),
}; };

View File

@ -57,7 +57,6 @@ import {
} from "./state"; } from "./state";
import { Amounts, getTimestampNow } from "@gnu-taler/taler-util"; import { Amounts, getTimestampNow } from "@gnu-taler/taler-util";
import { import {
Stores,
CoinSourceType, CoinSourceType,
CoinStatus, CoinStatus,
RefundState, RefundState,
@ -66,29 +65,28 @@ import {
} from "../../db.js"; } from "../../db.js";
import { encodeCrock, stringToBytes, getRandomBytes } from "../../index.js"; import { encodeCrock, stringToBytes, getRandomBytes } from "../../index.js";
import { canonicalizeBaseUrl, canonicalJson } from "@gnu-taler/taler-util"; import { canonicalizeBaseUrl, canonicalJson } from "@gnu-taler/taler-util";
import { getExchangeDetails } from "../exchanges.js";
export async function exportBackup( export async function exportBackup(
ws: InternalWalletState, ws: InternalWalletState,
): Promise<WalletBackupContentV1> { ): Promise<WalletBackupContentV1> {
await provideBackupState(ws); await provideBackupState(ws);
return ws.db.runWithWriteTransaction( return ws.db
[ .mktx((x) => ({
Stores.config, config: x.config,
Stores.exchanges, exchanges: x.exchanges,
Stores.exchangeDetails, exchangeDetails: x.exchangeDetails,
Stores.coins, coins: x.coins,
Stores.denominations, denominations: x.denominations,
Stores.purchases, purchases: x.purchases,
Stores.proposals, proposals: x.proposals,
Stores.refreshGroups, refreshGroups: x.refreshGroups,
Stores.backupProviders, backupProviders: x.backupProviders,
Stores.tips, tips: x.tips,
Stores.recoupGroups, recoupGroups: x.recoupGroups,
Stores.reserves, reserves: x.reserves,
Stores.withdrawalGroups, withdrawalGroups: x.withdrawalGroups,
], }))
async (tx) => { .runReadWrite(async (tx) => {
const bs = await getWalletBackupState(ws, tx); const bs = await getWalletBackupState(ws, tx);
const backupExchangeDetails: BackupExchangeDetails[] = []; const backupExchangeDetails: BackupExchangeDetails[] = [];
@ -108,7 +106,7 @@ export async function exportBackup(
[reservePub: string]: BackupWithdrawalGroup[]; [reservePub: string]: BackupWithdrawalGroup[];
} = {}; } = {};
await tx.iter(Stores.withdrawalGroups).forEachAsync(async (wg) => { await tx.withdrawalGroups.iter().forEachAsync(async (wg) => {
const withdrawalGroups = (withdrawalGroupsByReserve[ const withdrawalGroups = (withdrawalGroupsByReserve[
wg.reservePub wg.reservePub
] ??= []); ] ??= []);
@ -126,7 +124,7 @@ export async function exportBackup(
}); });
}); });
await tx.iter(Stores.reserves).forEach((reserve) => { await tx.reserves.iter().forEach((reserve) => {
const backupReserve: BackupReserve = { const backupReserve: BackupReserve = {
initial_selected_denoms: reserve.initialDenomSel.selectedDenoms.map( initial_selected_denoms: reserve.initialDenomSel.selectedDenoms.map(
(x) => ({ (x) => ({
@ -149,7 +147,7 @@ export async function exportBackup(
backupReserves.push(backupReserve); backupReserves.push(backupReserve);
}); });
await tx.iter(Stores.tips).forEach((tip) => { await tx.tips.iter().forEach((tip) => {
backupTips.push({ backupTips.push({
exchange_base_url: tip.exchangeBaseUrl, exchange_base_url: tip.exchangeBaseUrl,
merchant_base_url: tip.merchantBaseUrl, merchant_base_url: tip.merchantBaseUrl,
@ -169,7 +167,7 @@ export async function exportBackup(
}); });
}); });
await tx.iter(Stores.recoupGroups).forEach((recoupGroup) => { await tx.recoupGroups.iter().forEach((recoupGroup) => {
backupRecoupGroups.push({ backupRecoupGroups.push({
recoup_group_id: recoupGroup.recoupGroupId, recoup_group_id: recoupGroup.recoupGroupId,
timestamp_created: recoupGroup.timestampStarted, timestamp_created: recoupGroup.timestampStarted,
@ -182,7 +180,7 @@ export async function exportBackup(
}); });
}); });
await tx.iter(Stores.backupProviders).forEach((bp) => { await tx.backupProviders.iter().forEach((bp) => {
let terms: BackupBackupProviderTerms | undefined; let terms: BackupBackupProviderTerms | undefined;
if (bp.terms) { if (bp.terms) {
terms = { terms = {
@ -199,7 +197,7 @@ export async function exportBackup(
}); });
}); });
await tx.iter(Stores.coins).forEach((coin) => { await tx.coins.iter().forEach((coin) => {
let bcs: BackupCoinSource; let bcs: BackupCoinSource;
switch (coin.coinSource.type) { switch (coin.coinSource.type) {
case CoinSourceType.Refresh: case CoinSourceType.Refresh:
@ -236,7 +234,7 @@ export async function exportBackup(
}); });
}); });
await tx.iter(Stores.denominations).forEach((denom) => { await tx.denominations.iter().forEach((denom) => {
const backupDenoms = (backupDenominationsByExchange[ const backupDenoms = (backupDenominationsByExchange[
denom.exchangeBaseUrl denom.exchangeBaseUrl
] ??= []); ] ??= []);
@ -258,7 +256,7 @@ export async function exportBackup(
}); });
}); });
await tx.iter(Stores.exchanges).forEachAsync(async (ex) => { await tx.exchanges.iter().forEachAsync(async (ex) => {
const dp = ex.detailsPointer; const dp = ex.detailsPointer;
if (!dp) { if (!dp) {
return; return;
@ -271,7 +269,7 @@ export async function exportBackup(
}); });
}); });
await tx.iter(Stores.exchangeDetails).forEachAsync(async (ex) => { await tx.exchangeDetails.iter().forEachAsync(async (ex) => {
// Only back up permanently added exchanges. // Only back up permanently added exchanges.
const wi = ex.wireInfo; const wi = ex.wireInfo;
@ -323,7 +321,7 @@ export async function exportBackup(
const purchaseProposalIdSet = new Set<string>(); const purchaseProposalIdSet = new Set<string>();
await tx.iter(Stores.purchases).forEach((purch) => { await tx.purchases.iter().forEach((purch) => {
const refunds: BackupRefundItem[] = []; const refunds: BackupRefundItem[] = [];
purchaseProposalIdSet.add(purch.proposalId); purchaseProposalIdSet.add(purch.proposalId);
for (const refundKey of Object.keys(purch.refunds)) { for (const refundKey of Object.keys(purch.refunds)) {
@ -376,7 +374,7 @@ export async function exportBackup(
}); });
}); });
await tx.iter(Stores.proposals).forEach((prop) => { await tx.proposals.iter().forEach((prop) => {
if (purchaseProposalIdSet.has(prop.proposalId)) { if (purchaseProposalIdSet.has(prop.proposalId)) {
return; return;
} }
@ -413,7 +411,7 @@ export async function exportBackup(
}); });
}); });
await tx.iter(Stores.refreshGroups).forEach((rg) => { await tx.refreshGroups.iter().forEach((rg) => {
const oldCoins: BackupRefreshOldCoin[] = []; const oldCoins: BackupRefreshOldCoin[] = [];
for (let i = 0; i < rg.oldCoinPubs.length; i++) { for (let i = 0; i < rg.oldCoinPubs.length; i++) {
@ -482,13 +480,12 @@ export async function exportBackup(
hash(stringToBytes(canonicalJson(backupBlob))), hash(stringToBytes(canonicalJson(backupBlob))),
); );
bs.lastBackupNonce = encodeCrock(getRandomBytes(32)); bs.lastBackupNonce = encodeCrock(getRandomBytes(32));
await tx.put(Stores.config, { await tx.config.put({
key: WALLET_BACKUP_STATE_KEY, key: WALLET_BACKUP_STATE_KEY,
value: bs, value: bs,
}); });
} }
return backupBlob; return backupBlob;
}, });
);
} }

View File

@ -29,7 +29,6 @@ import {
BackupRefreshReason, BackupRefreshReason,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { import {
Stores,
WalletContractData, WalletContractData,
DenomSelectionState, DenomSelectionState,
ExchangeUpdateStatus, ExchangeUpdateStatus,
@ -46,8 +45,8 @@ import {
AbortStatus, AbortStatus,
RefreshSessionRecord, RefreshSessionRecord,
WireInfo, WireInfo,
WalletStoresV1,
} from "../../db.js"; } from "../../db.js";
import { TransactionHandle } from "../../index.js";
import { PayCoinSelection } from "../../util/coinSelection"; import { PayCoinSelection } from "../../util/coinSelection";
import { j2s } from "@gnu-taler/taler-util"; import { j2s } from "@gnu-taler/taler-util";
import { checkDbInvariant, checkLogicInvariant } from "../../util/invariants"; import { checkDbInvariant, checkLogicInvariant } from "../../util/invariants";
@ -57,6 +56,7 @@ import { InternalWalletState } from "../state";
import { provideBackupState } from "./state"; import { provideBackupState } from "./state";
import { makeEventId, TombstoneTag } from "../transactions.js"; import { makeEventId, TombstoneTag } from "../transactions.js";
import { getExchangeDetails } from "../exchanges.js"; import { getExchangeDetails } from "../exchanges.js";
import { GetReadOnlyAccess, GetReadWriteAccess } from "../../util/query.js";
const logger = new Logger("operations/backup/import.ts"); const logger = new Logger("operations/backup/import.ts");
@ -74,9 +74,12 @@ function checkBackupInvariant(b: boolean, m?: string): asserts b {
* Re-compute information about the coin selection for a payment. * Re-compute information about the coin selection for a payment.
*/ */
async function recoverPayCoinSelection( async function recoverPayCoinSelection(
tx: TransactionHandle< tx: GetReadWriteAccess<{
typeof Stores.exchanges | typeof Stores.coins | typeof Stores.denominations exchanges: typeof WalletStoresV1.exchanges;
>, exchangeDetails: typeof WalletStoresV1.exchangeDetails;
coins: typeof WalletStoresV1.coins;
denominations: typeof WalletStoresV1.denominations;
}>,
contractData: WalletContractData, contractData: WalletContractData,
backupPurchase: BackupPurchase, backupPurchase: BackupPurchase,
): Promise<PayCoinSelection> { ): Promise<PayCoinSelection> {
@ -93,9 +96,9 @@ async function recoverPayCoinSelection(
); );
for (const coinPub of coinPubs) { for (const coinPub of coinPubs) {
const coinRecord = await tx.get(Stores.coins, coinPub); const coinRecord = await tx.coins.get(coinPub);
checkBackupInvariant(!!coinRecord); checkBackupInvariant(!!coinRecord);
const denom = await tx.get(Stores.denominations, [ const denom = await tx.denominations.get([
coinRecord.exchangeBaseUrl, coinRecord.exchangeBaseUrl,
coinRecord.denomPubHash, coinRecord.denomPubHash,
]); ]);
@ -154,11 +157,11 @@ async function recoverPayCoinSelection(
} }
async function getDenomSelStateFromBackup( async function getDenomSelStateFromBackup(
tx: TransactionHandle<typeof Stores.denominations>, tx: GetReadOnlyAccess<{ denominations: typeof WalletStoresV1.denominations }>,
exchangeBaseUrl: string, exchangeBaseUrl: string,
sel: BackupDenomSel, sel: BackupDenomSel,
): Promise<DenomSelectionState> { ): Promise<DenomSelectionState> {
const d0 = await tx.get(Stores.denominations, [ const d0 = await tx.denominations.get([
exchangeBaseUrl, exchangeBaseUrl,
sel[0].denom_pub_hash, sel[0].denom_pub_hash,
]); ]);
@ -170,10 +173,7 @@ async function getDenomSelStateFromBackup(
let totalCoinValue = Amounts.getZero(d0.value.currency); let totalCoinValue = Amounts.getZero(d0.value.currency);
let totalWithdrawCost = Amounts.getZero(d0.value.currency); let totalWithdrawCost = Amounts.getZero(d0.value.currency);
for (const s of sel) { for (const s of sel) {
const d = await tx.get(Stores.denominations, [ const d = await tx.denominations.get([exchangeBaseUrl, s.denom_pub_hash]);
exchangeBaseUrl,
s.denom_pub_hash,
]);
checkBackupInvariant(!!d); checkBackupInvariant(!!d);
totalCoinValue = Amounts.add(totalCoinValue, d.value).amount; totalCoinValue = Amounts.add(totalCoinValue, d.value).amount;
totalWithdrawCost = Amounts.add(totalWithdrawCost, d.value, d.feeWithdraw) totalWithdrawCost = Amounts.add(totalWithdrawCost, d.value, d.feeWithdraw)
@ -215,32 +215,32 @@ export async function importBackup(
logger.info(`importing backup ${j2s(backupBlobArg)}`); logger.info(`importing backup ${j2s(backupBlobArg)}`);
return ws.db.runWithWriteTransaction( return ws.db
[ .mktx((x) => ({
Stores.config, config: x.config,
Stores.exchanges, exchanges: x.exchanges,
Stores.exchangeDetails, exchangeDetails: x.exchangeDetails,
Stores.coins, coins: x.coins,
Stores.denominations, denominations: x.denominations,
Stores.purchases, purchases: x.purchases,
Stores.proposals, proposals: x.proposals,
Stores.refreshGroups, refreshGroups: x.refreshGroups,
Stores.backupProviders, backupProviders: x.backupProviders,
Stores.tips, tips: x.tips,
Stores.recoupGroups, recoupGroups: x.recoupGroups,
Stores.reserves, reserves: x.reserves,
Stores.withdrawalGroups, withdrawalGroups: x.withdrawalGroups,
Stores.tombstones, tombstones: x.tombstones,
Stores.depositGroups, depositGroups: x.depositGroups,
], }))
async (tx) => { .runReadWrite(async (tx) => {
// FIXME: validate schema! // FIXME: validate schema!
const backupBlob = backupBlobArg as WalletBackupContentV1; const backupBlob = backupBlobArg as WalletBackupContentV1;
// FIXME: validate version // FIXME: validate version
for (const tombstone of backupBlob.tombstones) { for (const tombstone of backupBlob.tombstones) {
await tx.put(Stores.tombstones, { await tx.tombstones.put({
id: tombstone, id: tombstone,
}); });
} }
@ -250,14 +250,13 @@ export async function importBackup(
// FIXME: Validate that the "details pointer" is correct // FIXME: Validate that the "details pointer" is correct
for (const backupExchange of backupBlob.exchanges) { for (const backupExchange of backupBlob.exchanges) {
const existingExchange = await tx.get( const existingExchange = await tx.exchanges.get(
Stores.exchanges,
backupExchange.base_url, backupExchange.base_url,
); );
if (existingExchange) { if (existingExchange) {
continue; continue;
} }
await tx.put(Stores.exchanges, { await tx.exchanges.put({
baseUrl: backupExchange.base_url, baseUrl: backupExchange.base_url,
detailsPointer: { detailsPointer: {
currency: backupExchange.currency, currency: backupExchange.currency,
@ -272,7 +271,7 @@ export async function importBackup(
} }
for (const backupExchangeDetails of backupBlob.exchange_details) { for (const backupExchangeDetails of backupBlob.exchange_details) {
const existingExchangeDetails = await tx.get(Stores.exchangeDetails, [ const existingExchangeDetails = await tx.exchangeDetails.get([
backupExchangeDetails.base_url, backupExchangeDetails.base_url,
backupExchangeDetails.currency, backupExchangeDetails.currency,
backupExchangeDetails.master_public_key, backupExchangeDetails.master_public_key,
@ -296,7 +295,7 @@ export async function importBackup(
wireFee: Amounts.parseOrThrow(fee.wire_fee), wireFee: Amounts.parseOrThrow(fee.wire_fee),
}); });
} }
await tx.put(Stores.exchangeDetails, { await tx.exchangeDetails.put({
exchangeBaseUrl: backupExchangeDetails.base_url, exchangeBaseUrl: backupExchangeDetails.base_url,
termsOfServiceAcceptedEtag: backupExchangeDetails.tos_etag_accepted, termsOfServiceAcceptedEtag: backupExchangeDetails.tos_etag_accepted,
termsOfServiceText: undefined, termsOfServiceText: undefined,
@ -327,7 +326,7 @@ export async function importBackup(
const denomPubHash = const denomPubHash =
cryptoComp.denomPubToHash[backupDenomination.denom_pub]; cryptoComp.denomPubToHash[backupDenomination.denom_pub];
checkLogicInvariant(!!denomPubHash); checkLogicInvariant(!!denomPubHash);
const existingDenom = await tx.get(Stores.denominations, [ const existingDenom = await tx.denominations.get([
backupExchangeDetails.base_url, backupExchangeDetails.base_url,
denomPubHash, denomPubHash,
]); ]);
@ -336,7 +335,7 @@ export async function importBackup(
`importing backup denomination: ${j2s(backupDenomination)}`, `importing backup denomination: ${j2s(backupDenomination)}`,
); );
await tx.put(Stores.denominations, { await tx.denominations.put({
denomPub: backupDenomination.denom_pub, denomPub: backupDenomination.denom_pub,
denomPubHash: denomPubHash, denomPubHash: denomPubHash,
exchangeBaseUrl: backupExchangeDetails.base_url, exchangeBaseUrl: backupExchangeDetails.base_url,
@ -361,7 +360,7 @@ export async function importBackup(
const compCoin = const compCoin =
cryptoComp.coinPrivToCompletedCoin[backupCoin.coin_priv]; cryptoComp.coinPrivToCompletedCoin[backupCoin.coin_priv];
checkLogicInvariant(!!compCoin); checkLogicInvariant(!!compCoin);
const existingCoin = await tx.get(Stores.coins, compCoin.coinPub); const existingCoin = await tx.coins.get(compCoin.coinPub);
if (!existingCoin) { if (!existingCoin) {
let coinSource: CoinSource; let coinSource: CoinSource;
switch (backupCoin.coin_source.type) { switch (backupCoin.coin_source.type) {
@ -388,7 +387,7 @@ export async function importBackup(
}; };
break; break;
} }
await tx.put(Stores.coins, { await tx.coins.put({
blindingKey: backupCoin.blinding_key, blindingKey: backupCoin.blinding_key,
coinEvHash: compCoin.coinEvHash, coinEvHash: compCoin.coinEvHash,
coinPriv: backupCoin.coin_priv, coinPriv: backupCoin.coin_priv,
@ -416,7 +415,7 @@ export async function importBackup(
continue; continue;
} }
checkLogicInvariant(!!reservePub); checkLogicInvariant(!!reservePub);
const existingReserve = await tx.get(Stores.reserves, reservePub); const existingReserve = await tx.reserves.get(reservePub);
const instructedAmount = Amounts.parseOrThrow( const instructedAmount = Amounts.parseOrThrow(
backupReserve.instructed_amount, backupReserve.instructed_amount,
); );
@ -429,7 +428,7 @@ export async function importBackup(
confirmUrl: backupReserve.bank_info.confirm_url, confirmUrl: backupReserve.bank_info.confirm_url,
}; };
} }
await tx.put(Stores.reserves, { await tx.reserves.put({
currency: instructedAmount.currency, currency: instructedAmount.currency,
instructedAmount, instructedAmount,
exchangeBaseUrl: backupExchangeDetails.base_url, exchangeBaseUrl: backupExchangeDetails.base_url,
@ -467,12 +466,11 @@ export async function importBackup(
if (tombstoneSet.has(ts)) { if (tombstoneSet.has(ts)) {
continue; continue;
} }
const existingWg = await tx.get( const existingWg = await tx.withdrawalGroups.get(
Stores.withdrawalGroups,
backupWg.withdrawal_group_id, backupWg.withdrawal_group_id,
); );
if (!existingWg) { if (!existingWg) {
await tx.put(Stores.withdrawalGroups, { await tx.withdrawalGroups.put({
denomsSel: await getDenomSelStateFromBackup( denomsSel: await getDenomSelStateFromBackup(
tx, tx,
backupExchangeDetails.base_url, backupExchangeDetails.base_url,
@ -504,8 +502,7 @@ export async function importBackup(
if (tombstoneSet.has(ts)) { if (tombstoneSet.has(ts)) {
continue; continue;
} }
const existingProposal = await tx.get( const existingProposal = await tx.proposals.get(
Stores.proposals,
backupProposal.proposal_id, backupProposal.proposal_id,
); );
if (!existingProposal) { if (!existingProposal) {
@ -584,7 +581,7 @@ export async function importBackup(
contractTermsRaw: backupProposal.contract_terms_raw, contractTermsRaw: backupProposal.contract_terms_raw,
}; };
} }
await tx.put(Stores.proposals, { await tx.proposals.put({
claimToken: backupProposal.claim_token, claimToken: backupProposal.claim_token,
lastError: undefined, lastError: undefined,
merchantBaseUrl: backupProposal.merchant_base_url, merchantBaseUrl: backupProposal.merchant_base_url,
@ -610,17 +607,16 @@ export async function importBackup(
if (tombstoneSet.has(ts)) { if (tombstoneSet.has(ts)) {
continue; continue;
} }
const existingPurchase = await tx.get( const existingPurchase = await tx.purchases.get(
Stores.purchases,
backupPurchase.proposal_id, backupPurchase.proposal_id,
); );
if (!existingPurchase) { if (!existingPurchase) {
const refunds: { [refundKey: string]: WalletRefundItem } = {}; const refunds: { [refundKey: string]: WalletRefundItem } = {};
for (const backupRefund of backupPurchase.refunds) { for (const backupRefund of backupPurchase.refunds) {
const key = `${backupRefund.coin_pub}-${backupRefund.rtransaction_id}`; const key = `${backupRefund.coin_pub}-${backupRefund.rtransaction_id}`;
const coin = await tx.get(Stores.coins, backupRefund.coin_pub); const coin = await tx.coins.get(backupRefund.coin_pub);
checkBackupInvariant(!!coin); checkBackupInvariant(!!coin);
const denom = await tx.get(Stores.denominations, [ const denom = await tx.denominations.get([
coin.exchangeBaseUrl, coin.exchangeBaseUrl,
coin.denomPubHash, coin.denomPubHash,
]); ]);
@ -724,7 +720,7 @@ export async function importBackup(
}, },
contractTermsRaw: backupPurchase.contract_terms_raw, contractTermsRaw: backupPurchase.contract_terms_raw,
}; };
await tx.put(Stores.purchases, { await tx.purchases.put({
proposalId: backupPurchase.proposal_id, proposalId: backupPurchase.proposal_id,
noncePriv: backupPurchase.nonce_priv, noncePriv: backupPurchase.nonce_priv,
noncePub: noncePub:
@ -766,8 +762,7 @@ export async function importBackup(
if (tombstoneSet.has(ts)) { if (tombstoneSet.has(ts)) {
continue; continue;
} }
const existingRg = await tx.get( const existingRg = await tx.refreshGroups.get(
Stores.refreshGroups,
backupRefreshGroup.refresh_group_id, backupRefreshGroup.refresh_group_id,
); );
if (!existingRg) { if (!existingRg) {
@ -800,7 +795,7 @@ export async function importBackup(
| undefined | undefined
)[] = []; )[] = [];
for (const oldCoin of backupRefreshGroup.old_coins) { for (const oldCoin of backupRefreshGroup.old_coins) {
const c = await tx.get(Stores.coins, oldCoin.coin_pub); const c = await tx.coins.get(oldCoin.coin_pub);
checkBackupInvariant(!!c); checkBackupInvariant(!!c);
if (oldCoin.refresh_session) { if (oldCoin.refresh_session) {
const denomSel = await getDenomSelStateFromBackup( const denomSel = await getDenomSelStateFromBackup(
@ -821,7 +816,7 @@ export async function importBackup(
refreshSessionPerCoin.push(undefined); refreshSessionPerCoin.push(undefined);
} }
} }
await tx.put(Stores.refreshGroups, { await tx.refreshGroups.put({
timestampFinished: backupRefreshGroup.timestamp_finish, timestampFinished: backupRefreshGroup.timestamp_finish,
timestampCreated: backupRefreshGroup.timestamp_created, timestampCreated: backupRefreshGroup.timestamp_created,
refreshGroupId: backupRefreshGroup.refresh_group_id, refreshGroupId: backupRefreshGroup.refresh_group_id,
@ -849,14 +844,14 @@ export async function importBackup(
if (tombstoneSet.has(ts)) { if (tombstoneSet.has(ts)) {
continue; continue;
} }
const existingTip = await tx.get(Stores.tips, backupTip.wallet_tip_id); const existingTip = await tx.tips.get(backupTip.wallet_tip_id);
if (!existingTip) { if (!existingTip) {
const denomsSel = await getDenomSelStateFromBackup( const denomsSel = await getDenomSelStateFromBackup(
tx, tx,
backupTip.exchange_base_url, backupTip.exchange_base_url,
backupTip.selected_denoms, backupTip.selected_denoms,
); );
await tx.put(Stores.tips, { await tx.tips.put({
acceptedTimestamp: backupTip.timestamp_accepted, acceptedTimestamp: backupTip.timestamp_accepted,
createdTimestamp: backupTip.timestamp_created, createdTimestamp: backupTip.timestamp_created,
denomsSel, denomsSel,
@ -884,27 +879,26 @@ export async function importBackup(
for (const tombstone of backupBlob.tombstones) { for (const tombstone of backupBlob.tombstones) {
const [type, ...rest] = tombstone.split(":"); const [type, ...rest] = tombstone.split(":");
if (type === TombstoneTag.DeleteDepositGroup) { if (type === TombstoneTag.DeleteDepositGroup) {
await tx.delete(Stores.depositGroups, rest[0]); await tx.depositGroups.delete(rest[0]);
} else if (type === TombstoneTag.DeletePayment) { } else if (type === TombstoneTag.DeletePayment) {
await tx.delete(Stores.purchases, rest[0]); await tx.purchases.delete(rest[0]);
await tx.delete(Stores.proposals, rest[0]); await tx.proposals.delete(rest[0]);
} else if (type === TombstoneTag.DeleteRefreshGroup) { } else if (type === TombstoneTag.DeleteRefreshGroup) {
await tx.delete(Stores.refreshGroups, rest[0]); await tx.refreshGroups.delete(rest[0]);
} else if (type === TombstoneTag.DeleteRefund) { } else if (type === TombstoneTag.DeleteRefund) {
// Nothing required, will just prevent display // Nothing required, will just prevent display
// in the transactions list // in the transactions list
} else if (type === TombstoneTag.DeleteReserve) { } else if (type === TombstoneTag.DeleteReserve) {
// FIXME: Once we also have account (=kyc) reserves, // FIXME: Once we also have account (=kyc) reserves,
// we need to check if the reserve is an account before deleting here // we need to check if the reserve is an account before deleting here
await tx.delete(Stores.reserves, rest[0]); await tx.reserves.delete(rest[0]);
} else if (type === TombstoneTag.DeleteTip) { } else if (type === TombstoneTag.DeleteTip) {
await tx.delete(Stores.tips, rest[0]); await tx.tips.delete(rest[0]);
} else if (type === TombstoneTag.DeleteWithdrawalGroup) { } else if (type === TombstoneTag.DeleteWithdrawalGroup) {
await tx.delete(Stores.withdrawalGroups, rest[0]); await tx.withdrawalGroups.delete(rest[0]);
} else { } else {
logger.warn(`unable to process tombstone of type '${type}'`); logger.warn(`unable to process tombstone of type '${type}'`);
} }
} }
}, });
);
} }

View File

@ -35,7 +35,6 @@ import {
BackupProviderRecord, BackupProviderRecord,
BackupProviderTerms, BackupProviderTerms,
ConfigRecord, ConfigRecord,
Stores,
} from "../../db.js"; } from "../../db.js";
import { checkDbInvariant, checkLogicInvariant } from "../../util/invariants"; import { checkDbInvariant, checkLogicInvariant } from "../../util/invariants";
import { import {
@ -312,18 +311,17 @@ async function runBackupCycleForProvider(
// FIXME: check if the provider is overcharging us! // FIXME: check if the provider is overcharging us!
await ws.db.runWithWriteTransaction( await ws.db
[Stores.backupProviders], .mktx((x) => ({ backupProviders: x.backupProviders }))
async (tx) => { .runReadWrite(async (tx) => {
const provRec = await tx.get(Stores.backupProviders, provider.baseUrl); const provRec = await tx.backupProviders.get(provider.baseUrl);
checkDbInvariant(!!provRec); checkDbInvariant(!!provRec);
const ids = new Set(provRec.paymentProposalIds); const ids = new Set(provRec.paymentProposalIds);
ids.add(proposalId); ids.add(proposalId);
provRec.paymentProposalIds = Array.from(ids).sort(); provRec.paymentProposalIds = Array.from(ids).sort();
provRec.currentPaymentProposalId = proposalId; provRec.currentPaymentProposalId = proposalId;
await tx.put(Stores.backupProviders, provRec); await tx.backupProviders.put(provRec);
}, });
);
if (doPay) { if (doPay) {
const confirmRes = await confirmPay(ws, proposalId); const confirmRes = await confirmPay(ws, proposalId);
@ -344,19 +342,18 @@ async function runBackupCycleForProvider(
} }
if (resp.status === HttpResponseStatus.NoContent) { if (resp.status === HttpResponseStatus.NoContent) {
await ws.db.runWithWriteTransaction( await ws.db
[Stores.backupProviders], .mktx((x) => ({ backupProviders: x.backupProviders }))
async (tx) => { .runReadWrite(async (tx) => {
const prov = await tx.get(Stores.backupProviders, provider.baseUrl); const prov = await tx.backupProviders.get(provider.baseUrl);
if (!prov) { if (!prov) {
return; return;
} }
prov.lastBackupHash = encodeCrock(currentBackupHash); prov.lastBackupHash = encodeCrock(currentBackupHash);
prov.lastBackupTimestamp = getTimestampNow(); prov.lastBackupTimestamp = getTimestampNow();
prov.lastError = undefined; prov.lastError = undefined;
await tx.put(Stores.backupProviders, prov); await tx.backupProviders.put(prov);
}, });
);
return; return;
} }
@ -367,19 +364,18 @@ async function runBackupCycleForProvider(
const blob = await decryptBackup(backupConfig, backupEnc); const blob = await decryptBackup(backupConfig, backupEnc);
const cryptoData = await computeBackupCryptoData(ws.cryptoApi, blob); const cryptoData = await computeBackupCryptoData(ws.cryptoApi, blob);
await importBackup(ws, blob, cryptoData); await importBackup(ws, blob, cryptoData);
await ws.db.runWithWriteTransaction( await ws.db
[Stores.backupProviders], .mktx((x) => ({ backupProvider: x.backupProviders }))
async (tx) => { .runReadWrite(async (tx) => {
const prov = await tx.get(Stores.backupProviders, provider.baseUrl); const prov = await tx.backupProvider.get(provider.baseUrl);
if (!prov) { if (!prov) {
return; return;
} }
prov.lastBackupHash = encodeCrock(hash(backupEnc)); prov.lastBackupHash = encodeCrock(hash(backupEnc));
prov.lastBackupTimestamp = getTimestampNow(); prov.lastBackupTimestamp = getTimestampNow();
prov.lastError = undefined; prov.lastError = undefined;
await tx.put(Stores.backupProviders, prov); await tx.backupProvider.put(prov);
}, });
);
logger.info("processed existing backup"); logger.info("processed existing backup");
return; return;
} }
@ -390,13 +386,15 @@ async function runBackupCycleForProvider(
const err = await readTalerErrorResponse(resp); const err = await readTalerErrorResponse(resp);
logger.error(`got error response from backup provider: ${j2s(err)}`); logger.error(`got error response from backup provider: ${j2s(err)}`);
await ws.db.runWithWriteTransaction([Stores.backupProviders], async (tx) => { await ws.db
const prov = await tx.get(Stores.backupProviders, provider.baseUrl); .mktx((x) => ({ backupProvider: x.backupProviders }))
.runReadWrite(async (tx) => {
const prov = await tx.backupProvider.get(provider.baseUrl);
if (!prov) { if (!prov) {
return; return;
} }
prov.lastError = err; prov.lastError = err;
await tx.put(Stores.backupProviders, prov); await tx.backupProvider.put(prov);
}); });
} }
@ -408,7 +406,11 @@ async function runBackupCycleForProvider(
* 3. Upload the updated backup blob. * 3. Upload the updated backup blob.
*/ */
export async function runBackupCycle(ws: InternalWalletState): Promise<void> { export async function runBackupCycle(ws: InternalWalletState): Promise<void> {
const providers = await ws.db.iter(Stores.backupProviders).toArray(); const providers = await ws.db
.mktx((x) => ({ backupProviders: x.backupProviders }))
.runReadOnly(async (tx) => {
return await tx.backupProviders.iter().toArray();
});
logger.trace("got backup providers", providers); logger.trace("got backup providers", providers);
const backupJson = await exportBackup(ws); const backupJson = await exportBackup(ws);
@ -472,23 +474,30 @@ export async function addBackupProvider(
logger.info(`adding backup provider ${j2s(req)}`); logger.info(`adding backup provider ${j2s(req)}`);
await provideBackupState(ws); await provideBackupState(ws);
const canonUrl = canonicalizeBaseUrl(req.backupProviderBaseUrl); const canonUrl = canonicalizeBaseUrl(req.backupProviderBaseUrl);
const oldProv = await ws.db.get(Stores.backupProviders, canonUrl); await ws.db
.mktx((x) => ({ backupProviders: x.backupProviders }))
.runReadWrite(async (tx) => {
const oldProv = await tx.backupProviders.get(canonUrl);
if (oldProv) { if (oldProv) {
logger.info("old backup provider found"); logger.info("old backup provider found");
if (req.activate) { if (req.activate) {
oldProv.active = true; oldProv.active = true;
logger.info("setting existing backup provider to active"); logger.info("setting existing backup provider to active");
await ws.db.put(Stores.backupProviders, oldProv); await tx.backupProviders.put(oldProv);
} }
return; return;
} }
});
const termsUrl = new URL("terms", canonUrl); const termsUrl = new URL("terms", canonUrl);
const resp = await ws.http.get(termsUrl.href); const resp = await ws.http.get(termsUrl.href);
const terms = await readSuccessResponseJsonOrThrow( const terms = await readSuccessResponseJsonOrThrow(
resp, resp,
codecForSyncTermsOfServiceResponse(), codecForSyncTermsOfServiceResponse(),
); );
await ws.db.put(Stores.backupProviders, { await ws.db
.mktx((x) => ({ backupProviders: x.backupProviders }))
.runReadWrite(async (tx) => {
await tx.backupProviders.put({
active: !!req.activate, active: !!req.activate,
terms: { terms: {
annualFee: terms.annual_fee, annualFee: terms.annual_fee,
@ -501,6 +510,7 @@ export async function addBackupProvider(
retryInfo: initRetryInfo(false), retryInfo: initRetryInfo(false),
uids: [encodeCrock(getRandomBytes(32))], uids: [encodeCrock(getRandomBytes(32))],
}); });
});
} }
export async function removeBackupProvider( export async function removeBackupProvider(
@ -654,7 +664,11 @@ export async function getBackupInfo(
ws: InternalWalletState, ws: InternalWalletState,
): Promise<BackupInfo> { ): Promise<BackupInfo> {
const backupConfig = await provideBackupState(ws); const backupConfig = await provideBackupState(ws);
const providerRecords = await ws.db.iter(Stores.backupProviders).toArray(); const providerRecords = await ws.db
.mktx((x) => ({ backupProviders: x.backupProviders }))
.runReadOnly(async (tx) => {
return await tx.backupProviders.iter().toArray();
});
const providers: ProviderInfo[] = []; const providers: ProviderInfo[] = [];
for (const x of providerRecords) { for (const x of providerRecords) {
providers.push({ providers.push({
@ -675,13 +689,18 @@ export async function getBackupInfo(
} }
/** /**
* Get information about the current state of wallet backups. * Get backup recovery information, including the wallet's
* private key.
*/ */
export async function getBackupRecovery( export async function getBackupRecovery(
ws: InternalWalletState, ws: InternalWalletState,
): Promise<BackupRecovery> { ): Promise<BackupRecovery> {
const bs = await provideBackupState(ws); const bs = await provideBackupState(ws);
const providers = await ws.db.iter(Stores.backupProviders).toArray(); const providers = await ws.db
.mktx((x) => ({ backupProviders: x.backupProviders }))
.runReadOnly(async (tx) => {
return await tx.backupProviders.iter().toArray();
});
return { return {
providers: providers providers: providers
.filter((x) => x.active) .filter((x) => x.active)
@ -698,12 +717,12 @@ async function backupRecoveryTheirs(
ws: InternalWalletState, ws: InternalWalletState,
br: BackupRecovery, br: BackupRecovery,
) { ) {
await ws.db.runWithWriteTransaction( await ws.db
[Stores.config, Stores.backupProviders], .mktx((x) => ({ config: x.config, backupProviders: x.backupProviders }))
async (tx) => { .runReadWrite(async (tx) => {
let backupStateEntry: let backupStateEntry:
| ConfigRecord<WalletBackupConfState> | ConfigRecord<WalletBackupConfState>
| undefined = await tx.get(Stores.config, WALLET_BACKUP_STATE_KEY); | undefined = await tx.config.get(WALLET_BACKUP_STATE_KEY);
checkDbInvariant(!!backupStateEntry); checkDbInvariant(!!backupStateEntry);
backupStateEntry.value.lastBackupNonce = undefined; backupStateEntry.value.lastBackupNonce = undefined;
backupStateEntry.value.lastBackupTimestamp = undefined; backupStateEntry.value.lastBackupTimestamp = undefined;
@ -713,11 +732,11 @@ async function backupRecoveryTheirs(
backupStateEntry.value.walletRootPub = encodeCrock( backupStateEntry.value.walletRootPub = encodeCrock(
eddsaGetPublic(decodeCrock(br.walletRootPriv)), eddsaGetPublic(decodeCrock(br.walletRootPriv)),
); );
await tx.put(Stores.config, backupStateEntry); await tx.config.put(backupStateEntry);
for (const prov of br.providers) { for (const prov of br.providers) {
const existingProv = await tx.get(Stores.backupProviders, prov.url); const existingProv = await tx.backupProviders.get(prov.url);
if (!existingProv) { if (!existingProv) {
await tx.put(Stores.backupProviders, { await tx.backupProviders.put({
active: true, active: true,
baseUrl: prov.url, baseUrl: prov.url,
paymentProposalIds: [], paymentProposalIds: [],
@ -727,14 +746,13 @@ async function backupRecoveryTheirs(
}); });
} }
} }
const providers = await tx.iter(Stores.backupProviders).toArray(); const providers = await tx.backupProviders.iter().toArray();
for (const prov of providers) { for (const prov of providers) {
prov.lastBackupTimestamp = undefined; prov.lastBackupTimestamp = undefined;
prov.lastBackupHash = undefined; prov.lastBackupHash = undefined;
await tx.put(Stores.backupProviders, prov); await tx.backupProviders.put(prov);
} }
}, });
);
} }
async function backupRecoveryOurs(ws: InternalWalletState, br: BackupRecovery) { async function backupRecoveryOurs(ws: InternalWalletState, br: BackupRecovery) {
@ -746,7 +764,11 @@ export async function loadBackupRecovery(
br: RecoveryLoadRequest, br: RecoveryLoadRequest,
): Promise<void> { ): Promise<void> {
const bs = await provideBackupState(ws); const bs = await provideBackupState(ws);
const providers = await ws.db.iter(Stores.backupProviders).toArray(); const providers = await ws.db
.mktx((x) => ({ backupProviders: x.backupProviders }))
.runReadOnly(async (tx) => {
return await tx.backupProviders.iter().toArray();
});
let strategy = br.strategy; let strategy = br.strategy;
if ( if (
br.recovery.walletRootPriv != bs.walletRootPriv && br.recovery.walletRootPriv != bs.walletRootPriv &&
@ -772,12 +794,11 @@ export async function exportBackupEncrypted(
): Promise<Uint8Array> { ): Promise<Uint8Array> {
await provideBackupState(ws); await provideBackupState(ws);
const blob = await exportBackup(ws); const blob = await exportBackup(ws);
const bs = await ws.db.runWithWriteTransaction( const bs = await ws.db
[Stores.config], .mktx((x) => ({ config: x.config }))
async (tx) => { .runReadOnly(async (tx) => {
return await getWalletBackupState(ws, tx); return await getWalletBackupState(ws, tx);
}, });
);
return encryptBackup(bs, blob); return encryptBackup(bs, blob);
} }

View File

@ -15,9 +15,11 @@
*/ */
import { Timestamp } from "@gnu-taler/taler-util"; import { Timestamp } from "@gnu-taler/taler-util";
import { ConfigRecord, Stores } from "../../db.js"; import { ConfigRecord, WalletStoresV1 } from "../../db.js";
import { getRandomBytes, encodeCrock, TransactionHandle } from "../../index.js"; import { getRandomBytes, encodeCrock } from "../../index.js";
import { checkDbInvariant } from "../../util/invariants"; import { checkDbInvariant } from "../../util/invariants";
import { GetReadOnlyAccess } from "../../util/query.js";
import { Wallet } from "../../wallet.js";
import { InternalWalletState } from "../state"; import { InternalWalletState } from "../state";
export interface WalletBackupConfState { export interface WalletBackupConfState {
@ -48,10 +50,13 @@ export const WALLET_BACKUP_STATE_KEY = "walletBackupState";
export async function provideBackupState( export async function provideBackupState(
ws: InternalWalletState, ws: InternalWalletState,
): Promise<WalletBackupConfState> { ): Promise<WalletBackupConfState> {
const bs: ConfigRecord<WalletBackupConfState> | undefined = await ws.db.get( const bs: ConfigRecord<WalletBackupConfState> | undefined = await ws.db
Stores.config, .mktx((x) => ({
WALLET_BACKUP_STATE_KEY, config: x.config,
); }))
.runReadOnly(async (tx) => {
return tx.config.get(WALLET_BACKUP_STATE_KEY);
});
if (bs) { if (bs) {
return bs.value; return bs.value;
} }
@ -62,10 +67,14 @@ export async function provideBackupState(
// FIXME: device ID should be configured when wallet is initialized // FIXME: device ID should be configured when wallet is initialized
// and be based on hostname // and be based on hostname
const deviceId = `wallet-core-${encodeCrock(d)}`; const deviceId = `wallet-core-${encodeCrock(d)}`;
return await ws.db.runWithWriteTransaction([Stores.config], async (tx) => { return await ws.db
.mktx((x) => ({
config: x.config,
}))
.runReadWrite(async (tx) => {
let backupStateEntry: let backupStateEntry:
| ConfigRecord<WalletBackupConfState> | ConfigRecord<WalletBackupConfState>
| undefined = await tx.get(Stores.config, WALLET_BACKUP_STATE_KEY); | undefined = await tx.config.get(WALLET_BACKUP_STATE_KEY);
if (!backupStateEntry) { if (!backupStateEntry) {
backupStateEntry = { backupStateEntry = {
key: WALLET_BACKUP_STATE_KEY, key: WALLET_BACKUP_STATE_KEY,
@ -77,7 +86,7 @@ export async function provideBackupState(
lastBackupPlainHash: undefined, lastBackupPlainHash: undefined,
}, },
}; };
await tx.put(Stores.config, backupStateEntry); await tx.config.put(backupStateEntry);
} }
return backupStateEntry.value; return backupStateEntry.value;
}); });
@ -85,9 +94,9 @@ export async function provideBackupState(
export async function getWalletBackupState( export async function getWalletBackupState(
ws: InternalWalletState, ws: InternalWalletState,
tx: TransactionHandle<typeof Stores.config>, tx: GetReadOnlyAccess<{ config: typeof WalletStoresV1.config }>,
): Promise<WalletBackupConfState> { ): Promise<WalletBackupConfState> {
let bs = await tx.get(Stores.config, WALLET_BACKUP_STATE_KEY); const bs = await tx.config.get(WALLET_BACKUP_STATE_KEY);
checkDbInvariant(!!bs, "wallet backup state should be in DB"); checkDbInvariant(!!bs, "wallet backup state should be in DB");
return bs.value; return bs.value;
} }

View File

@ -17,10 +17,10 @@
/** /**
* Imports. * Imports.
*/ */
import { AmountJson, BalancesResponse, Amounts } from "@gnu-taler/taler-util"; import { AmountJson, BalancesResponse, Amounts, Logger } from "@gnu-taler/taler-util";
import { Stores, CoinStatus } from "../db.js";
import { TransactionHandle } from "../index.js"; import { CoinStatus, WalletStoresV1 } from "../db.js";
import { Logger } from "@gnu-taler/taler-util"; import { GetReadOnlyAccess } from "../util/query.js";
import { InternalWalletState } from "./state.js"; import { InternalWalletState } from "./state.js";
const logger = new Logger("withdraw.ts"); const logger = new Logger("withdraw.ts");
@ -36,13 +36,12 @@ interface WalletBalance {
*/ */
export async function getBalancesInsideTransaction( export async function getBalancesInsideTransaction(
ws: InternalWalletState, ws: InternalWalletState,
tx: TransactionHandle< tx: GetReadOnlyAccess<{
| typeof Stores.reserves reserves: typeof WalletStoresV1.reserves;
| typeof Stores.coins coins: typeof WalletStoresV1.coins;
| typeof Stores.reserves refreshGroups: typeof WalletStoresV1.refreshGroups;
| typeof Stores.refreshGroups withdrawalGroups: typeof WalletStoresV1.withdrawalGroups;
| typeof Stores.withdrawalGroups }>,
>,
): Promise<BalancesResponse> { ): Promise<BalancesResponse> {
const balanceStore: Record<string, WalletBalance> = {}; const balanceStore: Record<string, WalletBalance> = {};
@ -63,7 +62,7 @@ export async function getBalancesInsideTransaction(
}; };
// Initialize balance to zero, even if we didn't start withdrawing yet. // Initialize balance to zero, even if we didn't start withdrawing yet.
await tx.iter(Stores.reserves).forEach((r) => { await tx.reserves.iter().forEach((r) => {
const b = initBalance(r.currency); const b = initBalance(r.currency);
if (!r.initialWithdrawalStarted) { if (!r.initialWithdrawalStarted) {
b.pendingIncoming = Amounts.add( b.pendingIncoming = Amounts.add(
@ -73,7 +72,7 @@ export async function getBalancesInsideTransaction(
} }
}); });
await tx.iter(Stores.coins).forEach((c) => { await tx.coins.iter().forEach((c) => {
// Only count fresh coins, as dormant coins will // Only count fresh coins, as dormant coins will
// already be in a refresh session. // already be in a refresh session.
if (c.status === CoinStatus.Fresh) { if (c.status === CoinStatus.Fresh) {
@ -82,7 +81,7 @@ export async function getBalancesInsideTransaction(
} }
}); });
await tx.iter(Stores.refreshGroups).forEach((r) => { await tx.refreshGroups.iter().forEach((r) => {
// Don't count finished refreshes, since the refresh already resulted // Don't count finished refreshes, since the refresh already resulted
// in coins being added to the wallet. // in coins being added to the wallet.
if (r.timestampFinished) { if (r.timestampFinished) {
@ -108,7 +107,7 @@ export async function getBalancesInsideTransaction(
} }
}); });
await tx.iter(Stores.withdrawalGroups).forEach((wds) => { await tx.withdrawalGroups.iter().forEach((wds) => {
if (wds.timestampFinish) { if (wds.timestampFinish) {
return; return;
} }
@ -147,18 +146,17 @@ export async function getBalances(
): Promise<BalancesResponse> { ): Promise<BalancesResponse> {
logger.trace("starting to compute balance"); logger.trace("starting to compute balance");
const wbal = await ws.db.runWithReadTransaction( const wbal = await ws.db
[ .mktx((x) => ({
Stores.coins, coins: x.coins,
Stores.refreshGroups, refreshGroups: x.refreshGroups,
Stores.reserves, reserves: x.reserves,
Stores.purchases, purchases: x.purchases,
Stores.withdrawalGroups, withdrawalGroups: x.withdrawalGroups,
], }))
async (tx) => { .runReadOnly(async (tx) => {
return getBalancesInsideTransaction(ws, tx); return getBalancesInsideTransaction(ws, tx);
}, });
);
logger.trace("finished computing wallet balance"); logger.trace("finished computing wallet balance");

View File

@ -17,7 +17,7 @@
/** /**
* Imports. * Imports.
*/ */
import { ExchangeRecord, Stores } from "../db.js"; import { ExchangeRecord } from "../db.js";
import { Logger } from "@gnu-taler/taler-util"; import { Logger } from "@gnu-taler/taler-util";
import { getExchangeDetails } from "./exchanges.js"; import { getExchangeDetails } from "./exchanges.js";
import { InternalWalletState } from "./state.js"; import { InternalWalletState } from "./state.js";
@ -38,17 +38,24 @@ export async function getExchangeTrust(
): Promise<TrustInfo> { ): Promise<TrustInfo> {
let isTrusted = false; let isTrusted = false;
let isAudited = false; let isAudited = false;
const exchangeDetails = await ws.db.runWithReadTransaction(
[Stores.exchangeDetails, Stores.exchanges], return await ws.db
async (tx) => { .mktx((x) => ({
return getExchangeDetails(tx, exchangeInfo.baseUrl); exchanges: x.exchanges,
}, exchangeDetails: x.exchangeDetails,
exchangesTrustStore: x.exchangeTrust,
auditorTrust: x.auditorTrust,
}))
.runReadOnly(async (tx) => {
const exchangeDetails = await getExchangeDetails(
tx,
exchangeInfo.baseUrl,
); );
if (!exchangeDetails) { if (!exchangeDetails) {
throw Error(`exchange ${exchangeInfo.baseUrl} details not available`); throw Error(`exchange ${exchangeInfo.baseUrl} details not available`);
} }
const exchangeTrustRecord = await ws.db.getIndexed( const exchangeTrustRecord = await tx.exchangesTrustStore.indexes.byExchangeMasterPub.get(
Stores.exchangeTrustStore.exchangeMasterPubIndex,
exchangeDetails.masterPublicKey, exchangeDetails.masterPublicKey,
); );
if ( if (
@ -60,8 +67,7 @@ export async function getExchangeTrust(
} }
for (const auditor of exchangeDetails.auditors) { for (const auditor of exchangeDetails.auditors) {
const auditorTrustRecord = await ws.db.getIndexed( const auditorTrustRecord = await tx.auditorTrust.indexes.byAuditorPub.get(
Stores.auditorTrustStore.auditorPubIndex,
auditor.auditor_pub, auditor.auditor_pub,
); );
if (auditorTrustRecord && auditorTrustRecord.uids.length > 0) { if (auditorTrustRecord && auditorTrustRecord.uids.length > 0) {
@ -71,4 +77,5 @@ export async function getExchangeTrust(
} }
return { isTrusted, isAudited }; return { isTrusted, isAudited };
});
} }

View File

@ -56,7 +56,8 @@ import {
} from "./pay"; } from "./pay";
import { InternalWalletState } from "./state"; import { InternalWalletState } from "./state";
import { Logger } from "@gnu-taler/taler-util"; import { Logger } from "@gnu-taler/taler-util";
import { DepositGroupRecord, Stores } from "../db.js"; import { DepositGroupRecord } from "../db.js";
import { guardOperationException } from "./errors.js"; import { guardOperationException } from "./errors.js";
import { getExchangeDetails } from "./exchanges.js"; import { getExchangeDetails } from "./exchanges.js";
@ -116,11 +117,16 @@ async function resetDepositGroupRetry(
ws: InternalWalletState, ws: InternalWalletState,
depositGroupId: string, depositGroupId: string,
): Promise<void> { ): Promise<void> {
await ws.db.mutate(Stores.depositGroups, depositGroupId, (x) => { await ws.db
if (x.retryInfo.active) { .mktx((x) => ({
depositGroups: x.depositGroups,
}))
.runReadWrite(async (tx) => {
const x = await tx.depositGroups.get(depositGroupId);
if (x && x.retryInfo.active) {
x.retryInfo = initRetryInfo(); x.retryInfo = initRetryInfo();
await tx.depositGroups.put(x);
} }
return x;
}); });
} }
@ -129,8 +135,10 @@ async function incrementDepositRetry(
depositGroupId: string, depositGroupId: string,
err: TalerErrorDetails | undefined, err: TalerErrorDetails | undefined,
): Promise<void> { ): Promise<void> {
await ws.db.runWithWriteTransaction([Stores.depositGroups], async (tx) => { await ws.db
const r = await tx.get(Stores.depositGroups, depositGroupId); .mktx((x) => ({ depositGroups: x.depositGroups }))
.runReadWrite(async (tx) => {
const r = await tx.depositGroups.get(depositGroupId);
if (!r) { if (!r) {
return; return;
} }
@ -140,7 +148,7 @@ async function incrementDepositRetry(
r.retryInfo.retryCounter++; r.retryInfo.retryCounter++;
updateRetryInfoTimeout(r.retryInfo); updateRetryInfoTimeout(r.retryInfo);
r.lastError = err; r.lastError = err;
await tx.put(Stores.depositGroups, r); await tx.depositGroups.put(r);
}); });
if (err) { if (err) {
ws.notify({ type: NotificationType.DepositOperationError, error: err }); ws.notify({ type: NotificationType.DepositOperationError, error: err });
@ -170,7 +178,13 @@ async function processDepositGroupImpl(
if (forceNow) { if (forceNow) {
await resetDepositGroupRetry(ws, depositGroupId); await resetDepositGroupRetry(ws, depositGroupId);
} }
const depositGroup = await ws.db.get(Stores.depositGroups, depositGroupId); const depositGroup = await ws.db
.mktx((x) => ({
depositGroups: x.depositGroups,
}))
.runReadOnly(async (tx) => {
return tx.depositGroups.get(depositGroupId);
});
if (!depositGroup) { if (!depositGroup) {
logger.warn(`deposit group ${depositGroupId} not found`); logger.warn(`deposit group ${depositGroupId} not found`);
return; return;
@ -213,18 +227,24 @@ async function processDepositGroupImpl(
merchant_pub: depositGroup.merchantPub, merchant_pub: depositGroup.merchantPub,
}); });
await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess()); await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess());
await ws.db.runWithWriteTransaction([Stores.depositGroups], async (tx) => { await ws.db
const dg = await tx.get(Stores.depositGroups, depositGroupId); .mktx((x) => ({ depositGroups: x.depositGroups }))
.runReadWrite(async (tx) => {
const dg = await tx.depositGroups.get(depositGroupId);
if (!dg) { if (!dg) {
return; return;
} }
dg.depositedPerCoin[i] = true; dg.depositedPerCoin[i] = true;
await tx.put(Stores.depositGroups, dg); await tx.depositGroups.put(dg);
}); });
} }
await ws.db.runWithWriteTransaction([Stores.depositGroups], async (tx) => { await ws.db
const dg = await tx.get(Stores.depositGroups, depositGroupId); .mktx((x) => ({
depositGroups: x.depositGroups,
}))
.runReadWrite(async (tx) => {
const dg = await tx.depositGroups.get(depositGroupId);
if (!dg) { if (!dg) {
return; return;
} }
@ -236,7 +256,7 @@ async function processDepositGroupImpl(
} }
if (allDeposited) { if (allDeposited) {
dg.timestampFinished = getTimestampNow(); dg.timestampFinished = getTimestampNow();
await tx.put(Stores.depositGroups, dg); await tx.depositGroups.put(dg);
} }
}); });
} }
@ -249,10 +269,13 @@ export async function trackDepositGroup(
status: number; status: number;
body: any; body: any;
}[] = []; }[] = [];
const depositGroup = await ws.db.get( const depositGroup = await ws.db
Stores.depositGroups, .mktx((x) => ({
req.depositGroupId, depositGroups: x.depositGroups,
); }))
.runReadOnly(async (tx) => {
return tx.depositGroups.get(req.depositGroupId);
});
if (!depositGroup) { if (!depositGroup) {
throw Error("deposit group not found"); throw Error("deposit group not found");
} }
@ -306,15 +329,17 @@ export async function createDepositGroup(
const amount = Amounts.parseOrThrow(req.amount); const amount = Amounts.parseOrThrow(req.amount);
const allExchanges = await ws.db.iter(Stores.exchanges).toArray();
const exchangeInfos: { url: string; master_pub: string }[] = []; const exchangeInfos: { url: string; master_pub: string }[] = [];
await ws.db
.mktx((x) => ({
exchanges: x.exchanges,
exchangeDetails: x.exchangeDetails,
}))
.runReadOnly(async (tx) => {
const allExchanges = await tx.exchanges.iter().toArray();
for (const e of allExchanges) { for (const e of allExchanges) {
const details = await ws.db.runWithReadTransaction( const details = await getExchangeDetails(tx, e.baseUrl);
[Stores.exchanges, Stores.exchangeDetails],
async (tx) => {
return getExchangeDetails(tx, e.baseUrl);
},
);
if (!details) { if (!details) {
continue; continue;
} }
@ -323,6 +348,7 @@ export async function createDepositGroup(
url: e.baseUrl, url: e.baseUrl,
}); });
} }
});
const timestamp = getTimestampNow(); const timestamp = getTimestampNow();
const timestampRound = timestampTruncateToSecond(timestamp); const timestampRound = timestampTruncateToSecond(timestamp);
@ -421,20 +447,17 @@ export async function createDepositGroup(
lastError: undefined, lastError: undefined,
}; };
await ws.db.runWithWriteTransaction( await ws.db
[ .mktx((x) => ({
Stores.depositGroups, depositGroups: x.depositGroups,
Stores.coins, coins: x.coins,
Stores.refreshGroups, refreshGroups: x.refreshGroups,
Stores.denominations, denominations: x.denominations,
], }))
async (tx) => { .runReadWrite(async (tx) => {
await applyCoinSpend(ws, tx, payCoinSel); await applyCoinSpend(ws, tx, payCoinSel);
await tx.put(Stores.depositGroups, depositGroup); await tx.depositGroups.put(depositGroup);
}, });
);
await ws.db.put(Stores.depositGroups, depositGroup);
return { depositGroupId }; return { depositGroupId };
} }

View File

@ -41,13 +41,13 @@ import {
import { import {
DenominationRecord, DenominationRecord,
DenominationStatus, DenominationStatus,
Stores,
ExchangeRecord, ExchangeRecord,
ExchangeUpdateStatus, ExchangeUpdateStatus,
WireFee, WireFee,
ExchangeUpdateReason, ExchangeUpdateReason,
ExchangeDetailsRecord, ExchangeDetailsRecord,
WireInfo, WireInfo,
WalletStoresV1,
} from "../db.js"; } from "../db.js";
import { import {
URL, URL,
@ -73,7 +73,7 @@ import {
} from "./versions.js"; } from "./versions.js";
import { HttpRequestLibrary } from "../util/http.js"; import { HttpRequestLibrary } from "../util/http.js";
import { CryptoApi } from "../crypto/workers/cryptoApi.js"; import { CryptoApi } from "../crypto/workers/cryptoApi.js";
import { TransactionHandle } from "../util/query.js"; import { DbAccess, GetReadOnlyAccess } from "../util/query.js";
const logger = new Logger("exchanges.ts"); const logger = new Logger("exchanges.ts");
@ -108,8 +108,10 @@ async function handleExchangeUpdateError(
baseUrl: string, baseUrl: string,
err: TalerErrorDetails, err: TalerErrorDetails,
): Promise<void> { ): Promise<void> {
await ws.db.runWithWriteTransaction([Stores.exchanges], async (tx) => { await ws.db
const exchange = await tx.get(Stores.exchanges, baseUrl); .mktx((x) => ({ exchanges: x.exchanges }))
.runReadOnly(async (tx) => {
const exchange = await tx.exchanges.get(baseUrl);
if (!exchange) { if (!exchange) {
return; return;
} }
@ -153,12 +155,13 @@ async function downloadExchangeWithTermsOfService(
} }
export async function getExchangeDetails( export async function getExchangeDetails(
tx: TransactionHandle< tx: GetReadOnlyAccess<{
typeof Stores.exchanges | typeof Stores.exchangeDetails exchanges: typeof WalletStoresV1.exchanges;
>, exchangeDetails: typeof WalletStoresV1.exchangeDetails;
}>,
exchangeBaseUrl: string, exchangeBaseUrl: string,
): Promise<ExchangeDetailsRecord | undefined> { ): Promise<ExchangeDetailsRecord | undefined> {
const r = await tx.get(Stores.exchanges, exchangeBaseUrl); const r = await tx.exchanges.get(exchangeBaseUrl);
if (!r) { if (!r) {
return; return;
} }
@ -167,28 +170,32 @@ export async function getExchangeDetails(
return; return;
} }
const { currency, masterPublicKey } = dp; const { currency, masterPublicKey } = dp;
return await tx.get(Stores.exchangeDetails, [ return await tx.exchangeDetails.get([r.baseUrl, currency, masterPublicKey]);
r.baseUrl,
currency,
masterPublicKey,
]);
} }
getExchangeDetails.makeContext = (db: DbAccess<typeof WalletStoresV1>) =>
db.mktx((x) => ({
exchanges: x.exchanges,
exchangeDetails: x.exchangeDetails,
}));
export async function acceptExchangeTermsOfService( export async function acceptExchangeTermsOfService(
ws: InternalWalletState, ws: InternalWalletState,
exchangeBaseUrl: string, exchangeBaseUrl: string,
etag: string | undefined, etag: string | undefined,
): Promise<void> { ): Promise<void> {
await ws.db.runWithWriteTransaction( await ws.db
[Stores.exchanges, Stores.exchangeDetails], .mktx((x) => ({
async (tx) => { exchanges: x.exchanges,
exchangeDetails: x.exchangeDetails,
}))
.runReadWrite(async (tx) => {
const d = await getExchangeDetails(tx, exchangeBaseUrl); const d = await getExchangeDetails(tx, exchangeBaseUrl);
if (d) { if (d) {
d.termsOfServiceAcceptedEtag = etag; d.termsOfServiceAcceptedEtag = etag;
await tx.put(Stores.exchangeDetails, d); await tx.exchangeDetails.put(d);
} }
}, });
);
} }
async function validateWireInfo( async function validateWireInfo(
@ -284,9 +291,12 @@ async function provideExchangeRecord(
baseUrl: string, baseUrl: string,
now: Timestamp, now: Timestamp,
): Promise<ExchangeRecord> { ): Promise<ExchangeRecord> {
let r = await ws.db.get(Stores.exchanges, baseUrl); return await ws.db
.mktx((x) => ({ exchanges: x.exchanges }))
.runReadWrite(async (tx) => {
let r = await tx.exchanges.get(baseUrl);
if (!r) { if (!r) {
const newExchangeRecord: ExchangeRecord = { r = {
permanent: true, permanent: true,
baseUrl: baseUrl, baseUrl: baseUrl,
updateStatus: ExchangeUpdateStatus.FetchKeys, updateStatus: ExchangeUpdateStatus.FetchKeys,
@ -295,10 +305,10 @@ async function provideExchangeRecord(
retryInfo: initRetryInfo(false), retryInfo: initRetryInfo(false),
detailsPointer: undefined, detailsPointer: undefined,
}; };
await ws.db.put(Stores.exchanges, newExchangeRecord); await tx.exchanges.put(r);
r = newExchangeRecord;
} }
return r; return r;
});
} }
interface ExchangeKeysDownloadResult { interface ExchangeKeysDownloadResult {
@ -427,16 +437,17 @@ async function updateExchangeFromUrlImpl(
let recoupGroupId: string | undefined = undefined; let recoupGroupId: string | undefined = undefined;
const updated = await ws.db.runWithWriteTransaction( const updated = await ws.db
[ .mktx((x) => ({
Stores.exchanges, exchanges: x.exchanges,
Stores.exchangeDetails, exchangeDetails: x.exchangeDetails,
Stores.denominations, denominations: x.denominations,
Stores.recoupGroups, coins: x.coins,
Stores.coins, refreshGroups: x.refreshGroups,
], recoupGroups: x.recoupGroups,
async (tx) => { }))
const r = await tx.get(Stores.exchanges, baseUrl); .runReadWrite(async (tx) => {
const r = await tx.exchanges.get(baseUrl);
if (!r) { if (!r) {
logger.warn(`exchange ${baseUrl} no longer present`); logger.warn(`exchange ${baseUrl} no longer present`);
return; return;
@ -473,18 +484,18 @@ async function updateExchangeFromUrlImpl(
// FIXME: only change if pointer really changed // FIXME: only change if pointer really changed
updateClock: getTimestampNow(), updateClock: getTimestampNow(),
}; };
await tx.put(Stores.exchanges, r); await tx.exchanges.put(r);
await tx.put(Stores.exchangeDetails, details); await tx.exchangeDetails.put(details);
for (const currentDenom of keysInfo.currentDenominations) { for (const currentDenom of keysInfo.currentDenominations) {
const oldDenom = await tx.get(Stores.denominations, [ const oldDenom = await tx.denominations.get([
baseUrl, baseUrl,
currentDenom.denomPubHash, currentDenom.denomPubHash,
]); ]);
if (oldDenom) { if (oldDenom) {
// FIXME: Do consistency check // FIXME: Do consistency check
} else { } else {
await tx.put(Stores.denominations, currentDenom); await tx.denominations.put(currentDenom);
} }
} }
@ -493,7 +504,7 @@ async function updateExchangeFromUrlImpl(
const newlyRevokedCoinPubs: string[] = []; const newlyRevokedCoinPubs: string[] = [];
logger.trace("recoup list from exchange", recoupDenomList); logger.trace("recoup list from exchange", recoupDenomList);
for (const recoupInfo of recoupDenomList) { for (const recoupInfo of recoupDenomList) {
const oldDenom = await tx.get(Stores.denominations, [ const oldDenom = await tx.denominations.get([
r.baseUrl, r.baseUrl,
recoupInfo.h_denom_pub, recoupInfo.h_denom_pub,
]); ]);
@ -509,9 +520,9 @@ async function updateExchangeFromUrlImpl(
} }
logger.trace("revoking denom", recoupInfo.h_denom_pub); logger.trace("revoking denom", recoupInfo.h_denom_pub);
oldDenom.isRevoked = true; oldDenom.isRevoked = true;
await tx.put(Stores.denominations, oldDenom); await tx.denominations.put(oldDenom);
const affectedCoins = await tx const affectedCoins = await tx.coins.indexes.byDenomPubHash
.iterIndexed(Stores.coins.denomPubHashIndex, recoupInfo.h_denom_pub) .iter(recoupInfo.h_denom_pub)
.toArray(); .toArray();
for (const ac of affectedCoins) { for (const ac of affectedCoins) {
newlyRevokedCoinPubs.push(ac.coinPub); newlyRevokedCoinPubs.push(ac.coinPub);
@ -525,8 +536,7 @@ async function updateExchangeFromUrlImpl(
exchange: r, exchange: r,
exchangeDetails: details, exchangeDetails: details,
}; };
}, });
);
if (recoupGroupId) { if (recoupGroupId) {
// Asynchronously start recoup. This doesn't need to finish // Asynchronously start recoup. This doesn't need to finish
@ -553,12 +563,11 @@ export async function getExchangePaytoUri(
): Promise<string> { ): Promise<string> {
// We do the update here, since the exchange might not even exist // We do the update here, since the exchange might not even exist
// yet in our database. // yet in our database.
const details = await ws.db.runWithReadTransaction( const details = await getExchangeDetails
[Stores.exchangeDetails, Stores.exchanges], .makeContext(ws.db)
async (tx) => { .runReadOnly(async (tx) => {
return getExchangeDetails(tx, exchangeBaseUrl); return getExchangeDetails(tx, exchangeBaseUrl);
}, });
);
const accounts = details?.wireInfo.accounts ?? []; const accounts = details?.wireInfo.accounts ?? [];
for (const account of accounts) { for (const account of accounts) {
const res = parsePaytoUri(account.payto_uri); const res = parsePaytoUri(account.payto_uri);

View File

@ -72,9 +72,7 @@ import {
readSuccessResponseJsonOrErrorCode, readSuccessResponseJsonOrErrorCode,
readSuccessResponseJsonOrThrow, readSuccessResponseJsonOrThrow,
readTalerErrorResponse, readTalerErrorResponse,
Stores,
throwUnexpectedRequestError, throwUnexpectedRequestError,
TransactionHandle,
URL, URL,
WalletContractData, WalletContractData,
} from "../index.js"; } from "../index.js";
@ -85,7 +83,7 @@ import {
selectPayCoins, selectPayCoins,
PreviousPayCoins, PreviousPayCoins,
} from "../util/coinSelection.js"; } from "../util/coinSelection.js";
import { canonicalJson, j2s } from "@gnu-taler/taler-util"; import { j2s } from "@gnu-taler/taler-util";
import { import {
initRetryInfo, initRetryInfo,
updateRetryInfoTimeout, updateRetryInfoTimeout,
@ -95,6 +93,10 @@ import { getTotalRefreshCost, createRefreshGroup } from "./refresh.js";
import { InternalWalletState, EXCHANGE_COINS_LOCK } from "./state.js"; import { InternalWalletState, EXCHANGE_COINS_LOCK } from "./state.js";
import { ContractTermsUtil } from "../util/contractTerms.js"; import { ContractTermsUtil } from "../util/contractTerms.js";
import { getExchangeDetails } from "./exchanges.js"; import { getExchangeDetails } from "./exchanges.js";
import { DbAccess, GetReadWriteAccess } from "../util/query.js";
import { WalletStoresV1 } from "../db.js";
import { Wallet } from "../wallet.js";
import { x25519_edwards_keyPair_fromSecretKey } from "../crypto/primitives/nacl-fast.js";
/** /**
* Logger. * Logger.
@ -112,13 +114,16 @@ export async function getTotalPaymentCost(
ws: InternalWalletState, ws: InternalWalletState,
pcs: PayCoinSelection, pcs: PayCoinSelection,
): Promise<AmountJson> { ): Promise<AmountJson> {
return ws.db
.mktx((x) => ({ coins: x.coins, denominations: x.denominations }))
.runReadOnly(async (tx) => {
const costs = []; const costs = [];
for (let i = 0; i < pcs.coinPubs.length; i++) { for (let i = 0; i < pcs.coinPubs.length; i++) {
const coin = await ws.db.get(Stores.coins, pcs.coinPubs[i]); const coin = await tx.coins.get(pcs.coinPubs[i]);
if (!coin) { if (!coin) {
throw Error("can't calculate payment cost, coin not found"); throw Error("can't calculate payment cost, coin not found");
} }
const denom = await ws.db.get(Stores.denominations, [ const denom = await tx.denominations.get([
coin.exchangeBaseUrl, coin.exchangeBaseUrl,
coin.denomPubHash, coin.denomPubHash,
]); ]);
@ -127,11 +132,8 @@ export async function getTotalPaymentCost(
"can't calculate payment cost, denomination for coin not found", "can't calculate payment cost, denomination for coin not found",
); );
} }
const allDenoms = await ws.db const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
.iterIndex( .iter()
Stores.denominations.exchangeBaseUrlIndex,
coin.exchangeBaseUrl,
)
.toArray(); .toArray();
const amountLeft = Amounts.sub(denom.value, pcs.coinContributions[i]) const amountLeft = Amounts.sub(denom.value, pcs.coinContributions[i])
.amount; .amount;
@ -140,6 +142,7 @@ export async function getTotalPaymentCost(
costs.push(refreshCost); costs.push(refreshCost);
} }
return Amounts.sum(costs).amount; return Amounts.sum(costs).amount;
});
} }
/** /**
@ -154,12 +157,21 @@ export async function getEffectiveDepositAmount(
const amt: AmountJson[] = []; const amt: AmountJson[] = [];
const fees: AmountJson[] = []; const fees: AmountJson[] = [];
const exchangeSet: Set<string> = new Set(); const exchangeSet: Set<string> = new Set();
await ws.db
.mktx((x) => ({
coins: x.coins,
denominations: x.denominations,
exchanges: x.exchanges,
exchangeDetails: x.exchangeDetails,
}))
.runReadOnly(async (tx) => {
for (let i = 0; i < pcs.coinPubs.length; i++) { for (let i = 0; i < pcs.coinPubs.length; i++) {
const coin = await ws.db.get(Stores.coins, pcs.coinPubs[i]); const coin = await tx.coins.get(pcs.coinPubs[i]);
if (!coin) { if (!coin) {
throw Error("can't calculate deposit amountt, coin not found"); throw Error("can't calculate deposit amountt, coin not found");
} }
const denom = await ws.db.get(Stores.denominations, [ const denom = await tx.denominations.get([
coin.exchangeBaseUrl, coin.exchangeBaseUrl,
coin.denomPubHash, coin.denomPubHash,
]); ]);
@ -171,22 +183,22 @@ export async function getEffectiveDepositAmount(
exchangeSet.add(coin.exchangeBaseUrl); exchangeSet.add(coin.exchangeBaseUrl);
} }
for (const exchangeUrl of exchangeSet.values()) { for (const exchangeUrl of exchangeSet.values()) {
const exchangeDetails = await ws.db.runWithReadTransaction( const exchangeDetails = await getExchangeDetails(tx, exchangeUrl);
[Stores.exchanges, Stores.exchangeDetails],
async (tx) => {
return getExchangeDetails(tx, exchangeUrl);
},
);
if (!exchangeDetails) { if (!exchangeDetails) {
continue; continue;
} }
const fee = exchangeDetails.wireInfo.feesForType[wireType].find((x) => { const fee = exchangeDetails.wireInfo.feesForType[wireType].find((x) => {
return timestampIsBetween(getTimestampNow(), x.startStamp, x.endStamp); return timestampIsBetween(
getTimestampNow(),
x.startStamp,
x.endStamp,
);
})?.wireFee; })?.wireFee;
if (fee) { if (fee) {
fees.push(fee); fees.push(fee);
} }
} }
});
return Amounts.sub(Amounts.sum(amt).amount, Amounts.sum(fees).amount).amount; return Amounts.sub(Amounts.sum(amt).amount, Amounts.sum(fees).amount).amount;
} }
@ -243,15 +255,18 @@ export async function getCandidatePayCoins(
const candidateCoins: AvailableCoinInfo[] = []; const candidateCoins: AvailableCoinInfo[] = [];
const wireFeesPerExchange: Record<string, AmountJson> = {}; const wireFeesPerExchange: Record<string, AmountJson> = {};
const exchanges = await ws.db.iter(Stores.exchanges).toArray(); await ws.db
.mktx((x) => ({
exchanges: x.exchanges,
exchangeDetails: x.exchangeDetails,
denominations: x.denominations,
coins: x.coins,
}))
.runReadOnly(async (tx) => {
const exchanges = await tx.exchanges.iter().toArray();
for (const exchange of exchanges) { for (const exchange of exchanges) {
let isOkay = false; let isOkay = false;
const exchangeDetails = await ws.db.runWithReadTransaction( const exchangeDetails = await getExchangeDetails(tx, exchange.baseUrl);
[Stores.exchanges, Stores.exchangeDetails],
async (tx) => {
return getExchangeDetails(tx, exchange.baseUrl);
},
);
if (!exchangeDetails) { if (!exchangeDetails) {
continue; continue;
} }
@ -287,8 +302,8 @@ export async function getCandidatePayCoins(
continue; continue;
} }
const coins = await ws.db const coins = await tx.coins.indexes.byBaseUrl
.iterIndex(Stores.coins.exchangeBaseUrlIndex, exchange.baseUrl) .iter(exchange.baseUrl)
.toArray(); .toArray();
if (!coins || coins.length === 0) { if (!coins || coins.length === 0) {
@ -297,7 +312,7 @@ export async function getCandidatePayCoins(
// Denomination of the first coin, we assume that all other // Denomination of the first coin, we assume that all other
// coins have the same currency // coins have the same currency
const firstDenom = await ws.db.get(Stores.denominations, [ const firstDenom = await tx.denominations.get([
exchange.baseUrl, exchange.baseUrl,
coins[0].denomPubHash, coins[0].denomPubHash,
]); ]);
@ -306,7 +321,7 @@ export async function getCandidatePayCoins(
} }
const currency = firstDenom.value.currency; const currency = firstDenom.value.currency;
for (const coin of coins) { for (const coin of coins) {
const denom = await ws.db.get(Stores.denominations, [ const denom = await tx.denominations.get([
exchange.baseUrl, exchange.baseUrl,
coin.denomPubHash, coin.denomPubHash,
]); ]);
@ -333,7 +348,10 @@ export async function getCandidatePayCoins(
let wireFee: AmountJson | undefined; let wireFee: AmountJson | undefined;
for (const fee of exchangeFees.feesForType[req.wireMethod] || []) { for (const fee of exchangeFees.feesForType[req.wireMethod] || []) {
if (fee.startStamp <= req.timestamp && fee.endStamp >= req.timestamp) { if (
fee.startStamp <= req.timestamp &&
fee.endStamp >= req.timestamp
) {
wireFee = fee.wireFee; wireFee = fee.wireFee;
break; break;
} }
@ -342,6 +360,7 @@ export async function getCandidatePayCoins(
wireFeesPerExchange[exchange.baseUrl] = wireFee; wireFeesPerExchange[exchange.baseUrl] = wireFee;
} }
} }
});
return { return {
candidateCoins, candidateCoins,
@ -351,15 +370,15 @@ export async function getCandidatePayCoins(
export async function applyCoinSpend( export async function applyCoinSpend(
ws: InternalWalletState, ws: InternalWalletState,
tx: TransactionHandle< tx: GetReadWriteAccess<{
| typeof Stores.coins coins: typeof WalletStoresV1.coins;
| typeof Stores.refreshGroups refreshGroups: typeof WalletStoresV1.refreshGroups;
| typeof Stores.denominations denominations: typeof WalletStoresV1.denominations;
>, }>,
coinSelection: PayCoinSelection, coinSelection: PayCoinSelection,
) { ) {
for (let i = 0; i < coinSelection.coinPubs.length; i++) { for (let i = 0; i < coinSelection.coinPubs.length; i++) {
const coin = await tx.get(Stores.coins, coinSelection.coinPubs[i]); const coin = await tx.coins.get(coinSelection.coinPubs[i]);
if (!coin) { if (!coin) {
throw Error("coin allocated for payment doesn't exist anymore"); throw Error("coin allocated for payment doesn't exist anymore");
} }
@ -379,7 +398,7 @@ export async function applyCoinSpend(
throw Error("not enough remaining balance on coin for payment"); throw Error("not enough remaining balance on coin for payment");
} }
coin.currentAmount = remaining.amount; coin.currentAmount = remaining.amount;
await tx.put(Stores.coins, coin); await tx.coins.put(coin);
} }
const refreshCoinPubs = coinSelection.coinPubs.map((x) => ({ const refreshCoinPubs = coinSelection.coinPubs.map((x) => ({
coinPub: x, coinPub: x,
@ -437,26 +456,25 @@ async function recordConfirmPay(
noncePub: proposal.noncePub, noncePub: proposal.noncePub,
}; };
await ws.db.runWithWriteTransaction( await ws.db
[ .mktx((x) => ({
Stores.coins, proposals: x.proposals,
Stores.purchases, purchases: x.purchases,
Stores.proposals, coins: x.coins,
Stores.refreshGroups, refreshGroups: x.refreshGroups,
Stores.denominations, denominations: x.denominations,
], }))
async (tx) => { .runReadWrite(async (tx) => {
const p = await tx.get(Stores.proposals, proposal.proposalId); const p = await tx.proposals.get(proposal.proposalId);
if (p) { if (p) {
p.proposalStatus = ProposalStatus.ACCEPTED; p.proposalStatus = ProposalStatus.ACCEPTED;
p.lastError = undefined; p.lastError = undefined;
p.retryInfo = initRetryInfo(false); p.retryInfo = initRetryInfo(false);
await tx.put(Stores.proposals, p); await tx.proposals.put(p);
} }
await tx.put(Stores.purchases, t); await tx.purchases.put(t);
await applyCoinSpend(ws, tx, coinSelection); await applyCoinSpend(ws, tx, coinSelection);
}, });
);
ws.notify({ ws.notify({
type: NotificationType.ProposalAccepted, type: NotificationType.ProposalAccepted,
@ -470,8 +488,10 @@ async function incrementProposalRetry(
proposalId: string, proposalId: string,
err: TalerErrorDetails | undefined, err: TalerErrorDetails | undefined,
): Promise<void> { ): Promise<void> {
await ws.db.runWithWriteTransaction([Stores.proposals], async (tx) => { await ws.db
const pr = await tx.get(Stores.proposals, proposalId); .mktx((x) => ({ proposals: x.proposals }))
.runReadWrite(async (tx) => {
const pr = await tx.proposals.get(proposalId);
if (!pr) { if (!pr) {
return; return;
} }
@ -481,7 +501,7 @@ async function incrementProposalRetry(
pr.retryInfo.retryCounter++; pr.retryInfo.retryCounter++;
updateRetryInfoTimeout(pr.retryInfo); updateRetryInfoTimeout(pr.retryInfo);
pr.lastError = err; pr.lastError = err;
await tx.put(Stores.proposals, pr); await tx.proposals.put(pr);
}); });
if (err) { if (err) {
ws.notify({ type: NotificationType.ProposalOperationError, error: err }); ws.notify({ type: NotificationType.ProposalOperationError, error: err });
@ -494,8 +514,10 @@ async function incrementPurchasePayRetry(
err: TalerErrorDetails | undefined, err: TalerErrorDetails | undefined,
): Promise<void> { ): Promise<void> {
logger.warn("incrementing purchase pay retry with error", err); logger.warn("incrementing purchase pay retry with error", err);
await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => { await ws.db
const pr = await tx.get(Stores.purchases, proposalId); .mktx((x) => ({ purchases: x.purchases }))
.runReadWrite(async (tx) => {
const pr = await tx.purchases.get(proposalId);
if (!pr) { if (!pr) {
return; return;
} }
@ -505,7 +527,7 @@ async function incrementPurchasePayRetry(
pr.payRetryInfo.retryCounter++; pr.payRetryInfo.retryCounter++;
updateRetryInfoTimeout(pr.payRetryInfo); updateRetryInfoTimeout(pr.payRetryInfo);
pr.lastPayError = err; pr.lastPayError = err;
await tx.put(Stores.purchases, pr); await tx.purchases.put(pr);
}); });
if (err) { if (err) {
ws.notify({ type: NotificationType.PayOperationError, error: err }); ws.notify({ type: NotificationType.PayOperationError, error: err });
@ -529,11 +551,14 @@ async function resetDownloadProposalRetry(
ws: InternalWalletState, ws: InternalWalletState,
proposalId: string, proposalId: string,
): Promise<void> { ): Promise<void> {
await ws.db.mutate(Stores.proposals, proposalId, (x) => { await ws.db
if (x.retryInfo.active) { .mktx((x) => ({ proposals: x.proposals }))
x.retryInfo = initRetryInfo(); .runReadWrite(async (tx) => {
const p = await tx.proposals.get(proposalId);
if (p && p.retryInfo.active) {
p.retryInfo = initRetryInfo();
await tx.proposals.put(p);
} }
return x;
}); });
} }
@ -542,11 +567,17 @@ async function failProposalPermanently(
proposalId: string, proposalId: string,
err: TalerErrorDetails, err: TalerErrorDetails,
): Promise<void> { ): Promise<void> {
await ws.db.mutate(Stores.proposals, proposalId, (x) => { await ws.db
x.retryInfo.active = false; .mktx((x) => ({ proposals: x.proposals }))
x.lastError = err; .runReadWrite(async (tx) => {
x.proposalStatus = ProposalStatus.PERMANENTLY_FAILED; const p = await tx.proposals.get(proposalId);
return x; if (!p) {
return;
}
p.retryInfo.active = false;
p.lastError = err;
p.proposalStatus = ProposalStatus.PERMANENTLY_FAILED;
await tx.proposals.put(p);
}); });
} }
@ -616,7 +647,11 @@ async function processDownloadProposalImpl(
if (forceNow) { if (forceNow) {
await resetDownloadProposalRetry(ws, proposalId); await resetDownloadProposalRetry(ws, proposalId);
} }
const proposal = await ws.db.get(Stores.proposals, proposalId); const proposal = await ws.db
.mktx((x) => ({ proposals: x.proposals }))
.runReadOnly(async (tx) => {
return tx.proposals.get(proposalId);
});
if (!proposal) { if (!proposal) {
return; return;
} }
@ -750,10 +785,10 @@ async function processDownloadProposalImpl(
proposalResp.sig, proposalResp.sig,
); );
await ws.db.runWithWriteTransaction( await ws.db
[Stores.proposals, Stores.purchases], .mktx((x) => ({ proposals: x.proposals, purchases: x.purchases }))
async (tx) => { .runReadWrite(async (tx) => {
const p = await tx.get(Stores.proposals, proposalId); const p = await tx.proposals.get(proposalId);
if (!p) { if (!p) {
return; return;
} }
@ -769,22 +804,20 @@ async function processDownloadProposalImpl(
(fulfillmentUrl.startsWith("http://") || (fulfillmentUrl.startsWith("http://") ||
fulfillmentUrl.startsWith("https://")) fulfillmentUrl.startsWith("https://"))
) { ) {
const differentPurchase = await tx.getIndexed( const differentPurchase = await tx.purchases.indexes.byFulfillmentUrl.get(
Stores.purchases.fulfillmentUrlIndex,
fulfillmentUrl, fulfillmentUrl,
); );
if (differentPurchase) { if (differentPurchase) {
logger.warn("repurchase detected"); logger.warn("repurchase detected");
p.proposalStatus = ProposalStatus.REPURCHASE; p.proposalStatus = ProposalStatus.REPURCHASE;
p.repurchaseProposalId = differentPurchase.proposalId; p.repurchaseProposalId = differentPurchase.proposalId;
await tx.put(Stores.proposals, p); await tx.proposals.put(p);
return; return;
} }
} }
p.proposalStatus = ProposalStatus.PROPOSED; p.proposalStatus = ProposalStatus.PROPOSED;
await tx.put(Stores.proposals, p); await tx.proposals.put(p);
}, });
);
ws.notify({ ws.notify({
type: NotificationType.ProposalDownloaded, type: NotificationType.ProposalDownloaded,
@ -806,10 +839,14 @@ async function startDownloadProposal(
sessionId: string | undefined, sessionId: string | undefined,
claimToken: string | undefined, claimToken: string | undefined,
): Promise<string> { ): Promise<string> {
const oldProposal = await ws.db.getIndexed( const oldProposal = await ws.db
Stores.proposals.urlAndOrderIdIndex, .mktx((x) => ({ proposals: x.proposals }))
[merchantBaseUrl, orderId], .runReadOnly(async (tx) => {
); return tx.proposals.indexes.byUrlAndOrderId.get([
merchantBaseUrl,
orderId,
]);
});
if (oldProposal) { if (oldProposal) {
await processDownloadProposal(ws, oldProposal.proposalId); await processDownloadProposal(ws, oldProposal.proposalId);
return oldProposal.proposalId; return oldProposal.proposalId;
@ -834,16 +871,18 @@ async function startDownloadProposal(
downloadSessionId: sessionId, downloadSessionId: sessionId,
}; };
await ws.db.runWithWriteTransaction([Stores.proposals], async (tx) => { await ws.db
const existingRecord = await tx.getIndexed( .mktx((x) => ({ proposals: x.proposals }))
Stores.proposals.urlAndOrderIdIndex, .runReadWrite(async (tx) => {
[merchantBaseUrl, orderId], const existingRecord = tx.proposals.indexes.byUrlAndOrderId.get([
); merchantBaseUrl,
orderId,
]);
if (existingRecord) { if (existingRecord) {
// Created concurrently // Created concurrently
return; return;
} }
await tx.put(Stores.proposals, proposalRecord); await tx.proposals.put(proposalRecord);
}); });
await processDownloadProposal(ws, proposalId); await processDownloadProposal(ws, proposalId);
@ -857,8 +896,10 @@ async function storeFirstPaySuccess(
paySig: string, paySig: string,
): Promise<void> { ): Promise<void> {
const now = getTimestampNow(); const now = getTimestampNow();
await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => { await ws.db
const purchase = await tx.get(Stores.purchases, proposalId); .mktx((x) => ({ purchases: x.purchases }))
.runReadWrite(async (tx) => {
const purchase = await tx.purchases.get(proposalId);
if (!purchase) { if (!purchase) {
logger.warn("purchase does not exist anymore"); logger.warn("purchase does not exist anymore");
@ -885,8 +926,7 @@ async function storeFirstPaySuccess(
purchase.autoRefundDeadline = timestampAddDuration(now, ar); purchase.autoRefundDeadline = timestampAddDuration(now, ar);
} }
} }
await tx.purchases.put(purchase);
await tx.put(Stores.purchases, purchase);
}); });
} }
@ -895,8 +935,10 @@ async function storePayReplaySuccess(
proposalId: string, proposalId: string,
sessionId: string | undefined, sessionId: string | undefined,
): Promise<void> { ): Promise<void> {
await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => { await ws.db
const purchase = await tx.get(Stores.purchases, proposalId); .mktx((x) => ({ purchases: x.purchases }))
.runReadWrite(async (tx) => {
const purchase = await tx.purchases.get(proposalId);
if (!purchase) { if (!purchase) {
logger.warn("purchase does not exist anymore"); logger.warn("purchase does not exist anymore");
@ -910,7 +952,7 @@ async function storePayReplaySuccess(
purchase.lastPayError = undefined; purchase.lastPayError = undefined;
purchase.payRetryInfo = initRetryInfo(false); purchase.payRetryInfo = initRetryInfo(false);
purchase.lastSessionId = sessionId; purchase.lastSessionId = sessionId;
await tx.put(Stores.purchases, purchase); await tx.purchases.put(purchase);
}); });
} }
@ -929,7 +971,11 @@ async function handleInsufficientFunds(
): Promise<void> { ): Promise<void> {
logger.trace("handling insufficient funds, trying to re-select coins"); logger.trace("handling insufficient funds, trying to re-select coins");
const proposal = await ws.db.get(Stores.purchases, proposalId); const proposal = await ws.db
.mktx((x) => ({ purchaes: x.purchases }))
.runReadOnly(async (tx) => {
return tx.purchaes.get(proposalId);
});
if (!proposal) { if (!proposal) {
return; return;
} }
@ -961,17 +1007,20 @@ async function handleInsufficientFunds(
const prevPayCoins: PreviousPayCoins = []; const prevPayCoins: PreviousPayCoins = [];
await ws.db
.mktx((x) => ({ coins: x.coins, denominations: x.denominations }))
.runReadOnly(async (tx) => {
for (let i = 0; i < proposal.payCoinSelection.coinPubs.length; i++) { for (let i = 0; i < proposal.payCoinSelection.coinPubs.length; i++) {
const coinPub = proposal.payCoinSelection.coinPubs[i]; const coinPub = proposal.payCoinSelection.coinPubs[i];
if (coinPub === brokenCoinPub) { if (coinPub === brokenCoinPub) {
continue; continue;
} }
const contrib = proposal.payCoinSelection.coinContributions[i]; const contrib = proposal.payCoinSelection.coinContributions[i];
const coin = await ws.db.get(Stores.coins, coinPub); const coin = await tx.coins.get(coinPub);
if (!coin) { if (!coin) {
continue; continue;
} }
const denom = await ws.db.get(Stores.denominations, [ const denom = await tx.denominations.get([
coin.exchangeBaseUrl, coin.exchangeBaseUrl,
coin.denomPubHash, coin.denomPubHash,
]); ]);
@ -985,6 +1034,7 @@ async function handleInsufficientFunds(
feeDeposit: denom.feeDeposit, feeDeposit: denom.feeDeposit,
}); });
} }
});
const res = selectPayCoins({ const res = selectPayCoins({
candidates, candidates,
@ -1002,24 +1052,23 @@ async function handleInsufficientFunds(
logger.trace("re-selected coins"); logger.trace("re-selected coins");
await ws.db.runWithWriteTransaction( await ws.db
[ .mktx((x) => ({
Stores.purchases, purchases: x.purchases,
Stores.coins, coins: x.coins,
Stores.denominations, denominations: x.denominations,
Stores.refreshGroups, refreshGroups: x.refreshGroups,
], }))
async (tx) => { .runReadWrite(async (tx) => {
const p = await tx.get(Stores.purchases, proposalId); const p = await tx.purchases.get(proposalId);
if (!p) { if (!p) {
return; return;
} }
p.payCoinSelection = res; p.payCoinSelection = res;
p.coinDepositPermissions = undefined; p.coinDepositPermissions = undefined;
await tx.put(Stores.purchases, p); await tx.purchases.put(p);
await applyCoinSpend(ws, tx, res); await applyCoinSpend(ws, tx, res);
}, });
);
} }
/** /**
@ -1032,7 +1081,11 @@ async function submitPay(
ws: InternalWalletState, ws: InternalWalletState,
proposalId: string, proposalId: string,
): Promise<ConfirmPayResult> { ): Promise<ConfirmPayResult> {
const purchase = await ws.db.get(Stores.purchases, proposalId); const purchase = await ws.db
.mktx((x) => ({ purchases: x.purchases }))
.runReadOnly(async (tx) => {
return tx.purchases.get(proposalId);
});
if (!purchase) { if (!purchase) {
throw Error("Purchase not found: " + proposalId); throw Error("Purchase not found: " + proposalId);
} }
@ -1202,7 +1255,11 @@ export async function checkPaymentByProposalId(
proposalId: string, proposalId: string,
sessionId?: string, sessionId?: string,
): Promise<PreparePayResult> { ): Promise<PreparePayResult> {
let proposal = await ws.db.get(Stores.proposals, proposalId); let proposal = await ws.db
.mktx((x) => ({ proposals: x.proposals }))
.runReadOnly(async (tx) => {
return tx.proposals.get(proposalId);
});
if (!proposal) { if (!proposal) {
throw Error(`could not get proposal ${proposalId}`); throw Error(`could not get proposal ${proposalId}`);
} }
@ -1212,7 +1269,11 @@ export async function checkPaymentByProposalId(
throw Error("invalid proposal state"); throw Error("invalid proposal state");
} }
logger.trace("using existing purchase for same product"); logger.trace("using existing purchase for same product");
proposal = await ws.db.get(Stores.proposals, existingProposalId); proposal = await ws.db
.mktx((x) => ({ proposals: x.proposals }))
.runReadOnly(async (tx) => {
return tx.proposals.get(existingProposalId);
});
if (!proposal) { if (!proposal) {
throw Error("existing proposal is in wrong state"); throw Error("existing proposal is in wrong state");
} }
@ -1231,7 +1292,11 @@ export async function checkPaymentByProposalId(
proposalId = proposal.proposalId; proposalId = proposal.proposalId;
// First check if we already paid for it. // First check if we already paid for it.
const purchase = await ws.db.get(Stores.purchases, proposalId); const purchase = await ws.db
.mktx((x) => ({ purchases: x.purchases }))
.runReadOnly(async (tx) => {
return tx.purchases.get(proposalId);
});
if (!purchase) { if (!purchase) {
// If not already paid, check if we could pay for it. // If not already paid, check if we could pay for it.
@ -1281,13 +1346,15 @@ export async function checkPaymentByProposalId(
logger.trace( logger.trace(
"automatically re-submitting payment with different session ID", "automatically re-submitting payment with different session ID",
); );
await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => { await ws.db
const p = await tx.get(Stores.purchases, proposalId); .mktx((x) => ({ purchases: x.purchases }))
.runReadWrite(async (tx) => {
const p = await tx.purchases.get(proposalId);
if (!p) { if (!p) {
return; return;
} }
p.lastSessionId = sessionId; p.lastSessionId = sessionId;
await tx.put(Stores.purchases, p); await tx.purchases.put(p);
}); });
const r = await guardOperationException( const r = await guardOperationException(
() => submitPay(ws, proposalId), () => submitPay(ws, proposalId),
@ -1375,12 +1442,19 @@ export async function generateDepositPermissions(
contractData: WalletContractData, contractData: WalletContractData,
): Promise<CoinDepositPermission[]> { ): Promise<CoinDepositPermission[]> {
const depositPermissions: CoinDepositPermission[] = []; const depositPermissions: CoinDepositPermission[] = [];
const coinWithDenom: Array<{
coin: CoinRecord;
denom: DenominationRecord;
}> = [];
await ws.db
.mktx((x) => ({ coins: x.coins, denominations: x.denominations }))
.runReadOnly(async (tx) => {
for (let i = 0; i < payCoinSel.coinPubs.length; i++) { for (let i = 0; i < payCoinSel.coinPubs.length; i++) {
const coin = await ws.db.get(Stores.coins, payCoinSel.coinPubs[i]); const coin = await tx.coins.get(payCoinSel.coinPubs[i]);
if (!coin) { if (!coin) {
throw Error("can't pay, allocated coin not found anymore"); throw Error("can't pay, allocated coin not found anymore");
} }
const denom = await ws.db.get(Stores.denominations, [ const denom = await tx.denominations.get([
coin.exchangeBaseUrl, coin.exchangeBaseUrl,
coin.denomPubHash, coin.denomPubHash,
]); ]);
@ -1389,6 +1463,12 @@ export async function generateDepositPermissions(
"can't pay, denomination of allocated coin not found anymore", "can't pay, denomination of allocated coin not found anymore",
); );
} }
coinWithDenom.push({ coin, denom });
}
});
for (let i = 0; i < payCoinSel.coinPubs.length; i++) {
const { coin, denom } = coinWithDenom[i];
const dp = await ws.cryptoApi.signDepositPermission({ const dp = await ws.cryptoApi.signDepositPermission({
coinPriv: coin.coinPriv, coinPriv: coin.coinPriv,
coinPub: coin.coinPub, coinPub: coin.coinPub,
@ -1419,7 +1499,11 @@ export async function confirmPay(
logger.trace( logger.trace(
`executing confirmPay with proposalId ${proposalId} and sessionIdOverride ${sessionIdOverride}`, `executing confirmPay with proposalId ${proposalId} and sessionIdOverride ${sessionIdOverride}`,
); );
const proposal = await ws.db.get(Stores.proposals, proposalId); const proposal = await ws.db
.mktx((x) => ({ proposals: x.proposals }))
.runReadOnly(async (tx) => {
return tx.proposals.get(proposalId);
});
if (!proposal) { if (!proposal) {
throw Error(`proposal with id ${proposalId} not found`); throw Error(`proposal with id ${proposalId} not found`);
@ -1430,20 +1514,24 @@ export async function confirmPay(
throw Error("proposal is in invalid state"); throw Error("proposal is in invalid state");
} }
let purchase = await ws.db.get(Stores.purchases, proposalId); const existingPurchase = await ws.db
.mktx((x) => ({ purchases: x.purchases }))
if (purchase) { .runReadWrite(async (tx) => {
const purchase = await tx.purchases.get(proposalId);
if ( if (
purchase &&
sessionIdOverride !== undefined && sessionIdOverride !== undefined &&
sessionIdOverride != purchase.lastSessionId sessionIdOverride != purchase.lastSessionId
) { ) {
logger.trace(`changing session ID to ${sessionIdOverride}`); logger.trace(`changing session ID to ${sessionIdOverride}`);
await ws.db.mutate(Stores.purchases, purchase.proposalId, (x) => { purchase.lastSessionId = sessionIdOverride;
x.lastSessionId = sessionIdOverride; purchase.paymentSubmitPending = true;
x.paymentSubmitPending = true; await tx.purchases.put(purchase);
return x;
});
} }
return purchase;
});
if (existingPurchase) {
logger.trace("confirmPay: submitting payment for existing purchase"); logger.trace("confirmPay: submitting payment for existing purchase");
return await guardOperationException( return await guardOperationException(
() => submitPay(ws, proposalId), () => submitPay(ws, proposalId),
@ -1491,7 +1579,7 @@ export async function confirmPay(
res, res,
d.contractData, d.contractData,
); );
purchase = await recordConfirmPay( await recordConfirmPay(
ws, ws,
proposal, proposal,
res, res,
@ -1523,11 +1611,14 @@ async function resetPurchasePayRetry(
ws: InternalWalletState, ws: InternalWalletState,
proposalId: string, proposalId: string,
): Promise<void> { ): Promise<void> {
await ws.db.mutate(Stores.purchases, proposalId, (x) => { await ws.db
if (x.payRetryInfo.active) { .mktx((x) => ({ purchases: x.purchases }))
x.payRetryInfo = initRetryInfo(); .runReadWrite(async (tx) => {
const p = await tx.purchases.get(proposalId);
if (p) {
p.payRetryInfo = initRetryInfo();
await tx.purchases.put(p);
} }
return x;
}); });
} }
@ -1539,7 +1630,11 @@ async function processPurchasePayImpl(
if (forceNow) { if (forceNow) {
await resetPurchasePayRetry(ws, proposalId); await resetPurchasePayRetry(ws, proposalId);
} }
const purchase = await ws.db.get(Stores.purchases, proposalId); const purchase = await ws.db
.mktx((x) => ({ purchases: x.purchases }))
.runReadOnly(async (tx) => {
return tx.purchases.get(proposalId);
});
if (!purchase) { if (!purchase) {
return; return;
} }
@ -1554,10 +1649,9 @@ export async function refuseProposal(
ws: InternalWalletState, ws: InternalWalletState,
proposalId: string, proposalId: string,
): Promise<void> { ): Promise<void> {
const success = await ws.db.runWithWriteTransaction( const success = await ws.db.mktx((x) => ({proposals: x.proposals})).runReadWrite(
[Stores.proposals],
async (tx) => { async (tx) => {
const proposal = await tx.get(Stores.proposals, proposalId); const proposal = await tx.proposals.get(proposalId);
if (!proposal) { if (!proposal) {
logger.trace(`proposal ${proposalId} not found, won't refuse proposal`); logger.trace(`proposal ${proposalId} not found, won't refuse proposal`);
return false; return false;
@ -1566,7 +1660,7 @@ export async function refuseProposal(
return false; return false;
} }
proposal.proposalStatus = ProposalStatus.REFUSED; proposal.proposalStatus = ProposalStatus.REFUSED;
await tx.put(Stores.proposals, proposal); await tx.proposals.put(proposal);
return true; return true;
}, },
); );

View File

@ -21,8 +21,8 @@ import {
ExchangeUpdateStatus, ExchangeUpdateStatus,
ProposalStatus, ProposalStatus,
ReserveRecordStatus, ReserveRecordStatus,
Stores,
AbortStatus, AbortStatus,
WalletStoresV1,
} from "../db.js"; } from "../db.js";
import { import {
PendingOperationsResponse, PendingOperationsResponse,
@ -37,10 +37,10 @@ import {
getDurationRemaining, getDurationRemaining,
durationMin, durationMin,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { TransactionHandle } from "../util/query";
import { InternalWalletState } from "./state"; import { InternalWalletState } from "./state";
import { getBalancesInsideTransaction } from "./balance"; import { getBalancesInsideTransaction } from "./balance";
import { getExchangeDetails } from "./exchanges.js"; import { getExchangeDetails } from "./exchanges.js";
import { GetReadOnlyAccess } from "../util/query.js";
function updateRetryDelay( function updateRetryDelay(
oldDelay: Duration, oldDelay: Duration,
@ -53,14 +53,15 @@ function updateRetryDelay(
} }
async function gatherExchangePending( async function gatherExchangePending(
tx: TransactionHandle< tx: GetReadOnlyAccess<{
typeof Stores.exchanges | typeof Stores.exchangeDetails exchanges: typeof WalletStoresV1.exchanges;
>, exchangeDetails: typeof WalletStoresV1.exchangeDetails;
}>,
now: Timestamp, now: Timestamp,
resp: PendingOperationsResponse, resp: PendingOperationsResponse,
onlyDue = false, onlyDue = false,
): Promise<void> { ): Promise<void> {
await tx.iter(Stores.exchanges).forEachAsync(async (e) => { await tx.exchanges.iter().forEachAsync(async (e) => {
switch (e.updateStatus) { switch (e.updateStatus) {
case ExchangeUpdateStatus.Finished: case ExchangeUpdateStatus.Finished:
if (e.lastError) { if (e.lastError) {
@ -153,13 +154,13 @@ async function gatherExchangePending(
} }
async function gatherReservePending( async function gatherReservePending(
tx: TransactionHandle<typeof Stores.reserves>, tx: GetReadOnlyAccess<{ reserves: typeof WalletStoresV1.reserves }>,
now: Timestamp, now: Timestamp,
resp: PendingOperationsResponse, resp: PendingOperationsResponse,
onlyDue = false, onlyDue = false,
): Promise<void> { ): Promise<void> {
// FIXME: this should be optimized by using an index for "onlyDue==true". // FIXME: this should be optimized by using an index for "onlyDue==true".
await tx.iter(Stores.reserves).forEach((reserve) => { await tx.reserves.iter().forEach((reserve) => {
const reserveType = reserve.bankInfo const reserveType = reserve.bankInfo
? ReserveType.TalerBankWithdraw ? ReserveType.TalerBankWithdraw
: ReserveType.Manual; : ReserveType.Manual;
@ -207,12 +208,12 @@ async function gatherReservePending(
} }
async function gatherRefreshPending( async function gatherRefreshPending(
tx: TransactionHandle<typeof Stores.refreshGroups>, tx: GetReadOnlyAccess<{ refreshGroups: typeof WalletStoresV1.refreshGroups }>,
now: Timestamp, now: Timestamp,
resp: PendingOperationsResponse, resp: PendingOperationsResponse,
onlyDue = false, onlyDue = false,
): Promise<void> { ): Promise<void> {
await tx.iter(Stores.refreshGroups).forEach((r) => { await tx.refreshGroups.iter().forEach((r) => {
if (r.timestampFinished) { if (r.timestampFinished) {
return; return;
} }
@ -236,12 +237,15 @@ async function gatherRefreshPending(
} }
async function gatherWithdrawalPending( async function gatherWithdrawalPending(
tx: TransactionHandle<typeof Stores.withdrawalGroups>, tx: GetReadOnlyAccess<{
withdrawalGroups: typeof WalletStoresV1.withdrawalGroups;
planchets: typeof WalletStoresV1.planchets,
}>,
now: Timestamp, now: Timestamp,
resp: PendingOperationsResponse, resp: PendingOperationsResponse,
onlyDue = false, onlyDue = false,
): Promise<void> { ): Promise<void> {
await tx.iter(Stores.withdrawalGroups).forEachAsync(async (wsr) => { await tx.withdrawalGroups.iter().forEachAsync(async (wsr) => {
if (wsr.timestampFinish) { if (wsr.timestampFinish) {
return; return;
} }
@ -255,8 +259,8 @@ async function gatherWithdrawalPending(
} }
let numCoinsWithdrawn = 0; let numCoinsWithdrawn = 0;
let numCoinsTotal = 0; let numCoinsTotal = 0;
await tx await tx.planchets.indexes.byGroup
.iterIndexed(Stores.planchets.byGroup, wsr.withdrawalGroupId) .iter(wsr.withdrawalGroupId)
.forEach((x) => { .forEach((x) => {
numCoinsTotal++; numCoinsTotal++;
if (x.withdrawalDone) { if (x.withdrawalDone) {
@ -276,12 +280,12 @@ async function gatherWithdrawalPending(
} }
async function gatherProposalPending( async function gatherProposalPending(
tx: TransactionHandle<typeof Stores.proposals>, tx: GetReadOnlyAccess<{ proposals: typeof WalletStoresV1.proposals }>,
now: Timestamp, now: Timestamp,
resp: PendingOperationsResponse, resp: PendingOperationsResponse,
onlyDue = false, onlyDue = false,
): Promise<void> { ): Promise<void> {
await tx.iter(Stores.proposals).forEach((proposal) => { await tx.proposals.iter().forEach((proposal) => {
if (proposal.proposalStatus == ProposalStatus.PROPOSED) { if (proposal.proposalStatus == ProposalStatus.PROPOSED) {
if (onlyDue) { if (onlyDue) {
return; return;
@ -327,12 +331,12 @@ async function gatherProposalPending(
} }
async function gatherTipPending( async function gatherTipPending(
tx: TransactionHandle<typeof Stores.tips>, tx: GetReadOnlyAccess<{ tips: typeof WalletStoresV1.tips }>,
now: Timestamp, now: Timestamp,
resp: PendingOperationsResponse, resp: PendingOperationsResponse,
onlyDue = false, onlyDue = false,
): Promise<void> { ): Promise<void> {
await tx.iter(Stores.tips).forEach((tip) => { await tx.tips.iter().forEach((tip) => {
if (tip.pickedUpTimestamp) { if (tip.pickedUpTimestamp) {
return; return;
} }
@ -357,12 +361,12 @@ async function gatherTipPending(
} }
async function gatherPurchasePending( async function gatherPurchasePending(
tx: TransactionHandle<typeof Stores.purchases>, tx: GetReadOnlyAccess<{ purchases: typeof WalletStoresV1.purchases }>,
now: Timestamp, now: Timestamp,
resp: PendingOperationsResponse, resp: PendingOperationsResponse,
onlyDue = false, onlyDue = false,
): Promise<void> { ): Promise<void> {
await tx.iter(Stores.purchases).forEach((pr) => { await tx.purchases.iter().forEach((pr) => {
if (pr.paymentSubmitPending && pr.abortStatus === AbortStatus.None) { if (pr.paymentSubmitPending && pr.abortStatus === AbortStatus.None) {
resp.nextRetryDelay = updateRetryDelay( resp.nextRetryDelay = updateRetryDelay(
resp.nextRetryDelay, resp.nextRetryDelay,
@ -400,12 +404,12 @@ async function gatherPurchasePending(
} }
async function gatherRecoupPending( async function gatherRecoupPending(
tx: TransactionHandle<typeof Stores.recoupGroups>, tx: GetReadOnlyAccess<{ recoupGroups: typeof WalletStoresV1.recoupGroups }>,
now: Timestamp, now: Timestamp,
resp: PendingOperationsResponse, resp: PendingOperationsResponse,
onlyDue = false, onlyDue = false,
): Promise<void> { ): Promise<void> {
await tx.iter(Stores.recoupGroups).forEach((rg) => { await tx.recoupGroups.iter().forEach((rg) => {
if (rg.timestampFinished) { if (rg.timestampFinished) {
return; return;
} }
@ -428,12 +432,12 @@ async function gatherRecoupPending(
} }
async function gatherDepositPending( async function gatherDepositPending(
tx: TransactionHandle<typeof Stores.depositGroups>, tx: GetReadOnlyAccess<{ depositGroups: typeof WalletStoresV1.depositGroups }>,
now: Timestamp, now: Timestamp,
resp: PendingOperationsResponse, resp: PendingOperationsResponse,
onlyDue = false, onlyDue = false,
): Promise<void> { ): Promise<void> {
await tx.iter(Stores.depositGroups).forEach((dg) => { await tx.depositGroups.iter().forEach((dg) => {
if (dg.timestampFinished) { if (dg.timestampFinished) {
return; return;
} }
@ -460,20 +464,20 @@ export async function getPendingOperations(
{ onlyDue = false } = {}, { onlyDue = false } = {},
): Promise<PendingOperationsResponse> { ): Promise<PendingOperationsResponse> {
const now = getTimestampNow(); const now = getTimestampNow();
return await ws.db.runWithReadTransaction( return await ws.db.mktx((x) => ({
[ exchanges: x.exchanges,
Stores.exchanges, exchangeDetails: x.exchangeDetails,
Stores.reserves, reserves: x.reserves,
Stores.refreshGroups, refreshGroups: x.refreshGroups,
Stores.coins, coins: x.coins,
Stores.withdrawalGroups, withdrawalGroups: x.withdrawalGroups,
Stores.proposals, proposals: x.proposals,
Stores.tips, tips: x.tips,
Stores.purchases, purchases: x.purchases,
Stores.recoupGroups, planchets: x.planchets,
Stores.planchets, depositGroups: x.depositGroups,
Stores.depositGroups, recoupGroups: x.recoupGroups,
], })).runReadWrite(
async (tx) => { async (tx) => {
const walletBalance = await getBalancesInsideTransaction(ws, tx); const walletBalance = await getBalancesInsideTransaction(ws, tx);
const resp: PendingOperationsResponse = { const resp: PendingOperationsResponse = {

View File

@ -40,20 +40,19 @@ import {
RecoupGroupRecord, RecoupGroupRecord,
RefreshCoinSource, RefreshCoinSource,
ReserveRecordStatus, ReserveRecordStatus,
Stores,
WithdrawCoinSource, WithdrawCoinSource,
WalletStoresV1,
} from "../db.js"; } from "../db.js";
import { readSuccessResponseJsonOrThrow } from "../util/http"; import { readSuccessResponseJsonOrThrow } from "../util/http";
import { Logger } from "@gnu-taler/taler-util"; import { Logger } from "@gnu-taler/taler-util";
import { TransactionHandle } from "../util/query";
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries"; import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries";
import { URL } from "../util/url"; import { URL } from "../util/url";
import { guardOperationException } from "./errors"; import { guardOperationException } from "./errors";
import { getExchangeDetails } from "./exchanges.js";
import { createRefreshGroup, processRefreshGroup } from "./refresh"; import { createRefreshGroup, processRefreshGroup } from "./refresh";
import { getReserveRequestTimeout, processReserve } from "./reserves"; import { getReserveRequestTimeout, processReserve } from "./reserves";
import { InternalWalletState } from "./state"; import { InternalWalletState } from "./state";
import { GetReadWriteAccess } from "../util/query.js";
const logger = new Logger("operations/recoup.ts"); const logger = new Logger("operations/recoup.ts");
@ -62,8 +61,12 @@ async function incrementRecoupRetry(
recoupGroupId: string, recoupGroupId: string,
err: TalerErrorDetails | undefined, err: TalerErrorDetails | undefined,
): Promise<void> { ): Promise<void> {
await ws.db.runWithWriteTransaction([Stores.recoupGroups], async (tx) => { await ws.db
const r = await tx.get(Stores.recoupGroups, recoupGroupId); .mktx((x) => ({
recoupGroups: x.recoupGroups,
}))
.runReadWrite(async (tx) => {
const r = await tx.recoupGroups.get(recoupGroupId);
if (!r) { if (!r) {
return; return;
} }
@ -73,7 +76,7 @@ async function incrementRecoupRetry(
r.retryInfo.retryCounter++; r.retryInfo.retryCounter++;
updateRetryInfoTimeout(r.retryInfo); updateRetryInfoTimeout(r.retryInfo);
r.lastError = err; r.lastError = err;
await tx.put(Stores.recoupGroups, r); await tx.recoupGroups.put(r);
}); });
if (err) { if (err) {
ws.notify({ type: NotificationType.RecoupOperationError, error: err }); ws.notify({ type: NotificationType.RecoupOperationError, error: err });
@ -82,7 +85,12 @@ async function incrementRecoupRetry(
async function putGroupAsFinished( async function putGroupAsFinished(
ws: InternalWalletState, ws: InternalWalletState,
tx: TransactionHandle<typeof Stores.recoupGroups>, tx: GetReadWriteAccess<{
recoupGroups: typeof WalletStoresV1.recoupGroups;
denominations: typeof WalletStoresV1.denominations;
refreshGroups: typeof WalletStoresV1.refreshGroups;
coins: typeof WalletStoresV1.coins;
}>,
recoupGroup: RecoupGroupRecord, recoupGroup: RecoupGroupRecord,
coinIdx: number, coinIdx: number,
): Promise<void> { ): Promise<void> {
@ -116,7 +124,7 @@ async function putGroupAsFinished(
}); });
} }
} }
await tx.put(Stores.recoupGroups, recoupGroup); await tx.recoupGroups.put(recoupGroup);
} }
async function recoupTipCoin( async function recoupTipCoin(
@ -128,8 +136,15 @@ async function recoupTipCoin(
// We can't really recoup a coin we got via tipping. // We can't really recoup a coin we got via tipping.
// Thus we just put the coin to sleep. // Thus we just put the coin to sleep.
// FIXME: somehow report this to the user // FIXME: somehow report this to the user
await ws.db.runWithWriteTransaction([Stores.recoupGroups], async (tx) => { await ws.db
const recoupGroup = await tx.get(Stores.recoupGroups, recoupGroupId); .mktx((x) => ({
recoupGroups: x.recoupGroups,
denominations: WalletStoresV1.denominations,
refreshGroups: WalletStoresV1.refreshGroups,
coins: WalletStoresV1.coins,
}))
.runReadWrite(async (tx) => {
const recoupGroup = await tx.recoupGroups.get(recoupGroupId);
if (!recoupGroup) { if (!recoupGroup) {
return; return;
} }
@ -148,7 +163,13 @@ async function recoupWithdrawCoin(
cs: WithdrawCoinSource, cs: WithdrawCoinSource,
): Promise<void> { ): Promise<void> {
const reservePub = cs.reservePub; const reservePub = cs.reservePub;
const reserve = await ws.db.get(Stores.reserves, reservePub); const reserve = await ws.db
.mktx((x) => ({
reserves: x.reserves,
}))
.runReadOnly(async (tx) => {
return tx.reserves.get(reservePub);
});
if (!reserve) { if (!reserve) {
// FIXME: We should at least emit some pending operation / warning for this? // FIXME: We should at least emit some pending operation / warning for this?
return; return;
@ -172,35 +193,29 @@ async function recoupWithdrawCoin(
throw Error(`Coin's reserve doesn't match reserve on recoup`); throw Error(`Coin's reserve doesn't match reserve on recoup`);
} }
const exchangeDetails = await ws.db.runWithReadTransaction(
[Stores.exchanges, Stores.exchangeDetails],
async (tx) => {
return getExchangeDetails(tx, reserve.exchangeBaseUrl);
},
);
if (!exchangeDetails) {
// FIXME: report inconsistency?
return;
}
// FIXME: verify that our expectations about the amount match // FIXME: verify that our expectations about the amount match
await ws.db.runWithWriteTransaction( await ws.db
[Stores.coins, Stores.denominations, Stores.reserves, Stores.recoupGroups], .mktx((x) => ({
async (tx) => { coins: x.coins,
const recoupGroup = await tx.get(Stores.recoupGroups, recoupGroupId); denominations: x.denominations,
reserves: x.reserves,
recoupGroups: x.recoupGroups,
refreshGroups: x.refreshGroups,
}))
.runReadWrite(async (tx) => {
const recoupGroup = await tx.recoupGroups.get(recoupGroupId);
if (!recoupGroup) { if (!recoupGroup) {
return; return;
} }
if (recoupGroup.recoupFinishedPerCoin[coinIdx]) { if (recoupGroup.recoupFinishedPerCoin[coinIdx]) {
return; return;
} }
const updatedCoin = await tx.get(Stores.coins, coin.coinPub); const updatedCoin = await tx.coins.get(coin.coinPub);
if (!updatedCoin) { if (!updatedCoin) {
return; return;
} }
const updatedReserve = await tx.get(Stores.reserves, reserve.reservePub); const updatedReserve = await tx.reserves.get(reserve.reservePub);
if (!updatedReserve) { if (!updatedReserve) {
return; return;
} }
@ -214,11 +229,10 @@ async function recoupWithdrawCoin(
updatedReserve.requestedQuery = true; updatedReserve.requestedQuery = true;
updatedReserve.retryInfo = initRetryInfo(); updatedReserve.retryInfo = initRetryInfo();
} }
await tx.put(Stores.coins, updatedCoin); await tx.coins.put(updatedCoin);
await tx.put(Stores.reserves, updatedReserve); await tx.reserves.put(updatedReserve);
await putGroupAsFinished(ws, tx, recoupGroup, coinIdx); await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
}, });
);
ws.notify({ ws.notify({
type: NotificationType.RecoupFinished, type: NotificationType.RecoupFinished,
@ -250,38 +264,24 @@ async function recoupRefreshCoin(
throw Error(`Coin's oldCoinPub doesn't match reserve on recoup`); throw Error(`Coin's oldCoinPub doesn't match reserve on recoup`);
} }
const exchangeDetails = await ws.db.runWithReadTransaction( await ws.db
[Stores.exchanges, Stores.exchangeDetails], .mktx((x) => ({
async (tx) => { coins: x.coins,
// FIXME: Get the exchange details based on the denominations: x.denominations,
// exchange master public key instead of via just the URL. reserves: x.reserves,
return getExchangeDetails(tx, coin.exchangeBaseUrl); recoupGroups: x.recoupGroups,
}, refreshGroups: x.refreshGroups,
); }))
if (!exchangeDetails) { .runReadWrite(async (tx) => {
// FIXME: report inconsistency? const recoupGroup = await tx.recoupGroups.get(recoupGroupId);
logger.warn("exchange details for recoup not found");
return;
}
await ws.db.runWithWriteTransaction(
[
Stores.coins,
Stores.denominations,
Stores.reserves,
Stores.recoupGroups,
Stores.refreshGroups,
],
async (tx) => {
const recoupGroup = await tx.get(Stores.recoupGroups, recoupGroupId);
if (!recoupGroup) { if (!recoupGroup) {
return; return;
} }
if (recoupGroup.recoupFinishedPerCoin[coinIdx]) { if (recoupGroup.recoupFinishedPerCoin[coinIdx]) {
return; return;
} }
const oldCoin = await tx.get(Stores.coins, cs.oldCoinPub); const oldCoin = await tx.coins.get(cs.oldCoinPub);
const revokedCoin = await tx.get(Stores.coins, coin.coinPub); const revokedCoin = await tx.coins.get(coin.coinPub);
if (!revokedCoin) { if (!revokedCoin) {
logger.warn("revoked coin for recoup not found"); logger.warn("revoked coin for recoup not found");
return; return;
@ -300,22 +300,26 @@ async function recoupRefreshCoin(
Amounts.stringify(oldCoin.currentAmount), Amounts.stringify(oldCoin.currentAmount),
); );
recoupGroup.scheduleRefreshCoins.push(oldCoin.coinPub); recoupGroup.scheduleRefreshCoins.push(oldCoin.coinPub);
await tx.put(Stores.coins, revokedCoin); await tx.coins.put(revokedCoin);
await tx.put(Stores.coins, oldCoin); await tx.coins.put(oldCoin);
await putGroupAsFinished(ws, tx, recoupGroup, coinIdx); await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
}, });
);
} }
async function resetRecoupGroupRetry( async function resetRecoupGroupRetry(
ws: InternalWalletState, ws: InternalWalletState,
recoupGroupId: string, recoupGroupId: string,
): Promise<void> { ): Promise<void> {
await ws.db.mutate(Stores.recoupGroups, recoupGroupId, (x) => { await ws.db
if (x.retryInfo.active) { .mktx((x) => ({
recoupGroups: x.recoupGroups,
}))
.runReadWrite(async (tx) => {
const x = await tx.recoupGroups.get(recoupGroupId);
if (x && x.retryInfo.active) {
x.retryInfo = initRetryInfo(); x.retryInfo = initRetryInfo();
await tx.recoupGroups.put(x);
} }
return x;
}); });
} }
@ -342,7 +346,13 @@ async function processRecoupGroupImpl(
if (forceNow) { if (forceNow) {
await resetRecoupGroupRetry(ws, recoupGroupId); await resetRecoupGroupRetry(ws, recoupGroupId);
} }
const recoupGroup = await ws.db.get(Stores.recoupGroups, recoupGroupId); const recoupGroup = await ws.db
.mktx((x) => ({
recoupGroups: x.recoupGroups,
}))
.runReadOnly(async (tx) => {
return tx.recoupGroups.get(recoupGroupId);
});
if (!recoupGroup) { if (!recoupGroup) {
return; return;
} }
@ -358,9 +368,15 @@ async function processRecoupGroupImpl(
const reserveSet = new Set<string>(); const reserveSet = new Set<string>();
for (let i = 0; i < recoupGroup.coinPubs.length; i++) { for (let i = 0; i < recoupGroup.coinPubs.length; i++) {
const coinPub = recoupGroup.coinPubs[i]; const coinPub = recoupGroup.coinPubs[i];
const coin = await ws.db.get(Stores.coins, coinPub); const coin = await ws.db
.mktx((x) => ({
coins: x.coins,
}))
.runReadOnly(async (tx) => {
return tx.coins.get(coinPub);
});
if (!coin) { if (!coin) {
throw Error(`Coin ${coinPub} not found, can't request payback`); throw Error(`Coin ${coinPub} not found, can't request recoup`);
} }
if (coin.coinSource.type === CoinSourceType.Withdraw) { if (coin.coinSource.type === CoinSourceType.Withdraw) {
reserveSet.add(coin.coinSource.reservePub); reserveSet.add(coin.coinSource.reservePub);
@ -376,7 +392,12 @@ async function processRecoupGroupImpl(
export async function createRecoupGroup( export async function createRecoupGroup(
ws: InternalWalletState, ws: InternalWalletState,
tx: TransactionHandle<typeof Stores.recoupGroups | typeof Stores.coins>, tx: GetReadWriteAccess<{
recoupGroups: typeof WalletStoresV1.recoupGroups;
denominations: typeof WalletStoresV1.denominations;
refreshGroups: typeof WalletStoresV1.refreshGroups;
coins: typeof WalletStoresV1.coins;
}>,
coinPubs: string[], coinPubs: string[],
): Promise<string> { ): Promise<string> {
const recoupGroupId = encodeCrock(getRandomBytes(32)); const recoupGroupId = encodeCrock(getRandomBytes(32));
@ -396,7 +417,7 @@ export async function createRecoupGroup(
for (let coinIdx = 0; coinIdx < coinPubs.length; coinIdx++) { for (let coinIdx = 0; coinIdx < coinPubs.length; coinIdx++) {
const coinPub = coinPubs[coinIdx]; const coinPub = coinPubs[coinIdx];
const coin = await tx.get(Stores.coins, coinPub); const coin = await tx.coins.get(coinPub);
if (!coin) { if (!coin) {
await putGroupAsFinished(ws, tx, recoupGroup, coinIdx); await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
continue; continue;
@ -407,10 +428,10 @@ export async function createRecoupGroup(
} }
recoupGroup.oldAmountPerCoin[coinIdx] = coin.currentAmount; recoupGroup.oldAmountPerCoin[coinIdx] = coin.currentAmount;
coin.currentAmount = Amounts.getZero(coin.currentAmount.currency); coin.currentAmount = Amounts.getZero(coin.currentAmount.currency);
await tx.put(Stores.coins, coin); await tx.coins.put(coin);
} }
await tx.put(Stores.recoupGroups, recoupGroup); await tx.recoupGroups.put(recoupGroup);
return recoupGroupId; return recoupGroupId;
} }
@ -420,7 +441,13 @@ async function processRecoup(
recoupGroupId: string, recoupGroupId: string,
coinIdx: number, coinIdx: number,
): Promise<void> { ): Promise<void> {
const recoupGroup = await ws.db.get(Stores.recoupGroups, recoupGroupId); const coin = await ws.db
.mktx((x) => ({
recoupGroups: x.recoupGroups,
coins: x.coins,
}))
.runReadOnly(async (tx) => {
const recoupGroup = await tx.recoupGroups.get(recoupGroupId);
if (!recoupGroup) { if (!recoupGroup) {
return; return;
} }
@ -433,10 +460,16 @@ async function processRecoup(
const coinPub = recoupGroup.coinPubs[coinIdx]; const coinPub = recoupGroup.coinPubs[coinIdx];
const coin = await ws.db.get(Stores.coins, coinPub); const coin = await tx.coins.get(coinPub);
if (!coin) { if (!coin) {
throw Error(`Coin ${coinPub} not found, can't request payback`); throw Error(`Coin ${coinPub} not found, can't request payback`);
} }
return coin;
});
if (!coin) {
return;
}
const cs = coin.coinSource; const cs = coin.coinSource;

View File

@ -22,7 +22,7 @@ import {
DenominationRecord, DenominationRecord,
RefreshGroupRecord, RefreshGroupRecord,
RefreshPlanchet, RefreshPlanchet,
Stores, WalletStoresV1,
} from "../db.js"; } from "../db.js";
import { import {
codecForExchangeMeltResponse, codecForExchangeMeltResponse,
@ -38,7 +38,6 @@ import { amountToPretty } from "@gnu-taler/taler-util";
import { readSuccessResponseJsonOrThrow } from "../util/http"; import { readSuccessResponseJsonOrThrow } from "../util/http";
import { checkDbInvariant } from "../util/invariants"; import { checkDbInvariant } from "../util/invariants";
import { Logger } from "@gnu-taler/taler-util"; import { Logger } from "@gnu-taler/taler-util";
import { TransactionHandle } from "../util/query";
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries"; import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries";
import { import {
Duration, Duration,
@ -57,6 +56,8 @@ import { updateExchangeFromUrl } from "./exchanges";
import { EXCHANGE_COINS_LOCK, InternalWalletState } from "./state"; import { EXCHANGE_COINS_LOCK, InternalWalletState } from "./state";
import { isWithdrawableDenom, selectWithdrawalDenominations } from "./withdraw"; import { isWithdrawableDenom, selectWithdrawalDenominations } from "./withdraw";
import { RefreshNewDenomInfo } from "../crypto/cryptoTypes.js"; import { RefreshNewDenomInfo } from "../crypto/cryptoTypes.js";
import { GetReadWriteAccess } from "../util/query.js";
import { Wallet } from "../wallet.js";
const logger = new Logger("refresh.ts"); const logger = new Logger("refresh.ts");
@ -95,7 +96,7 @@ export function getTotalRefreshCost(
} }
/** /**
* Create a refresh session inside a refresh group. * Create a refresh session for one particular coin inside a refresh group.
*/ */
async function refreshCreateSession( async function refreshCreateSession(
ws: InternalWalletState, ws: InternalWalletState,
@ -105,29 +106,50 @@ async function refreshCreateSession(
logger.trace( logger.trace(
`creating refresh session for coin ${coinIndex} in refresh group ${refreshGroupId}`, `creating refresh session for coin ${coinIndex} in refresh group ${refreshGroupId}`,
); );
const refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId);
const d = await ws.db
.mktx((x) => ({
refreshGroups: x.refreshGroups,
coins: x.coins,
}))
.runReadWrite(async (tx) => {
const refreshGroup = await tx.refreshGroups.get(refreshGroupId);
if (!refreshGroup) { if (!refreshGroup) {
return; return;
} }
if (refreshGroup.finishedPerCoin[coinIndex]) { if (refreshGroup.finishedPerCoin[coinIndex]) {
return; return;
} }
const existingRefreshSession = refreshGroup.refreshSessionPerCoin[coinIndex]; const existingRefreshSession =
refreshGroup.refreshSessionPerCoin[coinIndex];
if (existingRefreshSession) { if (existingRefreshSession) {
return; return;
} }
const oldCoinPub = refreshGroup.oldCoinPubs[coinIndex]; const oldCoinPub = refreshGroup.oldCoinPubs[coinIndex];
const coin = await ws.db.get(Stores.coins, oldCoinPub); const coin = await tx.coins.get(oldCoinPub);
if (!coin) { if (!coin) {
throw Error("Can't refresh, coin not found"); throw Error("Can't refresh, coin not found");
} }
return { refreshGroup, coin };
});
if (!d) {
return;
}
const { refreshGroup, coin } = d;
const { exchange } = await updateExchangeFromUrl(ws, coin.exchangeBaseUrl); const { exchange } = await updateExchangeFromUrl(ws, coin.exchangeBaseUrl);
if (!exchange) { if (!exchange) {
throw Error("db inconsistent: exchange of coin not found"); throw Error("db inconsistent: exchange of coin not found");
} }
const oldDenom = await ws.db.get(Stores.denominations, [ const { availableAmount, availableDenoms } = await ws.db
.mktx((x) => ({
denominations: x.denominations,
}))
.runReadOnly(async (tx) => {
const oldDenom = await tx.denominations.get([
exchange.baseUrl, exchange.baseUrl,
coin.denomPubHash, coin.denomPubHash,
]); ]);
@ -136,14 +158,16 @@ async function refreshCreateSession(
throw Error("db inconsistent: denomination for coin not found"); throw Error("db inconsistent: denomination for coin not found");
} }
const availableDenoms: DenominationRecord[] = await ws.db const availableDenoms: DenominationRecord[] = await tx.denominations.indexes.byExchangeBaseUrl
.iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl) .iter(exchange.baseUrl)
.toArray(); .toArray();
const availableAmount = Amounts.sub( const availableAmount = Amounts.sub(
refreshGroup.inputPerCoin[coinIndex], refreshGroup.inputPerCoin[coinIndex],
oldDenom.feeRefresh, oldDenom.feeRefresh,
).amount; ).amount;
return { availableAmount, availableDenoms };
});
const newCoinDenoms = selectWithdrawalDenominations( const newCoinDenoms = selectWithdrawalDenominations(
availableAmount, availableAmount,
@ -156,10 +180,13 @@ async function refreshCreateSession(
availableAmount, availableAmount,
)} too small`, )} too small`,
); );
await ws.db.runWithWriteTransaction( await ws.db
[Stores.coins, Stores.refreshGroups], .mktx((x) => ({
async (tx) => { coins: x.coins,
const rg = await tx.get(Stores.refreshGroups, refreshGroupId); refreshGroups: x.refreshGroups,
}))
.runReadWrite(async (tx) => {
const rg = await tx.refreshGroups.get(refreshGroupId);
if (!rg) { if (!rg) {
return; return;
} }
@ -175,9 +202,8 @@ async function refreshCreateSession(
rg.timestampFinished = getTimestampNow(); rg.timestampFinished = getTimestampNow();
rg.retryInfo = initRetryInfo(false); rg.retryInfo = initRetryInfo(false);
} }
await tx.put(Stores.refreshGroups, rg); await tx.refreshGroups.put(rg);
}, });
);
ws.notify({ type: NotificationType.RefreshUnwarranted }); ws.notify({ type: NotificationType.RefreshUnwarranted });
return; return;
} }
@ -185,10 +211,13 @@ async function refreshCreateSession(
const sessionSecretSeed = encodeCrock(getRandomBytes(64)); const sessionSecretSeed = encodeCrock(getRandomBytes(64));
// Store refresh session for this coin in the database. // Store refresh session for this coin in the database.
await ws.db.runWithWriteTransaction( await ws.db
[Stores.refreshGroups, Stores.coins], .mktx((x) => ({
async (tx) => { refreshGroups: x.refreshGroups,
const rg = await tx.get(Stores.refreshGroups, refreshGroupId); coins: x.coins,
}))
.runReadWrite(async (tx) => {
const rg = await tx.refreshGroups.get(refreshGroupId);
if (!rg) { if (!rg) {
return; return;
} }
@ -204,9 +233,8 @@ async function refreshCreateSession(
})), })),
amountRefreshOutput: newCoinDenoms.totalCoinValue, amountRefreshOutput: newCoinDenoms.totalCoinValue,
}; };
await tx.put(Stores.refreshGroups, rg); await tx.refreshGroups.put(rg);
}, });
);
logger.info( logger.info(
`created refresh session for coin #${coinIndex} in ${refreshGroupId}`, `created refresh session for coin #${coinIndex} in ${refreshGroupId}`,
); );
@ -222,7 +250,14 @@ async function refreshMelt(
refreshGroupId: string, refreshGroupId: string,
coinIndex: number, coinIndex: number,
): Promise<void> { ): Promise<void> {
const refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId); const d = await ws.db
.mktx((x) => ({
refreshGroups: x.refreshGroups,
coins: x.coins,
denominations: x.denominations,
}))
.runReadWrite(async (tx) => {
const refreshGroup = await tx.refreshGroups.get(refreshGroupId);
if (!refreshGroup) { if (!refreshGroup) {
return; return;
} }
@ -234,21 +269,21 @@ async function refreshMelt(
return; return;
} }
const oldCoin = await ws.db.get( const oldCoin = await tx.coins.get(refreshGroup.oldCoinPubs[coinIndex]);
Stores.coins,
refreshGroup.oldCoinPubs[coinIndex],
);
checkDbInvariant(!!oldCoin, "melt coin doesn't exist"); checkDbInvariant(!!oldCoin, "melt coin doesn't exist");
const oldDenom = await ws.db.get(Stores.denominations, [ const oldDenom = await tx.denominations.get([
oldCoin.exchangeBaseUrl, oldCoin.exchangeBaseUrl,
oldCoin.denomPubHash, oldCoin.denomPubHash,
]); ]);
checkDbInvariant(!!oldDenom, "denomination for melted coin doesn't exist"); checkDbInvariant(
!!oldDenom,
"denomination for melted coin doesn't exist",
);
const newCoinDenoms: RefreshNewDenomInfo[] = []; const newCoinDenoms: RefreshNewDenomInfo[] = [];
for (const dh of refreshSession.newDenoms) { for (const dh of refreshSession.newDenoms) {
const newDenom = await ws.db.get(Stores.denominations, [ const newDenom = await tx.denominations.get([
oldCoin.exchangeBaseUrl, oldCoin.exchangeBaseUrl,
dh.denomPubHash, dh.denomPubHash,
]); ]);
@ -263,6 +298,14 @@ async function refreshMelt(
value: newDenom.value, value: newDenom.value,
}); });
} }
return { newCoinDenoms, oldCoin, oldDenom, refreshGroup, refreshSession };
});
if (!d) {
return;
}
const { newCoinDenoms, oldCoin, oldDenom, refreshGroup, refreshSession } = d;
const derived = await ws.cryptoApi.deriveRefreshSession({ const derived = await ws.cryptoApi.deriveRefreshSession({
kappa: 3, kappa: 3,
@ -303,11 +346,19 @@ async function refreshMelt(
refreshSession.norevealIndex = norevealIndex; refreshSession.norevealIndex = norevealIndex;
await ws.db.mutate(Stores.refreshGroups, refreshGroupId, (rg) => { await ws.db
const rs = rg.refreshSessionPerCoin[coinIndex]; .mktx((x) => ({
refreshGroups: x.refreshGroups,
}))
.runReadWrite(async (tx) => {
const rg = await tx.refreshGroups.get(refreshGroupId);
if (!rg) {
return;
}
if (rg.timestampFinished) { if (rg.timestampFinished) {
return; return;
} }
const rs = rg.refreshSessionPerCoin[coinIndex];
if (!rs) { if (!rs) {
return; return;
} }
@ -315,7 +366,7 @@ async function refreshMelt(
return; return;
} }
rs.norevealIndex = norevealIndex; rs.norevealIndex = norevealIndex;
return rg; await tx.refreshGroups.put(rg);
}); });
ws.notify({ ws.notify({
@ -328,7 +379,14 @@ async function refreshReveal(
refreshGroupId: string, refreshGroupId: string,
coinIndex: number, coinIndex: number,
): Promise<void> { ): Promise<void> {
const refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId); const d = await ws.db
.mktx((x) => ({
refreshGroups: x.refreshGroups,
coins: x.coins,
denominations: x.denominations,
}))
.runReadOnly(async (tx) => {
const refreshGroup = await tx.refreshGroups.get(refreshGroupId);
if (!refreshGroup) { if (!refreshGroup) {
return; return;
} }
@ -341,21 +399,21 @@ async function refreshReveal(
throw Error("can't reveal without melting first"); throw Error("can't reveal without melting first");
} }
const oldCoin = await ws.db.get( const oldCoin = await tx.coins.get(refreshGroup.oldCoinPubs[coinIndex]);
Stores.coins,
refreshGroup.oldCoinPubs[coinIndex],
);
checkDbInvariant(!!oldCoin, "melt coin doesn't exist"); checkDbInvariant(!!oldCoin, "melt coin doesn't exist");
const oldDenom = await ws.db.get(Stores.denominations, [ const oldDenom = await tx.denominations.get([
oldCoin.exchangeBaseUrl, oldCoin.exchangeBaseUrl,
oldCoin.denomPubHash, oldCoin.denomPubHash,
]); ]);
checkDbInvariant(!!oldDenom, "denomination for melted coin doesn't exist"); checkDbInvariant(
!!oldDenom,
"denomination for melted coin doesn't exist",
);
const newCoinDenoms: RefreshNewDenomInfo[] = []; const newCoinDenoms: RefreshNewDenomInfo[] = [];
for (const dh of refreshSession.newDenoms) { for (const dh of refreshSession.newDenoms) {
const newDenom = await ws.db.get(Stores.denominations, [ const newDenom = await tx.denominations.get([
oldCoin.exchangeBaseUrl, oldCoin.exchangeBaseUrl,
dh.denomPubHash, dh.denomPubHash,
]); ]);
@ -370,6 +428,28 @@ async function refreshReveal(
value: newDenom.value, value: newDenom.value,
}); });
} }
return {
oldCoin,
oldDenom,
newCoinDenoms,
refreshSession,
refreshGroup,
norevealIndex,
};
});
if (!d) {
return;
}
const {
oldCoin,
oldDenom,
newCoinDenoms,
refreshSession,
refreshGroup,
norevealIndex,
} = d;
const derived = await ws.cryptoApi.deriveRefreshSession({ const derived = await ws.cryptoApi.deriveRefreshSession({
kappa: 3, kappa: 3,
@ -389,14 +469,6 @@ async function refreshReveal(
throw Error("refresh index error"); throw Error("refresh index error");
} }
const meltCoinRecord = await ws.db.get(
Stores.coins,
refreshGroup.oldCoinPubs[coinIndex],
);
if (!meltCoinRecord) {
throw Error("inconsistent database");
}
const evs = planchets.map((x: RefreshPlanchet) => x.coinEv); const evs = planchets.map((x: RefreshPlanchet) => x.coinEv);
const newDenomsFlat: string[] = []; const newDenomsFlat: string[] = [];
const linkSigs: string[] = []; const linkSigs: string[] = [];
@ -406,9 +478,9 @@ async function refreshReveal(
for (let j = 0; j < dsel.count; j++) { for (let j = 0; j < dsel.count; j++) {
const newCoinIndex = linkSigs.length; const newCoinIndex = linkSigs.length;
const linkSig = await ws.cryptoApi.signCoinLink( const linkSig = await ws.cryptoApi.signCoinLink(
meltCoinRecord.coinPriv, oldCoin.coinPriv,
dsel.denomPubHash, dsel.denomPubHash,
meltCoinRecord.coinPub, oldCoin.coinPub,
derived.transferPubs[norevealIndex], derived.transferPubs[norevealIndex],
planchets[newCoinIndex].coinEv, planchets[newCoinIndex].coinEv,
); );
@ -447,10 +519,17 @@ async function refreshReveal(
for (let i = 0; i < refreshSession.newDenoms.length; i++) { for (let i = 0; i < refreshSession.newDenoms.length; i++) {
for (let j = 0; j < refreshSession.newDenoms[i].count; j++) { for (let j = 0; j < refreshSession.newDenoms[i].count; j++) {
const newCoinIndex = coins.length; const newCoinIndex = coins.length;
const denom = await ws.db.get(Stores.denominations, [ // FIXME: Look up in earlier transaction!
const denom = await ws.db
.mktx((x) => ({
denominations: x.denominations,
}))
.runReadOnly(async (tx) => {
return tx.denominations.get([
oldCoin.exchangeBaseUrl, oldCoin.exchangeBaseUrl,
refreshSession.newDenoms[i].denomPubHash, refreshSession.newDenoms[i].denomPubHash,
]); ]);
});
if (!denom) { if (!denom) {
console.error("denom not found"); console.error("denom not found");
continue; continue;
@ -483,10 +562,13 @@ async function refreshReveal(
} }
} }
await ws.db.runWithWriteTransaction( await ws.db
[Stores.coins, Stores.refreshGroups], .mktx((x) => ({
async (tx) => { coins: x.coins,
const rg = await tx.get(Stores.refreshGroups, refreshGroupId); refreshGroups: x.refreshGroups,
}))
.runReadWrite(async (tx) => {
const rg = await tx.refreshGroups.get(refreshGroupId);
if (!rg) { if (!rg) {
logger.warn("no refresh session found"); logger.warn("no refresh session found");
return; return;
@ -508,11 +590,10 @@ async function refreshReveal(
rg.retryInfo = initRetryInfo(false); rg.retryInfo = initRetryInfo(false);
} }
for (const coin of coins) { for (const coin of coins) {
await tx.put(Stores.coins, coin); await tx.coins.put(coin);
} }
await tx.put(Stores.refreshGroups, rg); await tx.refreshGroups.put(rg);
}, });
);
logger.trace("refresh finished (end of reveal)"); logger.trace("refresh finished (end of reveal)");
ws.notify({ ws.notify({
type: NotificationType.RefreshRevealed, type: NotificationType.RefreshRevealed,
@ -524,8 +605,12 @@ async function incrementRefreshRetry(
refreshGroupId: string, refreshGroupId: string,
err: TalerErrorDetails | undefined, err: TalerErrorDetails | undefined,
): Promise<void> { ): Promise<void> {
await ws.db.runWithWriteTransaction([Stores.refreshGroups], async (tx) => { await ws.db
const r = await tx.get(Stores.refreshGroups, refreshGroupId); .mktx((x) => ({
refreshGroups: x.refreshGroups,
}))
.runReadWrite(async (tx) => {
const r = await tx.refreshGroups.get(refreshGroupId);
if (!r) { if (!r) {
return; return;
} }
@ -535,7 +620,7 @@ async function incrementRefreshRetry(
r.retryInfo.retryCounter++; r.retryInfo.retryCounter++;
updateRetryInfoTimeout(r.retryInfo); updateRetryInfoTimeout(r.retryInfo);
r.lastError = err; r.lastError = err;
await tx.put(Stores.refreshGroups, r); await tx.refreshGroups.put(r);
}); });
if (err) { if (err) {
ws.notify({ type: NotificationType.RefreshOperationError, error: err }); ws.notify({ type: NotificationType.RefreshOperationError, error: err });
@ -562,13 +647,18 @@ export async function processRefreshGroup(
async function resetRefreshGroupRetry( async function resetRefreshGroupRetry(
ws: InternalWalletState, ws: InternalWalletState,
refreshSessionId: string, refreshGroupId: string,
): Promise<void> { ): Promise<void> {
await ws.db.mutate(Stores.refreshGroups, refreshSessionId, (x) => { await ws.db
if (x.retryInfo.active) { .mktx((x) => ({
refreshGroups: x.refreshGroups,
}))
.runReadWrite(async (tx) => {
const x = await tx.refreshGroups.get(refreshGroupId);
if (x && x.retryInfo.active) {
x.retryInfo = initRetryInfo(); x.retryInfo = initRetryInfo();
await tx.refreshGroups.put(x);
} }
return x;
}); });
} }
@ -580,13 +670,20 @@ async function processRefreshGroupImpl(
if (forceNow) { if (forceNow) {
await resetRefreshGroupRetry(ws, refreshGroupId); await resetRefreshGroupRetry(ws, refreshGroupId);
} }
const refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId); const refreshGroup = await ws.db
.mktx((x) => ({
refreshGroups: x.refreshGroups,
}))
.runReadOnly(async (tx) => {
return tx.refreshGroups.get(refreshGroupId);
});
if (!refreshGroup) { if (!refreshGroup) {
return; return;
} }
if (refreshGroup.timestampFinished) { if (refreshGroup.timestampFinished) {
return; return;
} }
// Process refresh sessions of the group in parallel.
const ps = refreshGroup.oldCoinPubs.map((x, i) => const ps = refreshGroup.oldCoinPubs.map((x, i) =>
processRefreshSession(ws, refreshGroupId, i), processRefreshSession(ws, refreshGroupId, i),
); );
@ -602,7 +699,11 @@ async function processRefreshSession(
logger.trace( logger.trace(
`processing refresh session for coin ${coinIndex} of group ${refreshGroupId}`, `processing refresh session for coin ${coinIndex} of group ${refreshGroupId}`,
); );
let refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId); let refreshGroup = await ws.db
.mktx((x) => ({ refreshGroups: x.refreshGroups }))
.runReadOnly(async (tx) => {
return tx.refreshGroups.get(refreshGroupId);
});
if (!refreshGroup) { if (!refreshGroup) {
return; return;
} }
@ -611,7 +712,11 @@ async function processRefreshSession(
} }
if (!refreshGroup.refreshSessionPerCoin[coinIndex]) { if (!refreshGroup.refreshSessionPerCoin[coinIndex]) {
await refreshCreateSession(ws, refreshGroupId, coinIndex); await refreshCreateSession(ws, refreshGroupId, coinIndex);
refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId); refreshGroup = await ws.db
.mktx((x) => ({ refreshGroups: x.refreshGroups }))
.runReadOnly(async (tx) => {
return tx.refreshGroups.get(refreshGroupId);
});
if (!refreshGroup) { if (!refreshGroup) {
return; return;
} }
@ -646,11 +751,11 @@ async function processRefreshSession(
*/ */
export async function createRefreshGroup( export async function createRefreshGroup(
ws: InternalWalletState, ws: InternalWalletState,
tx: TransactionHandle< tx: GetReadWriteAccess<{
| typeof Stores.denominations denominations: typeof WalletStoresV1.denominations;
| typeof Stores.coins coins: typeof WalletStoresV1.coins;
| typeof Stores.refreshGroups refreshGroups: typeof WalletStoresV1.refreshGroups;
>, }>,
oldCoinPubs: CoinPublicKey[], oldCoinPubs: CoinPublicKey[],
reason: RefreshReason, reason: RefreshReason,
): Promise<RefreshGroupId> { ): Promise<RefreshGroupId> {
@ -667,8 +772,8 @@ export async function createRefreshGroup(
if (denomsPerExchange[exchangeBaseUrl]) { if (denomsPerExchange[exchangeBaseUrl]) {
return denomsPerExchange[exchangeBaseUrl]; return denomsPerExchange[exchangeBaseUrl];
} }
const allDenoms = await tx const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
.iterIndexed(Stores.denominations.exchangeBaseUrlIndex, exchangeBaseUrl) .iter(exchangeBaseUrl)
.filter((x) => { .filter((x) => {
return isWithdrawableDenom(x); return isWithdrawableDenom(x);
}); });
@ -677,9 +782,9 @@ export async function createRefreshGroup(
}; };
for (const ocp of oldCoinPubs) { for (const ocp of oldCoinPubs) {
const coin = await tx.get(Stores.coins, ocp.coinPub); const coin = await tx.coins.get(ocp.coinPub);
checkDbInvariant(!!coin, "coin must be in database"); checkDbInvariant(!!coin, "coin must be in database");
const denom = await tx.get(Stores.denominations, [ const denom = await tx.denominations.get([
coin.exchangeBaseUrl, coin.exchangeBaseUrl,
coin.denomPubHash, coin.denomPubHash,
]); ]);
@ -691,7 +796,7 @@ export async function createRefreshGroup(
inputPerCoin.push(refreshAmount); inputPerCoin.push(refreshAmount);
coin.currentAmount = Amounts.getZero(refreshAmount.currency); coin.currentAmount = Amounts.getZero(refreshAmount.currency);
coin.status = CoinStatus.Dormant; coin.status = CoinStatus.Dormant;
await tx.put(Stores.coins, coin); await tx.coins.put(coin);
const denoms = await getDenoms(coin.exchangeBaseUrl); const denoms = await getDenoms(coin.exchangeBaseUrl);
const cost = getTotalRefreshCost(denoms, denom, refreshAmount); const cost = getTotalRefreshCost(denoms, denom, refreshAmount);
const output = Amounts.sub(refreshAmount, cost).amount; const output = Amounts.sub(refreshAmount, cost).amount;
@ -718,7 +823,7 @@ export async function createRefreshGroup(
refreshGroup.timestampFinished = getTimestampNow(); refreshGroup.timestampFinished = getTimestampNow();
} }
await tx.put(Stores.refreshGroups, refreshGroup); await tx.refreshGroups.put(refreshGroup);
logger.trace(`created refresh group ${refreshGroupId}`); logger.trace(`created refresh group ${refreshGroupId}`);
@ -760,20 +865,20 @@ export async function autoRefresh(
exchangeBaseUrl: string, exchangeBaseUrl: string,
): Promise<void> { ): Promise<void> {
await updateExchangeFromUrl(ws, exchangeBaseUrl, true); await updateExchangeFromUrl(ws, exchangeBaseUrl, true);
await ws.db.runWithWriteTransaction( await ws.db
[ .mktx((x) => ({
Stores.coins, coins: x.coins,
Stores.denominations, denominations: x.denominations,
Stores.refreshGroups, refreshGroups: x.refreshGroups,
Stores.exchanges, exchanges: x.exchanges,
], }))
async (tx) => { .runReadWrite(async (tx) => {
const exchange = await tx.get(Stores.exchanges, exchangeBaseUrl); const exchange = await tx.exchanges.get(exchangeBaseUrl);
if (!exchange) { if (!exchange) {
return; return;
} }
const coins = await tx const coins = await tx.coins.indexes.byBaseUrl
.iterIndexed(Stores.coins.exchangeBaseUrlIndex, exchangeBaseUrl) .iter(exchangeBaseUrl)
.toArray(); .toArray();
const refreshCoins: CoinPublicKey[] = []; const refreshCoins: CoinPublicKey[] = [];
for (const coin of coins) { for (const coin of coins) {
@ -783,7 +888,7 @@ export async function autoRefresh(
if (coin.suspended) { if (coin.suspended) {
continue; continue;
} }
const denom = await tx.get(Stores.denominations, [ const denom = await tx.denominations.get([
exchangeBaseUrl, exchangeBaseUrl,
coin.denomPubHash, coin.denomPubHash,
]); ]);
@ -800,8 +905,8 @@ export async function autoRefresh(
await createRefreshGroup(ws, tx, refreshCoins, RefreshReason.Scheduled); await createRefreshGroup(ws, tx, refreshCoins, RefreshReason.Scheduled);
} }
const denoms = await tx const denoms = await tx.denominations.indexes.byExchangeBaseUrl
.iterIndexed(Stores.denominations.exchangeBaseUrlIndex, exchangeBaseUrl) .iter(exchangeBaseUrl)
.toArray(); .toArray();
let minCheckThreshold = timestampAddDuration( let minCheckThreshold = timestampAddDuration(
getTimestampNow(), getTimestampNow(),
@ -817,7 +922,6 @@ export async function autoRefresh(
minCheckThreshold = timestampMin(minCheckThreshold, checkThreshold); minCheckThreshold = timestampMin(minCheckThreshold, checkThreshold);
} }
exchange.nextRefreshCheck = minCheckThreshold; exchange.nextRefreshCheck = minCheckThreshold;
await tx.put(Stores.exchanges, exchange); await tx.exchanges.put(exchange);
}, });
);
} }

View File

@ -48,13 +48,21 @@ import {
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { Logger } from "@gnu-taler/taler-util"; import { Logger } from "@gnu-taler/taler-util";
import { readSuccessResponseJsonOrThrow } from "../util/http"; import { readSuccessResponseJsonOrThrow } from "../util/http";
import { TransactionHandle } from "../util/query";
import { URL } from "../util/url"; import { URL } from "../util/url";
import { updateRetryInfoTimeout, initRetryInfo } from "../util/retries"; import { updateRetryInfoTimeout, initRetryInfo } from "../util/retries";
import { checkDbInvariant } from "../util/invariants"; import { checkDbInvariant } from "../util/invariants";
import { TalerErrorCode } from "@gnu-taler/taler-util"; import { TalerErrorCode } from "@gnu-taler/taler-util";
import { Stores, PurchaseRecord, CoinStatus, RefundState, AbortStatus, RefundReason } from "../db.js"; import {
PurchaseRecord,
CoinStatus,
RefundState,
AbortStatus,
RefundReason,
WalletStoresV1,
} from "../db.js";
import { getTotalRefreshCost, createRefreshGroup } from "./refresh.js"; import { getTotalRefreshCost, createRefreshGroup } from "./refresh.js";
import { GetReadWriteAccess } from "../util/query.js";
import { Wallet } from "../wallet.js";
const logger = new Logger("refund.ts"); const logger = new Logger("refund.ts");
@ -66,8 +74,12 @@ async function incrementPurchaseQueryRefundRetry(
proposalId: string, proposalId: string,
err: TalerErrorDetails | undefined, err: TalerErrorDetails | undefined,
): Promise<void> { ): Promise<void> {
await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => { await ws.db
const pr = await tx.get(Stores.purchases, proposalId); .mktx((x) => ({
purchases: x.purchases,
}))
.runReadWrite(async (tx) => {
const pr = await tx.purchases.get(proposalId);
if (!pr) { if (!pr) {
return; return;
} }
@ -77,7 +89,7 @@ async function incrementPurchaseQueryRefundRetry(
pr.refundStatusRetryInfo.retryCounter++; pr.refundStatusRetryInfo.retryCounter++;
updateRetryInfoTimeout(pr.refundStatusRetryInfo); updateRetryInfoTimeout(pr.refundStatusRetryInfo);
pr.lastRefundStatusError = err; pr.lastRefundStatusError = err;
await tx.put(Stores.purchases, pr); await tx.purchases.put(pr);
}); });
if (err) { if (err) {
ws.notify({ ws.notify({
@ -92,7 +104,10 @@ function getRefundKey(d: MerchantCoinRefundStatus): string {
} }
async function applySuccessfulRefund( async function applySuccessfulRefund(
tx: TransactionHandle<typeof Stores.coins | typeof Stores.denominations>, tx: GetReadWriteAccess<{
coins: typeof WalletStoresV1.coins;
denominations: typeof WalletStoresV1.denominations;
}>,
p: PurchaseRecord, p: PurchaseRecord,
refreshCoinsMap: Record<string, { coinPub: string }>, refreshCoinsMap: Record<string, { coinPub: string }>,
r: MerchantCoinRefundSuccessStatus, r: MerchantCoinRefundSuccessStatus,
@ -100,12 +115,12 @@ async function applySuccessfulRefund(
// FIXME: check signature before storing it as valid! // FIXME: check signature before storing it as valid!
const refundKey = getRefundKey(r); const refundKey = getRefundKey(r);
const coin = await tx.get(Stores.coins, r.coin_pub); const coin = await tx.coins.get(r.coin_pub);
if (!coin) { if (!coin) {
logger.warn("coin not found, can't apply refund"); logger.warn("coin not found, can't apply refund");
return; return;
} }
const denom = await tx.get(Stores.denominations, [ const denom = await tx.denominations.get([
coin.exchangeBaseUrl, coin.exchangeBaseUrl,
coin.denomPubHash, coin.denomPubHash,
]); ]);
@ -119,13 +134,10 @@ async function applySuccessfulRefund(
coin.currentAmount = Amounts.add(coin.currentAmount, refundAmount).amount; coin.currentAmount = Amounts.add(coin.currentAmount, refundAmount).amount;
coin.currentAmount = Amounts.sub(coin.currentAmount, refundFee).amount; coin.currentAmount = Amounts.sub(coin.currentAmount, refundFee).amount;
logger.trace(`coin amount after is ${Amounts.stringify(coin.currentAmount)}`); logger.trace(`coin amount after is ${Amounts.stringify(coin.currentAmount)}`);
await tx.put(Stores.coins, coin); await tx.coins.put(coin);
const allDenoms = await tx const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
.iterIndexed( .iter(coin.exchangeBaseUrl)
Stores.denominations.exchangeBaseUrlIndex,
coin.exchangeBaseUrl,
)
.toArray(); .toArray();
const amountLeft = Amounts.sub( const amountLeft = Amounts.sub(
@ -153,18 +165,21 @@ async function applySuccessfulRefund(
} }
async function storePendingRefund( async function storePendingRefund(
tx: TransactionHandle<typeof Stores.denominations | typeof Stores.coins>, tx: GetReadWriteAccess<{
denominations: typeof WalletStoresV1.denominations;
coins: typeof WalletStoresV1.coins;
}>,
p: PurchaseRecord, p: PurchaseRecord,
r: MerchantCoinRefundFailureStatus, r: MerchantCoinRefundFailureStatus,
): Promise<void> { ): Promise<void> {
const refundKey = getRefundKey(r); const refundKey = getRefundKey(r);
const coin = await tx.get(Stores.coins, r.coin_pub); const coin = await tx.coins.get(r.coin_pub);
if (!coin) { if (!coin) {
logger.warn("coin not found, can't apply refund"); logger.warn("coin not found, can't apply refund");
return; return;
} }
const denom = await tx.get(Stores.denominations, [ const denom = await tx.denominations.get([
coin.exchangeBaseUrl, coin.exchangeBaseUrl,
coin.denomPubHash, coin.denomPubHash,
]); ]);
@ -173,11 +188,8 @@ async function storePendingRefund(
throw Error("inconsistent database"); throw Error("inconsistent database");
} }
const allDenoms = await tx const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
.iterIndexed( .iter(coin.exchangeBaseUrl)
Stores.denominations.exchangeBaseUrlIndex,
coin.exchangeBaseUrl,
)
.toArray(); .toArray();
const amountLeft = Amounts.sub( const amountLeft = Amounts.sub(
@ -205,19 +217,22 @@ async function storePendingRefund(
} }
async function storeFailedRefund( async function storeFailedRefund(
tx: TransactionHandle<typeof Stores.coins | typeof Stores.denominations>, tx: GetReadWriteAccess<{
coins: typeof WalletStoresV1.coins;
denominations: typeof WalletStoresV1.denominations;
}>,
p: PurchaseRecord, p: PurchaseRecord,
refreshCoinsMap: Record<string, { coinPub: string }>, refreshCoinsMap: Record<string, { coinPub: string }>,
r: MerchantCoinRefundFailureStatus, r: MerchantCoinRefundFailureStatus,
): Promise<void> { ): Promise<void> {
const refundKey = getRefundKey(r); const refundKey = getRefundKey(r);
const coin = await tx.get(Stores.coins, r.coin_pub); const coin = await tx.coins.get(r.coin_pub);
if (!coin) { if (!coin) {
logger.warn("coin not found, can't apply refund"); logger.warn("coin not found, can't apply refund");
return; return;
} }
const denom = await tx.get(Stores.denominations, [ const denom = await tx.denominations.get([
coin.exchangeBaseUrl, coin.exchangeBaseUrl,
coin.denomPubHash, coin.denomPubHash,
]); ]);
@ -226,11 +241,8 @@ async function storeFailedRefund(
throw Error("inconsistent database"); throw Error("inconsistent database");
} }
const allDenoms = await tx const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
.iterIndexed( .iter(coin.exchangeBaseUrl)
Stores.denominations.exchangeBaseUrlIndex,
coin.exchangeBaseUrl,
)
.toArray(); .toArray();
const amountLeft = Amounts.sub( const amountLeft = Amounts.sub(
@ -260,12 +272,12 @@ async function storeFailedRefund(
// Refund failed because the merchant didn't even try to deposit // Refund failed because the merchant didn't even try to deposit
// the coin yet, so we try to refresh. // the coin yet, so we try to refresh.
if (r.exchange_code === TalerErrorCode.EXCHANGE_REFUND_DEPOSIT_NOT_FOUND) { if (r.exchange_code === TalerErrorCode.EXCHANGE_REFUND_DEPOSIT_NOT_FOUND) {
const coin = await tx.get(Stores.coins, r.coin_pub); const coin = await tx.coins.get(r.coin_pub);
if (!coin) { if (!coin) {
logger.warn("coin not found, can't apply refund"); logger.warn("coin not found, can't apply refund");
return; return;
} }
const denom = await tx.get(Stores.denominations, [ const denom = await tx.denominations.get([
coin.exchangeBaseUrl, coin.exchangeBaseUrl,
coin.denomPubHash, coin.denomPubHash,
]); ]);
@ -287,7 +299,7 @@ async function storeFailedRefund(
).amount; ).amount;
} }
refreshCoinsMap[coin.coinPub] = { coinPub: coin.coinPub }; refreshCoinsMap[coin.coinPub] = { coinPub: coin.coinPub };
await tx.put(Stores.coins, coin); await tx.coins.put(coin);
} }
} }
} }
@ -301,15 +313,15 @@ async function acceptRefunds(
logger.trace("handling refunds", refunds); logger.trace("handling refunds", refunds);
const now = getTimestampNow(); const now = getTimestampNow();
await ws.db.runWithWriteTransaction( await ws.db
[ .mktx((x) => ({
Stores.purchases, purchases: x.purchases,
Stores.coins, coins: x.coins,
Stores.denominations, denominations: x.denominations,
Stores.refreshGroups, refreshGroups: x.refreshGroups,
], }))
async (tx) => { .runReadWrite(async (tx) => {
const p = await tx.get(Stores.purchases, proposalId); const p = await tx.purchases.get(proposalId);
if (!p) { if (!p) {
logger.error("purchase not found, not adding refunds"); logger.error("purchase not found, not adding refunds");
return; return;
@ -409,9 +421,8 @@ async function acceptRefunds(
logger.trace("refund query not done"); logger.trace("refund query not done");
} }
await tx.put(Stores.purchases, p); await tx.purchases.put(p);
}, });
);
ws.notify({ ws.notify({
type: NotificationType.RefundQueried, type: NotificationType.RefundQueried,
@ -444,10 +455,16 @@ export async function applyRefund(
throw Error("invalid refund URI"); throw Error("invalid refund URI");
} }
let purchase = await ws.db.getIndexed(Stores.purchases.orderIdIndex, [ let purchase = await ws.db
.mktx((x) => ({
purchases: x.purchases,
}))
.runReadOnly(async (tx) => {
return tx.purchases.indexes.byMerchantUrlAndOrderId.get([
parseResult.merchantBaseUrl, parseResult.merchantBaseUrl,
parseResult.orderId, parseResult.orderId,
]); ]);
});
if (!purchase) { if (!purchase) {
throw Error( throw Error(
@ -458,10 +475,12 @@ export async function applyRefund(
const proposalId = purchase.proposalId; const proposalId = purchase.proposalId;
logger.info("processing purchase for refund"); logger.info("processing purchase for refund");
const success = await ws.db.runWithWriteTransaction( const success = await ws.db
[Stores.purchases], .mktx((x) => ({
async (tx) => { purchases: x.purchases,
const p = await tx.get(Stores.purchases, proposalId); }))
.runReadWrite(async (tx) => {
const p = await tx.purchases.get(proposalId);
if (!p) { if (!p) {
logger.error("no purchase found for refund URL"); logger.error("no purchase found for refund URL");
return false; return false;
@ -469,10 +488,9 @@ export async function applyRefund(
p.refundQueryRequested = true; p.refundQueryRequested = true;
p.lastRefundStatusError = undefined; p.lastRefundStatusError = undefined;
p.refundStatusRetryInfo = initRetryInfo(); p.refundStatusRetryInfo = initRetryInfo();
await tx.put(Stores.purchases, p); await tx.purchases.put(p);
return true; return true;
}, });
);
if (success) { if (success) {
ws.notify({ ws.notify({
@ -481,7 +499,13 @@ export async function applyRefund(
await processPurchaseQueryRefund(ws, proposalId); await processPurchaseQueryRefund(ws, proposalId);
} }
purchase = await ws.db.get(Stores.purchases, proposalId); purchase = await ws.db
.mktx((x) => ({
purchases: x.purchases,
}))
.runReadOnly(async (tx) => {
return tx.purchases.get(proposalId);
});
if (!purchase) { if (!purchase) {
throw Error("purchase no longer exists"); throw Error("purchase no longer exists");
@ -559,11 +583,16 @@ async function resetPurchaseQueryRefundRetry(
ws: InternalWalletState, ws: InternalWalletState,
proposalId: string, proposalId: string,
): Promise<void> { ): Promise<void> {
await ws.db.mutate(Stores.purchases, proposalId, (x) => { await ws.db
if (x.refundStatusRetryInfo.active) { .mktx((x) => ({
purchases: x.purchases,
}))
.runReadWrite(async (tx) => {
const x = await tx.purchases.get(proposalId);
if (x && x.refundStatusRetryInfo.active) {
x.refundStatusRetryInfo = initRetryInfo(); x.refundStatusRetryInfo = initRetryInfo();
await tx.purchases.put(x);
} }
return x;
}); });
} }
@ -575,7 +604,13 @@ async function processPurchaseQueryRefundImpl(
if (forceNow) { if (forceNow) {
await resetPurchaseQueryRefundRetry(ws, proposalId); await resetPurchaseQueryRefundRetry(ws, proposalId);
} }
const purchase = await ws.db.get(Stores.purchases, proposalId); const purchase = await ws.db
.mktx((x) => ({
purchases: x.purchases,
}))
.runReadOnly(async (tx) => {
return tx.purchases.get(proposalId);
});
if (!purchase) { if (!purchase) {
return; return;
} }
@ -590,7 +625,6 @@ async function processPurchaseQueryRefundImpl(
purchase.download.contractData.merchantBaseUrl, purchase.download.contractData.merchantBaseUrl,
); );
logger.trace(`making refund request to ${requestUrl.href}`); logger.trace(`making refund request to ${requestUrl.href}`);
const request = await ws.http.postJson(requestUrl.href, { const request = await ws.http.postJson(requestUrl.href, {
@ -620,9 +654,15 @@ async function processPurchaseQueryRefundImpl(
); );
const abortingCoins: AbortingCoin[] = []; const abortingCoins: AbortingCoin[] = [];
await ws.db
.mktx((x) => ({
coins: x.coins,
}))
.runReadOnly(async (tx) => {
for (let i = 0; i < purchase.payCoinSelection.coinPubs.length; i++) { for (let i = 0; i < purchase.payCoinSelection.coinPubs.length; i++) {
const coinPub = purchase.payCoinSelection.coinPubs[i]; const coinPub = purchase.payCoinSelection.coinPubs[i];
const coin = await ws.db.get(Stores.coins, coinPub); const coin = await tx.coins.get(coinPub);
checkDbInvariant(!!coin, "expected coin to be present"); checkDbInvariant(!!coin, "expected coin to be present");
abortingCoins.push({ abortingCoins.push({
coin_pub: coinPub, coin_pub: coinPub,
@ -632,6 +672,7 @@ async function processPurchaseQueryRefundImpl(
exchange_url: coin.exchangeBaseUrl, exchange_url: coin.exchangeBaseUrl,
}); });
} }
});
const abortReq: AbortRequest = { const abortReq: AbortRequest = {
h_contract: purchase.download.contractData.contractTermsHash, h_contract: purchase.download.contractData.contractTermsHash,
@ -678,8 +719,12 @@ export async function abortFailedPayWithRefund(
ws: InternalWalletState, ws: InternalWalletState,
proposalId: string, proposalId: string,
): Promise<void> { ): Promise<void> {
await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => { await ws.db
const purchase = await tx.get(Stores.purchases, proposalId); .mktx((x) => ({
purchases: x.purchases,
}))
.runReadWrite(async (tx) => {
const purchase = await tx.purchases.get(proposalId);
if (!purchase) { if (!purchase) {
throw Error("purchase not found"); throw Error("purchase not found");
} }
@ -696,7 +741,7 @@ export async function abortFailedPayWithRefund(
purchase.abortStatus = AbortStatus.AbortRefund; purchase.abortStatus = AbortStatus.AbortRefund;
purchase.lastPayError = undefined; purchase.lastPayError = undefined;
purchase.payRetryInfo = initRetryInfo(false); purchase.payRetryInfo = initRetryInfo(false);
await tx.put(Stores.purchases, purchase); await tx.purchases.put(purchase);
}); });
processPurchaseQueryRefund(ws, proposalId, true).catch((e) => { processPurchaseQueryRefund(ws, proposalId, true).catch((e) => {
logger.trace(`error during refund processing after abort pay: ${e}`); logger.trace(`error during refund processing after abort pay: ${e}`);

View File

@ -34,11 +34,11 @@ import {
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { randomBytes } from "../crypto/primitives/nacl-fast.js"; import { randomBytes } from "../crypto/primitives/nacl-fast.js";
import { import {
Stores,
ReserveRecordStatus, ReserveRecordStatus,
ReserveBankInfo, ReserveBankInfo,
ReserveRecord, ReserveRecord,
WithdrawalGroupRecord, WithdrawalGroupRecord,
WalletStoresV1,
} from "../db.js"; } from "../db.js";
import { assertUnreachable } from "../util/assertUnreachable.js"; import { assertUnreachable } from "../util/assertUnreachable.js";
import { canonicalizeBaseUrl } from "@gnu-taler/taler-util"; import { canonicalizeBaseUrl } from "@gnu-taler/taler-util";
@ -65,9 +65,13 @@ import {
import { getExchangeTrust } from "./currencies.js"; import { getExchangeTrust } from "./currencies.js";
import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto.js"; import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto.js";
import { Logger } from "@gnu-taler/taler-util"; import { Logger } from "@gnu-taler/taler-util";
import { readSuccessResponseJsonOrErrorCode, readSuccessResponseJsonOrThrow, throwUnexpectedRequestError } from "../util/http.js"; import {
readSuccessResponseJsonOrErrorCode,
readSuccessResponseJsonOrThrow,
throwUnexpectedRequestError,
} from "../util/http.js";
import { URL } from "../util/url.js"; import { URL } from "../util/url.js";
import { TransactionHandle } from "../util/query.js"; import { GetReadOnlyAccess } from "../util/query.js";
const logger = new Logger("reserves.ts"); const logger = new Logger("reserves.ts");
@ -75,11 +79,16 @@ async function resetReserveRetry(
ws: InternalWalletState, ws: InternalWalletState,
reservePub: string, reservePub: string,
): Promise<void> { ): Promise<void> {
await ws.db.mutate(Stores.reserves, reservePub, (x) => { await ws.db
if (x.retryInfo.active) { .mktx((x) => ({
reserves: x.reserves,
}))
.runReadWrite(async (tx) => {
const x = await tx.reserves.get(reservePub);
if (x && x.retryInfo.active) {
x.retryInfo = initRetryInfo(); x.retryInfo = initRetryInfo();
await tx.reserves.put(x);
} }
return x;
}); });
} }
@ -157,17 +166,20 @@ export async function createReserve(
exchangeInfo.exchange, exchangeInfo.exchange,
); );
const resp = await ws.db.runWithWriteTransaction( const resp = await ws.db
[Stores.exchangeTrustStore, Stores.reserves, Stores.bankWithdrawUris], .mktx((x) => ({
async (tx) => { exchangeTrust: x.exchangeTrust,
reserves: x.reserves,
bankWithdrawUris: x.bankWithdrawUris,
}))
.runReadWrite(async (tx) => {
// Check if we have already created a reserve for that bankWithdrawStatusUrl // Check if we have already created a reserve for that bankWithdrawStatusUrl
if (reserveRecord.bankInfo?.statusUrl) { if (reserveRecord.bankInfo?.statusUrl) {
const bwi = await tx.get( const bwi = await tx.bankWithdrawUris.get(
Stores.bankWithdrawUris,
reserveRecord.bankInfo.statusUrl, reserveRecord.bankInfo.statusUrl,
); );
if (bwi) { if (bwi) {
const otherReserve = await tx.get(Stores.reserves, bwi.reservePub); const otherReserve = await tx.reserves.get(bwi.reservePub);
if (otherReserve) { if (otherReserve) {
logger.trace( logger.trace(
"returning existing reserve for bankWithdrawStatusUri", "returning existing reserve for bankWithdrawStatusUri",
@ -178,27 +190,26 @@ export async function createReserve(
}; };
} }
} }
await tx.put(Stores.bankWithdrawUris, { await tx.bankWithdrawUris.put({
reservePub: reserveRecord.reservePub, reservePub: reserveRecord.reservePub,
talerWithdrawUri: reserveRecord.bankInfo.statusUrl, talerWithdrawUri: reserveRecord.bankInfo.statusUrl,
}); });
} }
if (!isAudited && !isTrusted) { if (!isAudited && !isTrusted) {
await tx.put(Stores.exchangeTrustStore, { await tx.exchangeTrust.put({
currency: reserveRecord.currency, currency: reserveRecord.currency,
exchangeBaseUrl: reserveRecord.exchangeBaseUrl, exchangeBaseUrl: reserveRecord.exchangeBaseUrl,
exchangeMasterPub: exchangeDetails.masterPublicKey, exchangeMasterPub: exchangeDetails.masterPublicKey,
uids: [encodeCrock(getRandomBytes(32))], uids: [encodeCrock(getRandomBytes(32))],
}); });
} }
await tx.put(Stores.reserves, reserveRecord); await tx.reserves.put(reserveRecord);
const r: CreateReserveResponse = { const r: CreateReserveResponse = {
exchange: canonExchange, exchange: canonExchange,
reservePub: keypair.pub, reservePub: keypair.pub,
}; };
return r; return r;
}, });
);
if (reserveRecord.reservePub === resp.reservePub) { if (reserveRecord.reservePub === resp.reservePub) {
// Only emit notification when a new reserve was created. // Only emit notification when a new reserve was created.
@ -224,8 +235,12 @@ export async function forceQueryReserve(
ws: InternalWalletState, ws: InternalWalletState,
reservePub: string, reservePub: string,
): Promise<void> { ): Promise<void> {
await ws.db.runWithWriteTransaction([Stores.reserves], async (tx) => { await ws.db
const reserve = await tx.get(Stores.reserves, reservePub); .mktx((x) => ({
reserves: x.reserves,
}))
.runReadWrite(async (tx) => {
const reserve = await tx.reserves.get(reservePub);
if (!reserve) { if (!reserve) {
return; return;
} }
@ -239,7 +254,7 @@ export async function forceQueryReserve(
break; break;
} }
reserve.retryInfo = initRetryInfo(); reserve.retryInfo = initRetryInfo();
await tx.put(Stores.reserves, reserve); await tx.reserves.put(reserve);
}); });
await processReserve(ws, reservePub, true); await processReserve(ws, reservePub, true);
} }
@ -270,7 +285,13 @@ async function registerReserveWithBank(
ws: InternalWalletState, ws: InternalWalletState,
reservePub: string, reservePub: string,
): Promise<void> { ): Promise<void> {
const reserve = await ws.db.get(Stores.reserves, reservePub); const reserve = await ws.db
.mktx((x) => ({
reserves: x.reserves,
}))
.runReadOnly(async (tx) => {
return await tx.reserves.get(reservePub);
});
switch (reserve?.reserveStatus) { switch (reserve?.reserveStatus) {
case ReserveRecordStatus.WAIT_CONFIRM_BANK: case ReserveRecordStatus.WAIT_CONFIRM_BANK:
case ReserveRecordStatus.REGISTERING_BANK: case ReserveRecordStatus.REGISTERING_BANK:
@ -297,7 +318,15 @@ async function registerReserveWithBank(
httpResp, httpResp,
codecForBankWithdrawalOperationPostResponse(), codecForBankWithdrawalOperationPostResponse(),
); );
await ws.db.mutate(Stores.reserves, reservePub, (r) => { await ws.db
.mktx((x) => ({
reserves: x.reserves,
}))
.runReadWrite(async (tx) => {
const r = await tx.reserves.get(reservePub);
if (!r) {
return;
}
switch (r.reserveStatus) { switch (r.reserveStatus) {
case ReserveRecordStatus.REGISTERING_BANK: case ReserveRecordStatus.REGISTERING_BANK:
case ReserveRecordStatus.WAIT_CONFIRM_BANK: case ReserveRecordStatus.WAIT_CONFIRM_BANK:
@ -311,7 +340,7 @@ async function registerReserveWithBank(
throw Error("invariant failed"); throw Error("invariant failed");
} }
r.retryInfo = initRetryInfo(); r.retryInfo = initRetryInfo();
return r; await tx.reserves.put(r);
}); });
ws.notify({ type: NotificationType.ReserveRegisteredWithBank }); ws.notify({ type: NotificationType.ReserveRegisteredWithBank });
return processReserveBankStatus(ws, reservePub); return processReserveBankStatus(ws, reservePub);
@ -340,7 +369,13 @@ async function processReserveBankStatusImpl(
ws: InternalWalletState, ws: InternalWalletState,
reservePub: string, reservePub: string,
): Promise<void> { ): Promise<void> {
const reserve = await ws.db.get(Stores.reserves, reservePub); const reserve = await ws.db
.mktx((x) => ({
reserves: x.reserves,
}))
.runReadOnly(async (tx) => {
return tx.reserves.get(reservePub);
});
switch (reserve?.reserveStatus) { switch (reserve?.reserveStatus) {
case ReserveRecordStatus.WAIT_CONFIRM_BANK: case ReserveRecordStatus.WAIT_CONFIRM_BANK:
case ReserveRecordStatus.REGISTERING_BANK: case ReserveRecordStatus.REGISTERING_BANK:
@ -363,7 +398,15 @@ async function processReserveBankStatusImpl(
if (status.aborted) { if (status.aborted) {
logger.trace("bank aborted the withdrawal"); logger.trace("bank aborted the withdrawal");
await ws.db.mutate(Stores.reserves, reservePub, (r) => { await ws.db
.mktx((x) => ({
reserves: x.reserves,
}))
.runReadWrite(async (tx) => {
const r = await tx.reserves.get(reservePub);
if (!r) {
return;
}
switch (r.reserveStatus) { switch (r.reserveStatus) {
case ReserveRecordStatus.REGISTERING_BANK: case ReserveRecordStatus.REGISTERING_BANK:
case ReserveRecordStatus.WAIT_CONFIRM_BANK: case ReserveRecordStatus.WAIT_CONFIRM_BANK:
@ -375,7 +418,7 @@ async function processReserveBankStatusImpl(
r.timestampBankConfirmed = now; r.timestampBankConfirmed = now;
r.reserveStatus = ReserveRecordStatus.BANK_ABORTED; r.reserveStatus = ReserveRecordStatus.BANK_ABORTED;
r.retryInfo = initRetryInfo(); r.retryInfo = initRetryInfo();
return r; await tx.reserves.put(r);
}); });
return; return;
} }
@ -390,8 +433,16 @@ async function processReserveBankStatusImpl(
return await processReserveBankStatus(ws, reservePub); return await processReserveBankStatus(ws, reservePub);
} }
await ws.db
.mktx((x) => ({
reserves: x.reserves,
}))
.runReadWrite(async (tx) => {
const r = await tx.reserves.get(reservePub);
if (!r) {
return;
}
if (status.transfer_done) { if (status.transfer_done) {
await ws.db.mutate(Stores.reserves, reservePub, (r) => {
switch (r.reserveStatus) { switch (r.reserveStatus) {
case ReserveRecordStatus.REGISTERING_BANK: case ReserveRecordStatus.REGISTERING_BANK:
case ReserveRecordStatus.WAIT_CONFIRM_BANK: case ReserveRecordStatus.WAIT_CONFIRM_BANK:
@ -403,11 +454,7 @@ async function processReserveBankStatusImpl(
r.timestampBankConfirmed = now; r.timestampBankConfirmed = now;
r.reserveStatus = ReserveRecordStatus.QUERYING_STATUS; r.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
r.retryInfo = initRetryInfo(); r.retryInfo = initRetryInfo();
return r;
});
await processReserveImpl(ws, reservePub, true);
} else { } else {
await ws.db.mutate(Stores.reserves, reservePub, (r) => {
switch (r.reserveStatus) { switch (r.reserveStatus) {
case ReserveRecordStatus.WAIT_CONFIRM_BANK: case ReserveRecordStatus.WAIT_CONFIRM_BANK:
break; break;
@ -417,10 +464,9 @@ async function processReserveBankStatusImpl(
if (r.bankInfo) { if (r.bankInfo) {
r.bankInfo.confirmUrl = status.confirm_transfer_url; r.bankInfo.confirmUrl = status.confirm_transfer_url;
} }
return r;
});
await incrementReserveRetry(ws, reservePub, undefined);
} }
await tx.reserves.put(r);
});
} }
async function incrementReserveRetry( async function incrementReserveRetry(
@ -428,8 +474,12 @@ async function incrementReserveRetry(
reservePub: string, reservePub: string,
err: TalerErrorDetails | undefined, err: TalerErrorDetails | undefined,
): Promise<void> { ): Promise<void> {
await ws.db.runWithWriteTransaction([Stores.reserves], async (tx) => { await ws.db
const r = await tx.get(Stores.reserves, reservePub); .mktx((x) => ({
reserves: x.reserves,
}))
.runReadWrite(async (tx) => {
const r = await tx.reserves.get(reservePub);
if (!r) { if (!r) {
return; return;
} }
@ -439,7 +489,7 @@ async function incrementReserveRetry(
r.retryInfo.retryCounter++; r.retryInfo.retryCounter++;
updateRetryInfoTimeout(r.retryInfo); updateRetryInfoTimeout(r.retryInfo);
r.lastError = err; r.lastError = err;
await tx.put(Stores.reserves, r); await tx.reserves.put(r);
}); });
if (err) { if (err) {
ws.notify({ ws.notify({
@ -461,7 +511,13 @@ async function updateReserve(
ws: InternalWalletState, ws: InternalWalletState,
reservePub: string, reservePub: string,
): Promise<{ ready: boolean }> { ): Promise<{ ready: boolean }> {
const reserve = await ws.db.get(Stores.reserves, reservePub); const reserve = await ws.db
.mktx((x) => ({
reserves: x.reserves,
}))
.runReadOnly(async (tx) => {
return tx.reserves.get(reservePub);
});
if (!reserve) { if (!reserve) {
throw Error("reserve not in db"); throw Error("reserve not in db");
} }
@ -508,10 +564,15 @@ async function updateReserve(
reserve.exchangeBaseUrl, reserve.exchangeBaseUrl,
); );
const newWithdrawalGroup = await ws.db.runWithWriteTransaction( const newWithdrawalGroup = await ws.db
[Stores.coins, Stores.planchets, Stores.withdrawalGroups, Stores.reserves], .mktx((x) => ({
async (tx) => { coins: x.coins,
const newReserve = await tx.get(Stores.reserves, reserve.reservePub); planchets: x.planchets,
withdrawalGroups: x.withdrawalGroups,
reserves: x.reserves,
}))
.runReadWrite(async (tx) => {
const newReserve = await tx.reserves.get(reserve.reservePub);
if (!newReserve) { if (!newReserve) {
return; return;
} }
@ -519,8 +580,8 @@ async function updateReserve(
let amountReserveMinus = Amounts.getZero(currency); let amountReserveMinus = Amounts.getZero(currency);
// Subtract withdrawal groups for this reserve from the available amount. // Subtract withdrawal groups for this reserve from the available amount.
await tx await tx.withdrawalGroups.indexes.byReservePub
.iterIndexed(Stores.withdrawalGroups.byReservePub, reservePub) .iter(reservePub)
.forEach((wg) => { .forEach((wg) => {
const cost = wg.denomsSel.totalWithdrawCost; const cost = wg.denomsSel.totalWithdrawCost;
amountReserveMinus = Amounts.add(amountReserveMinus, cost).amount; amountReserveMinus = Amounts.add(amountReserveMinus, cost).amount;
@ -549,16 +610,14 @@ async function updateReserve(
case ReserveTransactionType.Withdraw: { case ReserveTransactionType.Withdraw: {
// Now we check if the withdrawal transaction // Now we check if the withdrawal transaction
// is part of any withdrawal known to this wallet. // is part of any withdrawal known to this wallet.
const planchet = await tx.getIndexed( const planchet = await tx.planchets.indexes.byCoinEvHash.get(
Stores.planchets.coinEvHashIndex,
entry.h_coin_envelope, entry.h_coin_envelope,
); );
if (planchet) { if (planchet) {
// Amount is already accounted in some withdrawal session // Amount is already accounted in some withdrawal session
break; break;
} }
const coin = await tx.getIndexed( const coin = await tx.coins.indexes.byCoinEvHash.get(
Stores.coins.coinEvHashIndex,
entry.h_coin_envelope, entry.h_coin_envelope,
); );
if (coin) { if (coin) {
@ -594,7 +653,7 @@ async function updateReserve(
newReserve.reserveStatus = ReserveRecordStatus.DORMANT; newReserve.reserveStatus = ReserveRecordStatus.DORMANT;
newReserve.lastError = undefined; newReserve.lastError = undefined;
newReserve.retryInfo = initRetryInfo(false); newReserve.retryInfo = initRetryInfo(false);
await tx.put(Stores.reserves, newReserve); await tx.reserves.put(newReserve);
return; return;
} }
@ -624,11 +683,10 @@ async function updateReserve(
newReserve.retryInfo = initRetryInfo(false); newReserve.retryInfo = initRetryInfo(false);
newReserve.reserveStatus = ReserveRecordStatus.DORMANT; newReserve.reserveStatus = ReserveRecordStatus.DORMANT;
await tx.put(Stores.reserves, newReserve); await tx.reserves.put(newReserve);
await tx.put(Stores.withdrawalGroups, withdrawalRecord); await tx.withdrawalGroups.put(withdrawalRecord);
return withdrawalRecord; return withdrawalRecord;
}, });
);
if (newWithdrawalGroup) { if (newWithdrawalGroup) {
logger.trace("processing new withdraw group"); logger.trace("processing new withdraw group");
@ -647,7 +705,13 @@ async function processReserveImpl(
reservePub: string, reservePub: string,
forceNow = false, forceNow = false,
): Promise<void> { ): Promise<void> {
const reserve = await ws.db.get(Stores.reserves, reservePub); const reserve = await ws.db
.mktx((x) => ({
reserves: x.reserves,
}))
.runReadOnly(async (tx) => {
return tx.reserves.get(reservePub);
});
if (!reserve) { if (!reserve) {
logger.trace("not processing reserve: reserve does not exist"); logger.trace("not processing reserve: reserve does not exist");
return; return;
@ -712,7 +776,13 @@ export async function createTalerWithdrawReserve(
// We do this here, as the reserve should be registered before we return, // We do this here, as the reserve should be registered before we return,
// so that we can redirect the user to the bank's status page. // so that we can redirect the user to the bank's status page.
await processReserveBankStatus(ws, reserve.reservePub); await processReserveBankStatus(ws, reserve.reservePub);
const processedReserve = await ws.db.get(Stores.reserves, reserve.reservePub); const processedReserve = await ws.db
.mktx((x) => ({
reserves: x.reserves,
}))
.runReadOnly(async (tx) => {
return tx.reserves.get(reserve.reservePub);
});
if (processedReserve?.reserveStatus === ReserveRecordStatus.BANK_ABORTED) { if (processedReserve?.reserveStatus === ReserveRecordStatus.BANK_ABORTED) {
throw OperationFailedError.fromCode( throw OperationFailedError.fromCode(
TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK, TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
@ -730,14 +800,14 @@ export async function createTalerWithdrawReserve(
* Get payto URIs needed to fund a reserve. * Get payto URIs needed to fund a reserve.
*/ */
export async function getFundingPaytoUris( export async function getFundingPaytoUris(
tx: TransactionHandle< tx: GetReadOnlyAccess<{
| typeof Stores.reserves reserves: typeof WalletStoresV1.reserves;
| typeof Stores.exchanges exchanges: typeof WalletStoresV1.exchanges;
| typeof Stores.exchangeDetails exchangeDetails: typeof WalletStoresV1.exchangeDetails;
>, }>,
reservePub: string, reservePub: string,
): Promise<string[]> { ): Promise<string[]> {
const r = await tx.get(Stores.reserves, reservePub); const r = await tx.reserves.get(reservePub);
if (!r) { if (!r) {
logger.error(`reserve ${reservePub} not found (DB corrupted?)`); logger.error(`reserve ${reservePub} not found (DB corrupted?)`);
return []; return [];

View File

@ -17,12 +17,22 @@
/** /**
* Imports. * Imports.
*/ */
import { WalletNotification, BalancesResponse, Logger } from "@gnu-taler/taler-util"; import {
import { Stores } from "../db.js"; WalletNotification,
import { CryptoApi, OpenedPromise, Database, CryptoWorkerFactory, openPromise } from "../index.js"; BalancesResponse,
Logger,
} from "@gnu-taler/taler-util";
import { WalletStoresV1 } from "../db.js";
import {
CryptoApi,
OpenedPromise,
CryptoWorkerFactory,
openPromise,
} from "../index.js";
import { PendingOperationsResponse } from "../pending-types.js"; import { PendingOperationsResponse } from "../pending-types.js";
import { AsyncOpMemoMap, AsyncOpMemoSingle } from "../util/asyncMemo.js"; import { AsyncOpMemoMap, AsyncOpMemoSingle } from "../util/asyncMemo.js";
import { HttpRequestLibrary } from "../util/http"; import { HttpRequestLibrary } from "../util/http";
import { DbAccess } from "../util/query.js";
type NotificationListener = (n: WalletNotification) => void; type NotificationListener = (n: WalletNotification) => void;
@ -34,9 +44,7 @@ export const EXCHANGE_RESERVES_LOCK = "exchange-reserves-lock";
export class InternalWalletState { export class InternalWalletState {
memoProcessReserve: AsyncOpMemoMap<void> = new AsyncOpMemoMap(); memoProcessReserve: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
memoMakePlanchet: AsyncOpMemoMap<void> = new AsyncOpMemoMap(); memoMakePlanchet: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
memoGetPending: AsyncOpMemoSingle< memoGetPending: AsyncOpMemoSingle<PendingOperationsResponse> = new AsyncOpMemoSingle();
PendingOperationsResponse
> = new AsyncOpMemoSingle();
memoGetBalance: AsyncOpMemoSingle<BalancesResponse> = new AsyncOpMemoSingle(); memoGetBalance: AsyncOpMemoSingle<BalancesResponse> = new AsyncOpMemoSingle();
memoProcessRefresh: AsyncOpMemoMap<void> = new AsyncOpMemoMap(); memoProcessRefresh: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
memoProcessRecoup: AsyncOpMemoMap<void> = new AsyncOpMemoMap(); memoProcessRecoup: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
@ -60,7 +68,7 @@ export class InternalWalletState {
// the actual value nullable. // the actual value nullable.
// Check if we are in a DB migration / garbage collection // Check if we are in a DB migration / garbage collection
// and throw an error in that case. // and throw an error in that case.
public db: Database<typeof Stores>, public db: DbAccess<typeof WalletStoresV1>,
public http: HttpRequestLibrary, public http: HttpRequestLibrary,
cryptoWorkerFactory: CryptoWorkerFactory, cryptoWorkerFactory: CryptoWorkerFactory,
) { ) {

View File

@ -32,7 +32,6 @@ import {
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { DerivedTipPlanchet } from "../crypto/cryptoTypes.js"; import { DerivedTipPlanchet } from "../crypto/cryptoTypes.js";
import { import {
Stores,
DenominationRecord, DenominationRecord,
CoinRecord, CoinRecord,
CoinSourceType, CoinSourceType,
@ -70,10 +69,16 @@ export async function prepareTip(
throw Error("invalid taler://tip URI"); throw Error("invalid taler://tip URI");
} }
let tipRecord = await ws.db.getIndexed( let tipRecord = await ws.db
Stores.tips.byMerchantTipIdAndBaseUrl, .mktx((x) => ({
[res.merchantTipId, res.merchantBaseUrl], tips: x.tips,
); }))
.runReadOnly(async (tx) => {
return tx.tips.indexes.byMerchantTipIdAndBaseUrl.get([
res.merchantTipId,
res.merchantBaseUrl,
]);
});
if (!tipRecord) { if (!tipRecord) {
const tipStatusUrl = new URL( const tipStatusUrl = new URL(
@ -109,7 +114,7 @@ export async function prepareTip(
const secretSeed = encodeCrock(getRandomBytes(64)); const secretSeed = encodeCrock(getRandomBytes(64));
const denomSelUid = encodeCrock(getRandomBytes(32)); const denomSelUid = encodeCrock(getRandomBytes(32));
tipRecord = { const newTipRecord = {
walletTipId: walletTipId, walletTipId: walletTipId,
acceptedTimestamp: undefined, acceptedTimestamp: undefined,
tipAmountRaw: amount, tipAmountRaw: amount,
@ -130,7 +135,14 @@ export async function prepareTip(
secretSeed, secretSeed,
denomSelUid, denomSelUid,
}; };
await ws.db.put(Stores.tips, tipRecord); await ws.db
.mktx((x) => ({
tips: x.tips,
}))
.runReadWrite(async (tx) => {
await tx.tips.put(newTipRecord);
});
tipRecord = newTipRecord;
} }
const tipStatus: PrepareTipResult = { const tipStatus: PrepareTipResult = {
@ -151,8 +163,12 @@ async function incrementTipRetry(
walletTipId: string, walletTipId: string,
err: TalerErrorDetails | undefined, err: TalerErrorDetails | undefined,
): Promise<void> { ): Promise<void> {
await ws.db.runWithWriteTransaction([Stores.tips], async (tx) => { await ws.db
const t = await tx.get(Stores.tips, walletTipId); .mktx((x) => ({
tips: x.tips,
}))
.runReadWrite(async (tx) => {
const t = await tx.tips.get(walletTipId);
if (!t) { if (!t) {
return; return;
} }
@ -162,7 +178,7 @@ async function incrementTipRetry(
t.retryInfo.retryCounter++; t.retryInfo.retryCounter++;
updateRetryInfoTimeout(t.retryInfo); updateRetryInfoTimeout(t.retryInfo);
t.lastError = err; t.lastError = err;
await tx.put(Stores.tips, t); await tx.tips.put(t);
}); });
if (err) { if (err) {
ws.notify({ type: NotificationType.TipOperationError, error: err }); ws.notify({ type: NotificationType.TipOperationError, error: err });
@ -186,11 +202,16 @@ async function resetTipRetry(
ws: InternalWalletState, ws: InternalWalletState,
tipId: string, tipId: string,
): Promise<void> { ): Promise<void> {
await ws.db.mutate(Stores.tips, tipId, (x) => { await ws.db
if (x.retryInfo.active) { .mktx((x) => ({
tips: x.tips,
}))
.runReadWrite(async (tx) => {
const x = await tx.tips.get(tipId);
if (x && x.retryInfo.active) {
x.retryInfo = initRetryInfo(); x.retryInfo = initRetryInfo();
await tx.tips.put(x);
} }
return x;
}); });
} }
@ -202,7 +223,13 @@ async function processTipImpl(
if (forceNow) { if (forceNow) {
await resetTipRetry(ws, walletTipId); await resetTipRetry(ws, walletTipId);
} }
let tipRecord = await ws.db.get(Stores.tips, walletTipId); const tipRecord = await ws.db
.mktx((x) => ({
tips: x.tips,
}))
.runReadOnly(async (tx) => {
return tx.tips.get(walletTipId);
});
if (!tipRecord) { if (!tipRecord) {
return; return;
} }
@ -214,19 +241,22 @@ async function processTipImpl(
const denomsForWithdraw = tipRecord.denomsSel; const denomsForWithdraw = tipRecord.denomsSel;
tipRecord = await ws.db.get(Stores.tips, walletTipId);
checkDbInvariant(!!tipRecord, "tip record should be in database");
const planchets: DerivedTipPlanchet[] = []; const planchets: DerivedTipPlanchet[] = [];
// Planchets in the form that the merchant expects // Planchets in the form that the merchant expects
const planchetsDetail: TipPlanchetDetail[] = []; const planchetsDetail: TipPlanchetDetail[] = [];
const denomForPlanchet: { [index: number]: DenominationRecord } = []; const denomForPlanchet: { [index: number]: DenominationRecord } = [];
for (const dh of denomsForWithdraw.selectedDenoms) { for (const dh of denomsForWithdraw.selectedDenoms) {
const denom = await ws.db.get(Stores.denominations, [ const denom = await ws.db
.mktx((x) => ({
denominations: x.denominations,
}))
.runReadOnly(async (tx) => {
return tx.denominations.get([
tipRecord.exchangeBaseUrl, tipRecord.exchangeBaseUrl,
dh.denomPubHash, dh.denomPubHash,
]); ]);
});
checkDbInvariant(!!denom, "denomination should be in database"); checkDbInvariant(!!denom, "denomination should be in database");
for (let i = 0; i < dh.count; i++) { for (let i = 0; i < dh.count; i++) {
const deriveReq = { const deriveReq = {
@ -306,8 +336,10 @@ async function processTipImpl(
); );
if (!isValid) { if (!isValid) {
await ws.db.runWithWriteTransaction([Stores.tips], async (tx) => { await ws.db
const tipRecord = await tx.get(Stores.tips, walletTipId); .mktx((x) => ({ tips: x.tips }))
.runReadWrite(async (tx) => {
const tipRecord = await tx.tips.get(walletTipId);
if (!tipRecord) { if (!tipRecord) {
return; return;
} }
@ -316,7 +348,7 @@ async function processTipImpl(
"invalid signature from the exchange (via merchant tip) after unblinding", "invalid signature from the exchange (via merchant tip) after unblinding",
{}, {},
); );
await tx.put(Stores.tips, tipRecord); await tx.tips.put(tipRecord);
}); });
return; return;
} }
@ -341,10 +373,14 @@ async function processTipImpl(
}); });
} }
await ws.db.runWithWriteTransaction( await ws.db
[Stores.coins, Stores.tips, Stores.withdrawalGroups], .mktx((x) => ({
async (tx) => { coins: x.coins,
const tr = await tx.get(Stores.tips, walletTipId); tips: x.tips,
withdrawalGroups: x.withdrawalGroups,
}))
.runReadWrite(async (tx) => {
const tr = await tx.tips.get(walletTipId);
if (!tr) { if (!tr) {
return; return;
} }
@ -354,27 +390,32 @@ async function processTipImpl(
tr.pickedUpTimestamp = getTimestampNow(); tr.pickedUpTimestamp = getTimestampNow();
tr.lastError = undefined; tr.lastError = undefined;
tr.retryInfo = initRetryInfo(false); tr.retryInfo = initRetryInfo(false);
await tx.put(Stores.tips, tr); await tx.tips.put(tr);
for (const cr of newCoinRecords) { for (const cr of newCoinRecords) {
await tx.put(Stores.coins, cr); await tx.coins.put(cr);
} }
}, });
);
} }
export async function acceptTip( export async function acceptTip(
ws: InternalWalletState, ws: InternalWalletState,
tipId: string, tipId: string,
): Promise<void> { ): Promise<void> {
const tipRecord = await ws.db.get(Stores.tips, tipId); const found = await ws.db
.mktx((x) => ({
tips: x.tips,
}))
.runReadWrite(async (tx) => {
const tipRecord = await tx.tips.get(tipId);
if (!tipRecord) { if (!tipRecord) {
logger.error("tip not found"); logger.error("tip not found");
return; return false;
} }
tipRecord.acceptedTimestamp = getTimestampNow(); tipRecord.acceptedTimestamp = getTimestampNow();
await ws.db.put(Stores.tips, tipRecord); await tx.tips.put(tipRecord);
return true;
});
if (found) {
await processTip(ws, tipId); await processTip(ws, tipId);
return; }
} }

View File

@ -19,7 +19,6 @@
*/ */
import { InternalWalletState } from "./state"; import { InternalWalletState } from "./state";
import { import {
Stores,
WalletRefundItem, WalletRefundItem,
RefundState, RefundState,
ReserveRecordStatus, ReserveRecordStatus,
@ -85,26 +84,27 @@ export async function getTransactions(
): Promise<TransactionsResponse> { ): Promise<TransactionsResponse> {
const transactions: Transaction[] = []; const transactions: Transaction[] = [];
await ws.db.runWithReadTransaction( await ws.db
[ .mktx((x) => ({
Stores.coins, coins: x.coins,
Stores.denominations, denominations: x.denominations,
Stores.exchanges, exchanges: x.exchanges,
Stores.exchangeDetails, exchangeDetails: x.exchangeDetails,
Stores.proposals, proposals: x.proposals,
Stores.purchases, purchases: x.purchases,
Stores.refreshGroups, refreshGroups: x.refreshGroups,
Stores.reserves, reserves: x.reserves,
Stores.tips, tips: x.tips,
Stores.withdrawalGroups, withdrawalGroups: x.withdrawalGroups,
Stores.planchets, planchets: x.planchets,
Stores.recoupGroups, recoupGroups: x.recoupGroups,
Stores.depositGroups, depositGroups: x.depositGroups,
Stores.tombstones, tombstones: x.tombstones,
], }))
.runReadOnly(
// Report withdrawals that are currently in progress. // Report withdrawals that are currently in progress.
async (tx) => { async (tx) => {
tx.iter(Stores.withdrawalGroups).forEachAsync(async (wsr) => { tx.withdrawalGroups.iter().forEachAsync(async (wsr) => {
if ( if (
shouldSkipCurrency( shouldSkipCurrency(
transactionsRequest, transactionsRequest,
@ -118,7 +118,7 @@ export async function getTransactions(
return; return;
} }
const r = await tx.get(Stores.reserves, wsr.reservePub); const r = await tx.reserves.get(wsr.reservePub);
if (!r) { if (!r) {
return; return;
} }
@ -147,7 +147,8 @@ export async function getTransactions(
withdrawalDetails = { withdrawalDetails = {
type: WithdrawalType.ManualTransfer, type: WithdrawalType.ManualTransfer,
exchangePaytoUris: exchangePaytoUris:
exchangeDetails.wireInfo?.accounts.map((x) => x.payto_uri) ?? [], exchangeDetails.wireInfo?.accounts.map((x) => x.payto_uri) ??
[],
}; };
} }
transactions.push({ transactions.push({
@ -169,7 +170,7 @@ export async function getTransactions(
// Report pending withdrawals based on reserves that // Report pending withdrawals based on reserves that
// were created, but where the actual withdrawal group has // were created, but where the actual withdrawal group has
// not started yet. // not started yet.
tx.iter(Stores.reserves).forEachAsync(async (r) => { tx.reserves.iter().forEachAsync(async (r) => {
if (shouldSkipCurrency(transactionsRequest, r.currency)) { if (shouldSkipCurrency(transactionsRequest, r.currency)) {
return; return;
} }
@ -198,7 +199,9 @@ export async function getTransactions(
transactions.push({ transactions.push({
type: TransactionType.Withdrawal, type: TransactionType.Withdrawal,
amountRaw: Amounts.stringify(r.instructedAmount), amountRaw: Amounts.stringify(r.instructedAmount),
amountEffective: Amounts.stringify(r.initialDenomSel.totalCoinValue), amountEffective: Amounts.stringify(
r.initialDenomSel.totalCoinValue,
),
exchangeBaseUrl: r.exchangeBaseUrl, exchangeBaseUrl: r.exchangeBaseUrl,
pending: true, pending: true,
timestamp: r.timestampCreated, timestamp: r.timestampCreated,
@ -211,7 +214,7 @@ export async function getTransactions(
}); });
}); });
tx.iter(Stores.depositGroups).forEachAsync(async (dg) => { tx.depositGroups.iter().forEachAsync(async (dg) => {
const amount = Amounts.parseOrThrow(dg.contractTermsRaw.amount); const amount = Amounts.parseOrThrow(dg.contractTermsRaw.amount);
if (shouldSkipCurrency(transactionsRequest, amount.currency)) { if (shouldSkipCurrency(transactionsRequest, amount.currency)) {
return; return;
@ -233,7 +236,7 @@ export async function getTransactions(
}); });
}); });
tx.iter(Stores.purchases).forEachAsync(async (pr) => { tx.purchases.iter().forEachAsync(async (pr) => {
if ( if (
shouldSkipCurrency( shouldSkipCurrency(
transactionsRequest, transactionsRequest,
@ -246,7 +249,7 @@ export async function getTransactions(
if (shouldSkipSearch(transactionsRequest, [contractData.summary])) { if (shouldSkipSearch(transactionsRequest, [contractData.summary])) {
return; return;
} }
const proposal = await tx.get(Stores.proposals, pr.proposalId); const proposal = await tx.proposals.get(pr.proposalId);
if (!proposal) { if (!proposal) {
return; return;
} }
@ -297,7 +300,7 @@ export async function getTransactions(
pr.proposalId, pr.proposalId,
groupKey, groupKey,
); );
const tombstone = await tx.get(Stores.tombstones, refundTombstoneId); const tombstone = await tx.tombstones.get(refundTombstoneId);
if (tombstone) { if (tombstone) {
continue; continue;
} }
@ -347,7 +350,7 @@ export async function getTransactions(
} }
}); });
tx.iter(Stores.tips).forEachAsync(async (tipRecord) => { tx.tips.iter().forEachAsync(async (tipRecord) => {
if ( if (
shouldSkipCurrency( shouldSkipCurrency(
transactionsRequest, transactionsRequest,
@ -406,110 +409,126 @@ export async function deleteTransaction(
if (type === TransactionType.Withdrawal) { if (type === TransactionType.Withdrawal) {
const withdrawalGroupId = rest[0]; const withdrawalGroupId = rest[0];
await ws.db.runWithWriteTransaction( await ws.db
[Stores.withdrawalGroups, Stores.reserves, Stores.tombstones], .mktx((x) => ({
async (tx) => { withdrawalGroups: x.withdrawalGroups,
const withdrawalGroupRecord = await tx.get( reserves: x.reserves,
Stores.withdrawalGroups, tombstones: x.tombstones,
}))
.runReadWrite(async (tx) => {
const withdrawalGroupRecord = await tx.withdrawalGroups.get(
withdrawalGroupId, withdrawalGroupId,
); );
if (withdrawalGroupRecord) { if (withdrawalGroupRecord) {
await tx.delete(Stores.withdrawalGroups, withdrawalGroupId); await tx.withdrawalGroups.delete(withdrawalGroupId);
await tx.put(Stores.tombstones, { await tx.tombstones.put({
id: TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId, id: TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId,
}); });
return; return;
} }
const reserveRecord: ReserveRecord | undefined = await tx.getIndexed( const reserveRecord:
Stores.reserves.byInitialWithdrawalGroupId, | ReserveRecord
| undefined = await tx.reserves.indexes.byInitialWithdrawalGroupId.get(
withdrawalGroupId, withdrawalGroupId,
); );
if (reserveRecord && !reserveRecord.initialWithdrawalStarted) { if (reserveRecord && !reserveRecord.initialWithdrawalStarted) {
const reservePub = reserveRecord.reservePub; const reservePub = reserveRecord.reservePub;
await tx.delete(Stores.reserves, reservePub); await tx.reserves.delete(reservePub);
await tx.put(Stores.tombstones, { await tx.tombstones.put({
id: TombstoneTag.DeleteReserve + ":" + reservePub, id: TombstoneTag.DeleteReserve + ":" + reservePub,
}); });
} }
}, });
);
} else if (type === TransactionType.Payment) { } else if (type === TransactionType.Payment) {
const proposalId = rest[0]; const proposalId = rest[0];
await ws.db.runWithWriteTransaction( await ws.db
[Stores.proposals, Stores.purchases, Stores.tombstones], .mktx((x) => ({
async (tx) => { proposals: x.proposals,
purchases: x.purchases,
tombstones: x.tombstones,
}))
.runReadWrite(async (tx) => {
let found = false; let found = false;
const proposal = await tx.get(Stores.proposals, proposalId); const proposal = await tx.proposals.get(proposalId);
if (proposal) { if (proposal) {
found = true; found = true;
await tx.delete(Stores.proposals, proposalId); await tx.proposals.delete(proposalId);
} }
const purchase = await tx.get(Stores.purchases, proposalId); const purchase = await tx.purchases.get(proposalId);
if (purchase) { if (purchase) {
found = true; found = true;
await tx.delete(Stores.proposals, proposalId); await tx.proposals.delete(proposalId);
} }
if (found) { if (found) {
await tx.put(Stores.tombstones, { await tx.tombstones.put({
id: TombstoneTag.DeletePayment + ":" + proposalId, id: TombstoneTag.DeletePayment + ":" + proposalId,
}); });
} }
}, });
);
} else if (type === TransactionType.Refresh) { } else if (type === TransactionType.Refresh) {
const refreshGroupId = rest[0]; const refreshGroupId = rest[0];
await ws.db.runWithWriteTransaction( await ws.db
[Stores.refreshGroups, Stores.tombstones], .mktx((x) => ({
async (tx) => { refreshGroups: x.refreshGroups,
const rg = await tx.get(Stores.refreshGroups, refreshGroupId); tombstones: x.tombstones,
}))
.runReadWrite(async (tx) => {
const rg = await tx.refreshGroups.get(refreshGroupId);
if (rg) { if (rg) {
await tx.delete(Stores.refreshGroups, refreshGroupId); await tx.refreshGroups.delete(refreshGroupId);
await tx.put(Stores.tombstones, { await tx.tombstones.put({
id: TombstoneTag.DeleteRefreshGroup + ":" + refreshGroupId, id: TombstoneTag.DeleteRefreshGroup + ":" + refreshGroupId,
}); });
} }
}, });
);
} else if (type === TransactionType.Tip) { } else if (type === TransactionType.Tip) {
const tipId = rest[0]; const tipId = rest[0];
await ws.db.runWithWriteTransaction( await ws.db
[Stores.tips, Stores.tombstones], .mktx((x) => ({
async (tx) => { tips: x.tips,
const tipRecord = await tx.get(Stores.tips, tipId); tombstones: x.tombstones,
}))
.runReadWrite(async (tx) => {
const tipRecord = await tx.tips.get(tipId);
if (tipRecord) { if (tipRecord) {
await tx.delete(Stores.tips, tipId); await tx.tips.delete(tipId);
await tx.put(Stores.tombstones, { await tx.tombstones.put({
id: TombstoneTag.DeleteTip + ":" + tipId, id: TombstoneTag.DeleteTip + ":" + tipId,
}); });
} }
}, });
);
} else if (type === TransactionType.Deposit) { } else if (type === TransactionType.Deposit) {
const depositGroupId = rest[0]; const depositGroupId = rest[0];
await ws.db.runWithWriteTransaction( await ws.db
[Stores.depositGroups, Stores.tombstones], .mktx((x) => ({
async (tx) => { depositGroups: x.depositGroups,
const tipRecord = await tx.get(Stores.depositGroups, depositGroupId); tombstones: x.tombstones,
}))
.runReadWrite(async (tx) => {
const tipRecord = await tx.depositGroups.get(depositGroupId);
if (tipRecord) { if (tipRecord) {
await tx.delete(Stores.depositGroups, depositGroupId); await tx.depositGroups.delete(depositGroupId);
await tx.put(Stores.tombstones, { await tx.tombstones.put({
id: TombstoneTag.DeleteDepositGroup + ":" + depositGroupId, id: TombstoneTag.DeleteDepositGroup + ":" + depositGroupId,
}); });
} }
}, });
);
} else if (type === TransactionType.Refund) { } else if (type === TransactionType.Refund) {
const proposalId = rest[0]; const proposalId = rest[0];
const executionTimeStr = rest[1]; const executionTimeStr = rest[1];
await ws.db.runWithWriteTransaction( await ws.db
[Stores.proposals, Stores.purchases, Stores.tombstones], .mktx((x) => ({
async (tx) => { proposals: x.proposals,
const purchase = await tx.get(Stores.purchases, proposalId); purchases: x.purchases,
tombstones: x.tombstones,
}))
.runReadWrite(async (tx) => {
const purchase = await tx.purchases.get(proposalId);
if (purchase) { if (purchase) {
// This should just influence the history view, // This should just influence the history view,
// but won't delete any actual refund information. // but won't delete any actual refund information.
await tx.put(Stores.tombstones, { await tx.tombstones.put({
id: makeEventId( id: makeEventId(
TombstoneTag.DeleteRefund, TombstoneTag.DeleteRefund,
proposalId, proposalId,
@ -517,8 +536,7 @@ export async function deleteTransaction(
), ),
}); });
} }
}, });
);
} else { } else {
throw Error(`can't delete a '${type}' transaction`); throw Error(`can't delete a '${type}' transaction`);
} }

View File

@ -26,7 +26,6 @@ import {
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { import {
DenominationRecord, DenominationRecord,
Stores,
DenominationStatus, DenominationStatus,
CoinStatus, CoinStatus,
CoinRecord, CoinRecord,
@ -314,7 +313,10 @@ export async function getCandidateWithdrawalDenoms(
exchangeBaseUrl: string, exchangeBaseUrl: string,
): Promise<DenominationRecord[]> { ): Promise<DenominationRecord[]> {
return await ws.db return await ws.db
.iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchangeBaseUrl) .mktx((x) => ({ denominations: x.denominations }))
.runReadOnly(async (tx) => {
return tx.denominations.indexes.byExchangeBaseUrl
.iter(exchangeBaseUrl)
.filter((d) => { .filter((d) => {
return ( return (
(d.status === DenominationStatus.Unverified || (d.status === DenominationStatus.Unverified ||
@ -322,6 +324,7 @@ export async function getCandidateWithdrawalDenoms(
!d.isRevoked !d.isRevoked
); );
}); });
});
} }
/** /**
@ -336,17 +339,24 @@ async function processPlanchetGenerate(
withdrawalGroupId: string, withdrawalGroupId: string,
coinIdx: number, coinIdx: number,
): Promise<void> { ): Promise<void> {
const withdrawalGroup = await ws.db.get( const withdrawalGroup = await ws.db
Stores.withdrawalGroups, .mktx((x) => ({ withdrawalGroups: x.withdrawalGroups }))
withdrawalGroupId, .runReadOnly(async (tx) => {
); return await tx.withdrawalGroups.get(withdrawalGroupId);
});
if (!withdrawalGroup) { if (!withdrawalGroup) {
return; return;
} }
let planchet = await ws.db.getIndexed(Stores.planchets.byGroupAndIndex, [ let planchet = await ws.db
.mktx((x) => ({
planchets: x.planchets,
}))
.runReadOnly(async (tx) => {
return tx.planchets.indexes.byGroupAndIndex.get([
withdrawalGroupId, withdrawalGroupId,
coinIdx, coinIdx,
]); ]);
});
if (!planchet) { if (!planchet) {
let ci = 0; let ci = 0;
let denomPubHash: string | undefined; let denomPubHash: string | undefined;
@ -365,20 +375,26 @@ async function processPlanchetGenerate(
if (!denomPubHash) { if (!denomPubHash) {
throw Error("invariant violated"); throw Error("invariant violated");
} }
const denom = await ws.db.get(Stores.denominations, [
const { denom, reserve } = await ws.db
.mktx((x) => ({
reserves: x.reserves,
denominations: x.denominations,
}))
.runReadOnly(async (tx) => {
const denom = await tx.denominations.get([
withdrawalGroup.exchangeBaseUrl, withdrawalGroup.exchangeBaseUrl,
denomPubHash, denomPubHash!,
]); ]);
if (!denom) { if (!denom) {
throw Error("invariant violated"); throw Error("invariant violated");
} }
const reserve = await ws.db.get( const reserve = await tx.reserves.get(withdrawalGroup.reservePub);
Stores.reserves,
withdrawalGroup.reservePub,
);
if (!reserve) { if (!reserve) {
throw Error("invariant violated"); throw Error("invariant violated");
} }
return { denom, reserve };
});
const r = await ws.cryptoApi.createPlanchet({ const r = await ws.cryptoApi.createPlanchet({
denomPub: denom.denomPub, denomPub: denom.denomPub,
feeWithdraw: denom.feeWithdraw, feeWithdraw: denom.feeWithdraw,
@ -405,8 +421,10 @@ async function processPlanchetGenerate(
withdrawalGroupId: withdrawalGroupId, withdrawalGroupId: withdrawalGroupId,
lastError: undefined, lastError: undefined,
}; };
await ws.db.runWithWriteTransaction([Stores.planchets], async (tx) => { await ws.db
const p = await tx.getIndexed(Stores.planchets.byGroupAndIndex, [ .mktx((x) => ({ planchets: x.planchets }))
.runReadWrite(async (tx) => {
const p = await tx.planchets.indexes.byGroupAndIndex.get([
withdrawalGroupId, withdrawalGroupId,
coinIdx, coinIdx,
]); ]);
@ -414,7 +432,7 @@ async function processPlanchetGenerate(
planchet = p; planchet = p;
return; return;
} }
await tx.put(Stores.planchets, newPlanchet); await tx.planchets.put(newPlanchet);
planchet = newPlanchet; planchet = newPlanchet;
}); });
} }
@ -430,14 +448,19 @@ async function processPlanchetExchangeRequest(
withdrawalGroupId: string, withdrawalGroupId: string,
coinIdx: number, coinIdx: number,
): Promise<WithdrawResponse | undefined> { ): Promise<WithdrawResponse | undefined> {
const withdrawalGroup = await ws.db.get( const d = await ws.db
Stores.withdrawalGroups, .mktx((x) => ({
withdrawalGroupId, withdrawalGroups: x.withdrawalGroups,
); planchets: x.planchets,
exchanges: x.exchanges,
denominations: x.denominations,
}))
.runReadOnly(async (tx) => {
const withdrawalGroup = await tx.withdrawalGroups.get(withdrawalGroupId);
if (!withdrawalGroup) { if (!withdrawalGroup) {
return; return;
} }
let planchet = await ws.db.getIndexed(Stores.planchets.byGroupAndIndex, [ let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
withdrawalGroupId, withdrawalGroupId,
coinIdx, coinIdx,
]); ]);
@ -448,16 +471,13 @@ async function processPlanchetExchangeRequest(
logger.warn("processPlanchet: planchet already withdrawn"); logger.warn("processPlanchet: planchet already withdrawn");
return; return;
} }
const exchange = await ws.db.get( const exchange = await tx.exchanges.get(withdrawalGroup.exchangeBaseUrl);
Stores.exchanges,
withdrawalGroup.exchangeBaseUrl,
);
if (!exchange) { if (!exchange) {
logger.error("db inconsistent: exchange for planchet not found"); logger.error("db inconsistent: exchange for planchet not found");
return; return;
} }
const denom = await ws.db.get(Stores.denominations, [ const denom = await tx.denominations.get([
withdrawalGroup.exchangeBaseUrl, withdrawalGroup.exchangeBaseUrl,
planchet.denomPubHash, planchet.denomPubHash,
]); ]);
@ -471,18 +491,27 @@ async function processPlanchetExchangeRequest(
`processing planchet #${coinIdx} in withdrawal ${withdrawalGroupId}`, `processing planchet #${coinIdx} in withdrawal ${withdrawalGroupId}`,
); );
const wd: any = {}; const reqBody: any = {
wd.denom_pub_hash = planchet.denomPubHash; denom_pub_hash: planchet.denomPubHash,
wd.reserve_pub = planchet.reservePub; reserve_pub: planchet.reservePub,
wd.reserve_sig = planchet.withdrawSig; reserve_sig: planchet.withdrawSig,
wd.coin_ev = planchet.coinEv; coin_ev: planchet.coinEv,
};
const reqUrl = new URL( const reqUrl = new URL(
`reserves/${planchet.reservePub}/withdraw`, `reserves/${planchet.reservePub}/withdraw`,
exchange.baseUrl, exchange.baseUrl,
).href; ).href;
return { reqUrl, reqBody };
});
if (!d) {
return;
}
const { reqUrl, reqBody } = d;
try { try {
const resp = await ws.http.postJson(reqUrl, wd); const resp = await ws.http.postJson(reqUrl, reqBody);
const r = await readSuccessResponseJsonOrThrow( const r = await readSuccessResponseJsonOrThrow(
resp, resp,
codecForWithdrawResponse(), codecForWithdrawResponse(),
@ -495,8 +524,10 @@ async function processPlanchetExchangeRequest(
throw e; throw e;
} }
const errDetails = e.operationError; const errDetails = e.operationError;
await ws.db.runWithWriteTransaction([Stores.planchets], async (tx) => { await ws.db
let planchet = await tx.getIndexed(Stores.planchets.byGroupAndIndex, [ .mktx((x) => ({ planchets: x.planchets }))
.runReadWrite(async (tx) => {
let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
withdrawalGroupId, withdrawalGroupId,
coinIdx, coinIdx,
]); ]);
@ -504,7 +535,7 @@ async function processPlanchetExchangeRequest(
return; return;
} }
planchet.lastError = errDetails; planchet.lastError = errDetails;
await tx.put(Stores.planchets, planchet); await tx.planchets.put(planchet);
}); });
return; return;
} }
@ -516,14 +547,17 @@ async function processPlanchetVerifyAndStoreCoin(
coinIdx: number, coinIdx: number,
resp: WithdrawResponse, resp: WithdrawResponse,
): Promise<void> { ): Promise<void> {
const withdrawalGroup = await ws.db.get( const d = await ws.db
Stores.withdrawalGroups, .mktx((x) => ({
withdrawalGroupId, withdrawalGroups: x.withdrawalGroups,
); planchets: x.planchets,
}))
.runReadOnly(async (tx) => {
const withdrawalGroup = await tx.withdrawalGroups.get(withdrawalGroupId);
if (!withdrawalGroup) { if (!withdrawalGroup) {
return; return;
} }
let planchet = await ws.db.getIndexed(Stores.planchets.byGroupAndIndex, [ let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
withdrawalGroupId, withdrawalGroupId,
coinIdx, coinIdx,
]); ]);
@ -534,6 +568,14 @@ async function processPlanchetVerifyAndStoreCoin(
logger.warn("processPlanchet: planchet already withdrawn"); logger.warn("processPlanchet: planchet already withdrawn");
return; return;
} }
return { planchet, exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl };
});
if (!d) {
return;
}
const { planchet, exchangeBaseUrl } = d;
const denomSig = await ws.cryptoApi.rsaUnblind( const denomSig = await ws.cryptoApi.rsaUnblind(
resp.ev_sig, resp.ev_sig,
@ -548,8 +590,10 @@ async function processPlanchetVerifyAndStoreCoin(
); );
if (!isValid) { if (!isValid) {
await ws.db.runWithWriteTransaction([Stores.planchets], async (tx) => { await ws.db
let planchet = await tx.getIndexed(Stores.planchets.byGroupAndIndex, [ .mktx((x) => ({ planchets: x.planchets }))
.runReadWrite(async (tx) => {
let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
withdrawalGroupId, withdrawalGroupId,
coinIdx, coinIdx,
]); ]);
@ -561,7 +605,7 @@ async function processPlanchetVerifyAndStoreCoin(
"invalid signature from the exchange after unblinding", "invalid signature from the exchange after unblinding",
{}, {},
); );
await tx.put(Stores.planchets, planchet); await tx.planchets.put(planchet);
}); });
return; return;
} }
@ -575,7 +619,7 @@ async function processPlanchetVerifyAndStoreCoin(
denomPubHash: planchet.denomPubHash, denomPubHash: planchet.denomPubHash,
denomSig, denomSig,
coinEvHash: planchet.coinEvHash, coinEvHash: planchet.coinEvHash,
exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl, exchangeBaseUrl: exchangeBaseUrl,
status: CoinStatus.Fresh, status: CoinStatus.Fresh,
coinSource: { coinSource: {
type: CoinSourceType.Withdraw, type: CoinSourceType.Withdraw,
@ -588,23 +632,27 @@ async function processPlanchetVerifyAndStoreCoin(
const planchetCoinPub = planchet.coinPub; const planchetCoinPub = planchet.coinPub;
const firstSuccess = await ws.db.runWithWriteTransaction( const firstSuccess = await ws.db
[Stores.coins, Stores.withdrawalGroups, Stores.reserves, Stores.planchets], .mktx((x) => ({
async (tx) => { coins: x.coins,
const ws = await tx.get(Stores.withdrawalGroups, withdrawalGroupId); withdrawalGroups: x.withdrawalGroups,
reserves: x.reserves,
planchets: x.planchets,
}))
.runReadWrite(async (tx) => {
const ws = await tx.withdrawalGroups.get(withdrawalGroupId);
if (!ws) { if (!ws) {
return false; return false;
} }
const p = await tx.get(Stores.planchets, planchetCoinPub); const p = await tx.planchets.get(planchetCoinPub);
if (!p || p.withdrawalDone) { if (!p || p.withdrawalDone) {
return false; return false;
} }
p.withdrawalDone = true; p.withdrawalDone = true;
await tx.put(Stores.planchets, p); await tx.planchets.put(p);
await tx.add(Stores.coins, coin); await tx.coins.add(coin);
return true; return true;
}, });
);
if (firstSuccess) { if (firstSuccess) {
ws.notify({ ws.notify({
@ -636,12 +684,14 @@ export async function updateWithdrawalDenoms(
ws: InternalWalletState, ws: InternalWalletState,
exchangeBaseUrl: string, exchangeBaseUrl: string,
): Promise<void> { ): Promise<void> {
const exchangeDetails = await ws.db.runWithReadTransaction( const exchangeDetails = await ws.db
[Stores.exchanges, Stores.exchangeDetails], .mktx((x) => ({
async (tx) => { exchanges: x.exchanges,
exchangeDetails: x.exchangeDetails,
}))
.runReadOnly(async (tx) => {
return getExchangeDetails(tx, exchangeBaseUrl); return getExchangeDetails(tx, exchangeBaseUrl);
}, });
);
if (!exchangeDetails) { if (!exchangeDetails) {
logger.error("exchange details not available"); logger.error("exchange details not available");
throw Error(`exchange ${exchangeBaseUrl} details not available`); throw Error(`exchange ${exchangeBaseUrl} details not available`);
@ -663,7 +713,11 @@ export async function updateWithdrawalDenoms(
} else { } else {
denom.status = DenominationStatus.VerifiedGood; denom.status = DenominationStatus.VerifiedGood;
} }
await ws.db.put(Stores.denominations, denom); await ws.db
.mktx((x) => ({ denominations: x.denominations }))
.runReadWrite(async (tx) => {
await tx.denominations.put(denom);
});
} }
} }
// FIXME: This debug info should either be made conditional on some flag // FIXME: This debug info should either be made conditional on some flag
@ -698,15 +752,17 @@ async function incrementWithdrawalRetry(
withdrawalGroupId: string, withdrawalGroupId: string,
err: TalerErrorDetails | undefined, err: TalerErrorDetails | undefined,
): Promise<void> { ): Promise<void> {
await ws.db.runWithWriteTransaction([Stores.withdrawalGroups], async (tx) => { await ws.db
const wsr = await tx.get(Stores.withdrawalGroups, withdrawalGroupId); .mktx((x) => ({ withdrawalGroups: x.withdrawalGroups }))
.runReadWrite(async (tx) => {
const wsr = await tx.withdrawalGroups.get(withdrawalGroupId);
if (!wsr) { if (!wsr) {
return; return;
} }
wsr.retryInfo.retryCounter++; wsr.retryInfo.retryCounter++;
updateRetryInfoTimeout(wsr.retryInfo); updateRetryInfoTimeout(wsr.retryInfo);
wsr.lastError = err; wsr.lastError = err;
await tx.put(Stores.withdrawalGroups, wsr); await tx.withdrawalGroups.put(wsr);
}); });
if (err) { if (err) {
ws.notify({ type: NotificationType.WithdrawOperationError, error: err }); ws.notify({ type: NotificationType.WithdrawOperationError, error: err });
@ -730,11 +786,14 @@ async function resetWithdrawalGroupRetry(
ws: InternalWalletState, ws: InternalWalletState,
withdrawalGroupId: string, withdrawalGroupId: string,
): Promise<void> { ): Promise<void> {
await ws.db.mutate(Stores.withdrawalGroups, withdrawalGroupId, (x) => { await ws.db
if (x.retryInfo.active) { .mktx((x) => ({ withdrawalGroups: x.withdrawalGroups }))
.runReadWrite(async (tx) => {
const x = await tx.withdrawalGroups.get(withdrawalGroupId);
if (x && x.retryInfo.active) {
x.retryInfo = initRetryInfo(); x.retryInfo = initRetryInfo();
await tx.withdrawalGroups.put(x);
} }
return x;
}); });
} }
@ -747,10 +806,11 @@ async function processWithdrawGroupImpl(
if (forceNow) { if (forceNow) {
await resetWithdrawalGroupRetry(ws, withdrawalGroupId); await resetWithdrawalGroupRetry(ws, withdrawalGroupId);
} }
const withdrawalGroup = await ws.db.get( const withdrawalGroup = await ws.db
Stores.withdrawalGroups, .mktx((x) => ({ withdrawalGroups: x.withdrawalGroups }))
withdrawalGroupId, .runReadOnly(async (tx) => {
); return tx.withdrawalGroups.get(withdrawalGroupId);
});
if (!withdrawalGroup) { if (!withdrawalGroup) {
logger.trace("withdraw session doesn't exist"); logger.trace("withdraw session doesn't exist");
return; return;
@ -793,16 +853,21 @@ async function processWithdrawGroupImpl(
let finishedForFirstTime = false; let finishedForFirstTime = false;
let errorsPerCoin: Record<number, TalerErrorDetails> = {}; let errorsPerCoin: Record<number, TalerErrorDetails> = {};
await ws.db.runWithWriteTransaction( await ws.db
[Stores.coins, Stores.withdrawalGroups, Stores.reserves, Stores.planchets], .mktx((x) => ({
async (tx) => { coins: x.coins,
const wg = await tx.get(Stores.withdrawalGroups, withdrawalGroupId); withdrawalGroups: x.withdrawalGroups,
reserves: x.reserves,
planchets: x.planchets,
}))
.runReadWrite(async (tx) => {
const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
if (!wg) { if (!wg) {
return; return;
} }
await tx await tx.planchets.indexes.byGroup
.iterIndexed(Stores.planchets.byGroup, withdrawalGroupId) .iter(withdrawalGroupId)
.forEach((x) => { .forEach((x) => {
if (x.withdrawalDone) { if (x.withdrawalDone) {
numFinished++; numFinished++;
@ -819,9 +884,8 @@ async function processWithdrawGroupImpl(
wg.retryInfo = initRetryInfo(false); wg.retryInfo = initRetryInfo(false);
} }
await tx.put(Stores.withdrawalGroups, wg); await tx.withdrawalGroups.put(wg);
}, });
);
if (numFinished != numTotalCoins) { if (numFinished != numTotalCoins) {
throw OperationFailedError.fromCode( throw OperationFailedError.fromCode(
@ -871,8 +935,12 @@ export async function getExchangeWithdrawalInfo(
} }
const possibleDenoms = await ws.db const possibleDenoms = await ws.db
.iterIndex(Stores.denominations.exchangeBaseUrlIndex, baseUrl) .mktx((x) => ({ denominations: x.denominations }))
.runReadOnly(async (tx) => {
return tx.denominations.indexes.byExchangeBaseUrl
.iter()
.filter((d) => d.isOffered); .filter((d) => d.isOffered);
});
let versionMatch; let versionMatch;
if (exchangeDetails.protocolVersion) { if (exchangeDetails.protocolVersion) {
@ -953,15 +1021,15 @@ export async function getWithdrawalDetailsForUri(
const exchanges: ExchangeListItem[] = []; const exchanges: ExchangeListItem[] = [];
const exchangeRecords = await ws.db.iter(Stores.exchanges).toArray(); await ws.db
.mktx((x) => ({
exchanges: x.exchanges,
exchangeDetails: x.exchangeDetails,
}))
.runReadOnly(async (tx) => {
const exchangeRecords = await tx.exchanges.iter().toArray();
for (const r of exchangeRecords) { for (const r of exchangeRecords) {
const details = await ws.db.runWithReadTransaction( const details = await getExchangeDetails(tx, r.baseUrl);
[Stores.exchanges, Stores.exchangeDetails],
async (tx) => {
return getExchangeDetails(tx, r.baseUrl);
},
);
if (details) { if (details) {
exchanges.push({ exchanges.push({
exchangeBaseUrl: details.exchangeBaseUrl, exchangeBaseUrl: details.exchangeBaseUrl,
@ -970,6 +1038,7 @@ export async function getWithdrawalDetailsForUri(
}); });
} }
} }
});
return { return {
amount: Amounts.stringify(info.amount), amount: Amounts.stringify(info.amount),

View File

@ -33,6 +33,7 @@ import {
IDBVersionChangeEvent, IDBVersionChangeEvent,
Event, Event,
IDBCursor, IDBCursor,
IDBKeyPath,
} from "@gnu-taler/idb-bridge"; } from "@gnu-taler/idb-bridge";
import { Logger } from "@gnu-taler/taler-util"; import { Logger } from "@gnu-taler/taler-util";
@ -43,25 +44,6 @@ const logger = new Logger("query.ts");
*/ */
export const TransactionAbort = Symbol("transaction_abort"); export const TransactionAbort = Symbol("transaction_abort");
export interface StoreParams<T> {
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<N extends string, T> {
constructor(public name: N, public storeParams?: StoreParams<T>) {}
}
/** /**
* Options for an index. * Options for an index.
*/ */
@ -111,37 +93,6 @@ function transactionToPromise(tx: IDBTransaction): Promise<void> {
}); });
} }
function applyMutation<T>(
req: IDBRequest,
f: (x: T) => T | undefined,
): Promise<void> {
return new Promise((resolve, reject) => {
req.onsuccess = () => {
const cursor = req.result;
if (cursor) {
const val = cursor.value;
const modVal = f(val);
if (modVal !== undefined && modVal !== null) {
const req2: IDBRequest = cursor.update(modVal);
req2.onerror = () => {
reject(req2.error);
};
req2.onsuccess = () => {
cursor.continue();
};
} else {
cursor.continue();
}
} else {
resolve();
}
};
req.onerror = () => {
reject(req.error);
};
});
}
type CursorResult<T> = CursorEmptyResult<T> | CursorValueResult<T>; type CursorResult<T> = CursorEmptyResult<T> | CursorValueResult<T>;
interface CursorEmptyResult<T> { interface CursorEmptyResult<T> {
@ -269,210 +220,6 @@ class ResultStream<T> {
} }
} }
export type AnyStoreMap = { [s: string]: Store<any, any> };
type StoreName<S> = S extends Store<infer N, any> ? N : never;
type StoreContent<S> = S extends Store<any, infer R> ? R : never;
type IndexRecord<Ind> = Ind extends Index<any, any, any, infer R> ? R : never;
type InferStore<S> = S extends Store<infer N, infer R> ? Store<N, R> : never;
type InferIndex<Ind> = Ind extends Index<
infer StN,
infer IndN,
infer KT,
infer RT
>
? Index<StN, IndN, KT, RT>
: never;
export class TransactionHandle<StoreTypes extends Store<string, any>> {
constructor(private tx: IDBTransaction) {}
put<S extends StoreTypes>(
store: S,
value: StoreContent<S>,
key?: any,
): Promise<any> {
const req = this.tx.objectStore(store.name).put(value, key);
return requestToPromise(req);
}
add<S extends StoreTypes>(
store: S,
value: StoreContent<S>,
key?: any,
): Promise<any> {
const req = this.tx.objectStore(store.name).add(value, key);
return requestToPromise(req);
}
get<S extends StoreTypes>(
store: S,
key: any,
): Promise<StoreContent<S> | undefined> {
const req = this.tx.objectStore(store.name).get(key);
return requestToPromise(req);
}
getIndexed<
St extends StoreTypes,
Ind extends Index<StoreName<St>, string, any, any>
>(index: InferIndex<Ind>, key: any): Promise<IndexRecord<Ind> | undefined> {
const req = this.tx
.objectStore(index.storeName)
.index(index.indexName)
.get(key);
return requestToPromise(req);
}
iter<St extends InferStore<StoreTypes>>(
store: St,
key?: any,
): ResultStream<StoreContent<St>> {
const req = this.tx.objectStore(store.name).openCursor(key);
return new ResultStream<StoreContent<St>>(req);
}
iterIndexed<
St extends InferStore<StoreTypes>,
Ind extends InferIndex<Index<StoreName<St>, string, any, any>>
>(index: Ind, key?: any): ResultStream<IndexRecord<Ind>> {
const req = this.tx
.objectStore(index.storeName)
.index(index.indexName)
.openCursor(key);
return new ResultStream<IndexRecord<Ind>>(req);
}
delete<St extends StoreTypes>(
store: InferStore<St>,
key: any,
): Promise<void> {
const req = this.tx.objectStore(store.name).delete(key);
return requestToPromise(req);
}
mutate<St extends StoreTypes>(
store: InferStore<St>,
key: any,
f: (x: StoreContent<St>) => StoreContent<St> | undefined,
): Promise<void> {
const req = this.tx.objectStore(store.name).openCursor(key);
return applyMutation(req, f);
}
}
function runWithTransaction<T, StoreTypes extends Store<string, {}>>(
db: IDBDatabase,
stores: StoreTypes[],
f: (t: TransactionHandle<StoreTypes>) => Promise<T>,
mode: "readonly" | "readwrite",
): Promise<T> {
const stack = Error("Failed transaction was started here.");
return new Promise((resolve, reject) => {
const storeName = stores.map((x) => x.name);
let txOrUndef: IDBTransaction | undefined = undefined
try {
txOrUndef = db.transaction(storeName, mode);
} catch (e) {
logger.error("error opening transaction");
logger.error(`${e}`);
return
}
const tx = txOrUndef;
let funResult: any = undefined;
let gotFunResult = false;
tx.oncomplete = () => {
// This is a fatal error: The transaction completed *before*
// the transaction function returned. Likely, the transaction
// function waited on a promise that is *not* resolved in the
// microtask queue, thus triggering the auto-commit behavior.
// Unfortunately, the auto-commit behavior of IDB can't be switched
// of. There are some proposals to add this functionality in the future.
if (!gotFunResult) {
const msg =
"BUG: transaction closed before transaction function returned";
console.error(msg);
reject(Error(msg));
}
resolve(funResult);
};
tx.onerror = () => {
logger.error("error in transaction");
logger.error(`${stack}`);
};
tx.onabort = () => {
if (tx.error) {
logger.error("Transaction aborted with error:", tx.error);
} else {
logger.error("Transaction aborted (no error)");
}
reject(TransactionAbort);
};
const th = new TransactionHandle(tx);
const resP = Promise.resolve().then(() => f(th));
resP
.then((result) => {
gotFunResult = true;
funResult = result;
})
.catch((e) => {
if (e == TransactionAbort) {
logger.trace("aborting transaction");
} else {
console.error("Transaction failed:", e);
console.error(stack);
tx.abort();
}
})
.catch((e) => {
console.error("fatal: aborting transaction failed", e);
});
});
}
/**
* Definition of an index.
*/
export class Index<
StoreName extends string,
IndexName extends string,
S extends IDBValidKey,
T
> {
/**
* Name of the store that this index is associated with.
*/
storeName: string;
/**
* Options to use for the index.
*/
options: IndexOptions;
constructor(
s: Store<StoreName, T>,
public indexName: IndexName,
public keyPath: string | string[],
options?: IndexOptions,
) {
const defaultOptions = {
multiEntry: false,
};
this.options = { ...defaultOptions, ...(options || {}) };
this.storeName = s.name;
}
/**
* We want to have the key type parameter in use somewhere,
* because otherwise the compiler complains. In iterIndex the
* key type is pretty useful.
*/
protected _dummyKey: S | undefined;
}
/** /**
* Return a promise that resolves to the opened IndexedDB database. * Return a promise that resolves to the opened IndexedDB database.
*/ */
@ -519,152 +266,334 @@ export function openDatabase(
}); });
} }
export class Database<StoreMap extends AnyStoreMap> { export interface IndexDescriptor {
constructor(private db: IDBDatabase, stores: StoreMap) {} name: string;
keyPath: IDBKeyPath | IDBKeyPath[];
multiEntry?: boolean;
}
static deleteDatabase(idbFactory: IDBFactory, dbName: string): Promise<void> { export interface StoreDescriptor<RecordType> {
const req = idbFactory.deleteDatabase(dbName) _dummy: undefined & RecordType;
return requestToPromise(req) name: string;
} keyPath?: IDBKeyPath | IDBKeyPath[];
autoIncrement?: boolean;
}
async exportDatabase(): Promise<any> { export interface StoreOptions {
const db = this.db; keyPath?: IDBKeyPath | IDBKeyPath[];
const dump = { autoIncrement?: boolean;
name: db.name, }
stores: {} as { [s: string]: any },
version: db.version, export function describeContents<RecordType = never>(
name: string,
options: StoreOptions,
): StoreDescriptor<RecordType> {
return { name, keyPath: options.keyPath, _dummy: undefined as any };
}
export function describeIndex(
name: string,
keyPath: IDBKeyPath | IDBKeyPath[],
options: IndexOptions = {},
): IndexDescriptor {
return {
keyPath,
name,
multiEntry: options.multiEntry,
}; };
}
interface IndexReadOnlyAccessor<RecordType> {
iter(query?: IDBValidKey): ResultStream<RecordType>;
get(query: IDBValidKey): Promise<RecordType | undefined>;
}
type GetIndexReadOnlyAccess<RecordType, IndexMap> = {
[P in keyof IndexMap]: IndexReadOnlyAccessor<RecordType>;
};
interface IndexReadWriteAccessor<RecordType> {
iter(query: IDBValidKey): ResultStream<RecordType>;
get(query: IDBValidKey): Promise<RecordType | undefined>;
}
type GetIndexReadWriteAccess<RecordType, IndexMap> = {
[P in keyof IndexMap]: IndexReadWriteAccessor<RecordType>;
};
export interface StoreReadOnlyAccessor<RecordType, IndexMap> {
get(key: IDBValidKey): Promise<RecordType | undefined>;
iter(query?: IDBValidKey): ResultStream<RecordType>;
indexes: GetIndexReadOnlyAccess<RecordType, IndexMap>;
}
export interface StoreReadWriteAccessor<RecordType, IndexMap> {
get(key: IDBValidKey): Promise<RecordType | undefined>;
iter(query?: IDBValidKey): ResultStream<RecordType>;
put(r: RecordType): Promise<void>;
add(r: RecordType): Promise<void>;
delete(key: IDBValidKey): Promise<void>;
indexes: GetIndexReadWriteAccess<RecordType, IndexMap>;
}
export interface StoreWithIndexes<
SD extends StoreDescriptor<unknown>,
IndexMap
> {
store: SD;
indexMap: IndexMap;
/**
* Type marker symbol, to check that the descriptor
* has been created through the right function.
*/
mark: Symbol;
}
export type GetRecordType<T> = T extends StoreDescriptor<infer X> ? X : unknown;
const storeWithIndexesSymbol = Symbol("StoreWithIndexesMark");
export function describeStore<SD extends StoreDescriptor<unknown>, IndexMap>(
s: SD,
m: IndexMap,
): StoreWithIndexes<SD, IndexMap> {
return {
store: s,
indexMap: m,
mark: storeWithIndexesSymbol,
};
}
export type GetReadOnlyAccess<BoundStores> = {
[P in keyof BoundStores]: BoundStores[P] extends StoreWithIndexes<
infer SD,
infer IM
>
? StoreReadOnlyAccessor<GetRecordType<SD>, IM>
: unknown;
};
export type GetReadWriteAccess<BoundStores> = {
[P in keyof BoundStores]: BoundStores[P] extends StoreWithIndexes<
infer SD,
infer IM
>
? StoreReadWriteAccessor<GetRecordType<SD>, IM>
: unknown;
};
type ReadOnlyTransactionFunction<BoundStores, T> = (
t: GetReadOnlyAccess<BoundStores>,
) => Promise<T>;
type ReadWriteTransactionFunction<BoundStores, T> = (
t: GetReadWriteAccess<BoundStores>,
) => Promise<T>;
export interface TransactionContext<BoundStores> {
runReadWrite<T>(f: ReadWriteTransactionFunction<BoundStores, T>): Promise<T>;
runReadOnly<T>(f: ReadOnlyTransactionFunction<BoundStores, T>): Promise<T>;
}
type CheckDescriptor<T> = T extends StoreWithIndexes<infer SD, infer IM>
? StoreWithIndexes<SD, IM>
: unknown;
type GetPickerType<F, SM> = F extends (x: SM) => infer Out
? { [P in keyof Out]: CheckDescriptor<Out[P]> }
: unknown;
function runTx<Arg, Res>(
tx: IDBTransaction,
arg: Arg,
f: (t: Arg) => Promise<Res>,
): Promise<Res> {
const stack = Error("Failed transaction was started here.");
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const tx = db.transaction(Array.from(db.objectStoreNames)); let funResult: any = undefined;
tx.addEventListener("complete", () => { let gotFunResult = false;
resolve(dump); tx.oncomplete = () => {
}); // This is a fatal error: The transaction completed *before*
// tslint:disable-next-line:prefer-for-of // the transaction function returned. Likely, the transaction
for (let i = 0; i < db.objectStoreNames.length; i++) { // function waited on a promise that is *not* resolved in the
const name = db.objectStoreNames[i]; // microtask queue, thus triggering the auto-commit behavior.
const storeDump = {} as { [s: string]: any }; // Unfortunately, the auto-commit behavior of IDB can't be switched
dump.stores[name] = storeDump; // of. There are some proposals to add this functionality in the future.
tx.objectStore(name) if (!gotFunResult) {
.openCursor() const msg =
.addEventListener("success", (e: Event) => { "BUG: transaction closed before transaction function returned";
const cursor = (e.target as any).result; console.error(msg);
if (cursor) { reject(Error(msg));
storeDump[cursor.key] = cursor.value;
cursor.continue();
} }
}); resolve(funResult);
};
tx.onerror = () => {
logger.error("error in transaction");
logger.error(`${stack}`);
};
tx.onabort = () => {
if (tx.error) {
logger.error("Transaction aborted with error:", tx.error);
} else {
logger.error("Transaction aborted (no error)");
} }
}); reject(TransactionAbort);
};
const resP = Promise.resolve().then(() => f(arg));
resP
.then((result) => {
gotFunResult = true;
funResult = result;
})
.catch((e) => {
if (e == TransactionAbort) {
logger.trace("aborting transaction");
} else {
console.error("Transaction failed:", e);
console.error(stack);
tx.abort();
} }
})
importDatabase(dump: any): Promise<void> { .catch((e) => {
const db = this.db; console.error("fatal: aborting transaction failed", e);
logger.info("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]);
}
logger.info(`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<N extends keyof StoreMap, S extends StoreMap[N]>( function makeReadContext(
store: S, tx: IDBTransaction,
key: IDBValidKey, storePick: { [n: string]: StoreWithIndexes<any, any> },
): Promise<StoreContent<S> | undefined> { ): any {
const tx = this.db.transaction([store.name], "readonly"); const ctx: { [s: string]: StoreReadOnlyAccessor<any, any> } = {};
const req = tx.objectStore(store.name).get(key); for (const storeAlias in storePick) {
const v = await requestToPromise(req); const indexes: { [s: string]: IndexReadOnlyAccessor<any> } = {};
await transactionToPromise(tx); const swi = storePick[storeAlias];
return v; const storeName = swi.store.name;
} for (const indexName in storePick[storeAlias].indexMap) {
indexes[indexName] = {
async getIndexed<Ind extends Index<string, string, any, any>>( get(key) {
index: Ind, const req = tx.objectStore(storeName).index(indexName).get(key);
key: IDBValidKey, return requestToPromise(req);
): Promise<IndexRecord<Ind> | undefined> { },
const tx = this.db.transaction([index.storeName], "readonly"); iter(query) {
const req = tx.objectStore(index.storeName).index(index.indexName).get(key);
const v = await requestToPromise(req);
await transactionToPromise(tx);
return v;
}
async put<St extends Store<string, any>>(
store: St,
value: StoreContent<St>,
key?: IDBValidKey,
): 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<N extends string, T>(
store: Store<N, T>,
key: IDBValidKey,
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<N extends string, T>(store: Store<N, T>): ResultStream<T> {
const tx = this.db.transaction([store.name], "readonly");
const req = tx.objectStore(store.name).openCursor();
return new ResultStream<T>(req);
}
iterIndex<Ind extends Index<string, string, any, any>>(
index: InferIndex<Ind>,
query?: any,
): ResultStream<IndexRecord<Ind>> {
const tx = this.db.transaction([index.storeName], "readonly");
const req = tx const req = tx
.objectStore(index.storeName) .objectStore(storeName)
.index(index.indexName) .index(indexName)
.openCursor(query); .openCursor(query);
return new ResultStream<IndexRecord<Ind>>(req); return new ResultStream<any>(req);
},
};
} }
ctx[storeAlias] = {
async runWithReadTransaction< indexes,
T, get(key) {
N extends keyof StoreMap, const req = tx.objectStore(storeName).get(key);
StoreTypes extends StoreMap[N] return requestToPromise(req);
>( },
stores: StoreTypes[], iter(query) {
f: (t: TransactionHandle<StoreTypes>) => Promise<T>, const req = tx.objectStore(storeName).openCursor(query);
): Promise<T> { return new ResultStream<any>(req);
return runWithTransaction<T, StoreTypes>(this.db, stores, f, "readonly"); },
};
} }
return ctx;
}
async runWithWriteTransaction< function makeWriteContext(
T, tx: IDBTransaction,
N extends keyof StoreMap, storePick: { [n: string]: StoreWithIndexes<any, any> },
StoreTypes extends StoreMap[N] ): any {
>( const ctx: { [s: string]: StoreReadWriteAccessor<any, any> } = {};
stores: StoreTypes[], for (const storeAlias in storePick) {
f: (t: TransactionHandle<StoreTypes>) => Promise<T>, const indexes: { [s: string]: IndexReadWriteAccessor<any> } = {};
): Promise<T> { const swi = storePick[storeAlias];
return runWithTransaction<T, StoreTypes>(this.db, stores, f, "readwrite"); const storeName = swi.store.name;
for (const indexName in storePick[storeAlias].indexMap) {
indexes[indexName] = {
get(key) {
const req = tx.objectStore(storeName).index(indexName).get(key);
return requestToPromise(req);
},
iter(query) {
const req = tx
.objectStore(storeName)
.index(indexName)
.openCursor(query);
return new ResultStream<any>(req);
},
};
}
ctx[storeAlias] = {
indexes,
get(key) {
const req = tx.objectStore(storeName).get(key);
return requestToPromise(req);
},
iter(query) {
const req = tx.objectStore(storeName).openCursor(query);
return new ResultStream<any>(req);
},
add(r) {
const req = tx.objectStore(storeName).add(r);
return requestToPromise(req);
},
put(r) {
const req = tx.objectStore(storeName).put(r);
return requestToPromise(req);
},
delete(k) {
const req = tx.objectStore(storeName).delete(k);
return requestToPromise(req);
},
};
}
}
/**
* Type-safe access to a database with a particular store map.
*
* A store map is the metadata that describes the store.
*/
export class DbAccess<StoreMap> {
constructor(private db: IDBDatabase, private stores: StoreMap) {}
mktx<
PickerType extends (x: StoreMap) => unknown,
BoundStores extends GetPickerType<PickerType, StoreMap>
>(f: PickerType): TransactionContext<BoundStores> {
const storePick = f(this.stores) as any;
if (typeof storePick !== "object" || storePick === null) {
throw Error();
}
const storeNames: string[] = [];
for (const storeAlias of Object.keys(storePick)) {
const swi = (storePick as any)[storeAlias] as StoreWithIndexes<any, any>;
if (swi.mark !== storeWithIndexesSymbol) {
throw Error("invalid store descriptor returned from selector function");
}
storeNames.push(swi.store.name);
}
const runReadOnly = <T>(
txf: ReadOnlyTransactionFunction<BoundStores, T>,
): Promise<T> => {
const tx = this.db.transaction(storeNames, "readonly");
const readContext = makeReadContext(tx, storePick);
return runTx(tx, readContext, txf);
};
const runReadWrite = <T>(
txf: ReadWriteTransactionFunction<BoundStores, T>,
): Promise<T> => {
const tx = this.db.transaction(storeNames, "readwrite");
const writeContext = makeWriteContext(tx, storePick);
return runTx(tx, writeContext, txf);
};
return {
runReadOnly,
runReadWrite,
};
} }
} }

View File

@ -58,6 +58,7 @@ import {
} from "./operations/errors"; } from "./operations/errors";
import { import {
acceptExchangeTermsOfService, acceptExchangeTermsOfService,
getExchangeDetails,
getExchangePaytoUri, getExchangePaytoUri,
updateExchangeFromUrl, updateExchangeFromUrl,
} from "./operations/exchanges"; } from "./operations/exchanges";
@ -111,7 +112,7 @@ import {
RefundState, RefundState,
ReserveRecord, ReserveRecord,
ReserveRecordStatus, ReserveRecordStatus,
Stores, WalletStoresV1,
} from "./db.js"; } from "./db.js";
import { NotificationType, WalletNotification } from "@gnu-taler/taler-util"; import { NotificationType, WalletNotification } from "@gnu-taler/taler-util";
import { import {
@ -179,10 +180,10 @@ import { AsyncOpMemoSingle } from "./util/asyncMemo";
import { HttpRequestLibrary } from "./util/http"; import { HttpRequestLibrary } from "./util/http";
import { Logger } from "@gnu-taler/taler-util"; import { Logger } from "@gnu-taler/taler-util";
import { AsyncCondition } from "./util/promiseUtils"; import { AsyncCondition } from "./util/promiseUtils";
import { Database } from "./util/query";
import { Duration, durationMin } from "@gnu-taler/taler-util"; import { Duration, durationMin } from "@gnu-taler/taler-util";
import { TimerGroup } from "./util/timer"; import { TimerGroup } from "./util/timer";
import { getExchangeTrust } from "./operations/currencies.js"; import { getExchangeTrust } from "./operations/currencies.js";
import { DbAccess } from "./util/query.js";
const builtinAuditors: AuditorTrustRecord[] = [ const builtinAuditors: AuditorTrustRecord[] = [
{ {
@ -205,12 +206,12 @@ export class Wallet {
private stopped = false; private stopped = false;
private memoRunRetryLoop = new AsyncOpMemoSingle<void>(); private memoRunRetryLoop = new AsyncOpMemoSingle<void>();
get db(): Database<typeof Stores> { get db(): DbAccess<typeof WalletStoresV1> {
return this.ws.db; return this.ws.db;
} }
constructor( constructor(
db: Database<typeof Stores>, db: DbAccess<typeof WalletStoresV1>,
http: HttpRequestLibrary, http: HttpRequestLibrary,
cryptoWorkerFactory: CryptoWorkerFactory, cryptoWorkerFactory: CryptoWorkerFactory,
) { ) {
@ -481,22 +482,21 @@ export class Wallet {
* already been applied. * already been applied.
*/ */
async fillDefaults(): Promise<void> { async fillDefaults(): Promise<void> {
await this.db.runWithWriteTransaction( await this.db
[Stores.config, Stores.auditorTrustStore], .mktx((x) => ({ config: x.config, auditorTrustStore: x.auditorTrust }))
async (tx) => { .runReadWrite(async (tx) => {
let applied = false; let applied = false;
await tx.iter(Stores.config).forEach((x) => { await tx.config.iter().forEach((x) => {
if (x.key == "currencyDefaultsApplied" && x.value == true) { if (x.key == "currencyDefaultsApplied" && x.value == true) {
applied = true; applied = true;
} }
}); });
if (!applied) { if (!applied) {
for (const c of builtinAuditors) { for (const c of builtinAuditors) {
await tx.put(Stores.auditorTrustStore, c); await tx.auditorTrustStore.put(c);
} }
} }
}, });
);
} }
/** /**
@ -553,10 +553,13 @@ export class Wallet {
amount, amount,
exchange: exchangeBaseUrl, exchange: exchangeBaseUrl,
}); });
const exchangePaytoUris = await this.db.runWithReadTransaction( const exchangePaytoUris = await this.db
[Stores.exchanges, Stores.reserves], .mktx((x) => ({
(tx) => getFundingPaytoUris(tx, resp.reservePub), exchanges: x.exchanges,
); exchangeDetails: x.exchangeDetails,
reserves: x.reserves,
}))
.runReadWrite((tx) => getFundingPaytoUris(tx, resp.reservePub));
return { return {
reservePub: resp.reservePub, reservePub: resp.reservePub,
exchangePaytoUris, exchangePaytoUris,
@ -627,29 +630,26 @@ export class Wallet {
async refresh(oldCoinPub: string): Promise<void> { async refresh(oldCoinPub: string): Promise<void> {
try { try {
const refreshGroupId = await this.db.runWithWriteTransaction( const refreshGroupId = await this.db
[Stores.refreshGroups, Stores.denominations, Stores.coins], .mktx((x) => ({
async (tx) => { refreshGroups: x.refreshGroups,
denominations: x.denominations,
coins: x.coins,
}))
.runReadWrite(async (tx) => {
return await createRefreshGroup( return await createRefreshGroup(
this.ws, this.ws,
tx, tx,
[{ coinPub: oldCoinPub }], [{ coinPub: oldCoinPub }],
RefreshReason.Manual, RefreshReason.Manual,
); );
}, });
);
await processRefreshGroup(this.ws, refreshGroupId.refreshGroupId); await processRefreshGroup(this.ws, refreshGroupId.refreshGroupId);
} catch (e) { } catch (e) {
this.latch.trigger(); this.latch.trigger();
} }
} }
async findExchange(
exchangeBaseUrl: string,
): Promise<ExchangeRecord | undefined> {
return await this.db.get(Stores.exchanges, exchangeBaseUrl);
}
async getPendingOperations({ async getPendingOperations({
onlyDue = false, onlyDue = false,
} = {}): Promise<PendingOperationsResponse> { } = {}): Promise<PendingOperationsResponse> {
@ -665,55 +665,46 @@ export class Wallet {
return acceptExchangeTermsOfService(this.ws, exchangeBaseUrl, etag); return acceptExchangeTermsOfService(this.ws, exchangeBaseUrl, etag);
} }
async getDenoms(exchangeUrl: string): Promise<DenominationRecord[]> {
const denoms = await this.db
.iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchangeUrl)
.toArray();
return denoms;
}
/**
* Get all exchanges known to the exchange.
*
* @deprecated Use getExchanges instead
*/
async getExchangeRecords(): Promise<ExchangeRecord[]> {
return await this.db.iter(Stores.exchanges).toArray();
}
async getExchanges(): Promise<ExchangesListRespose> { async getExchanges(): Promise<ExchangesListRespose> {
const exchangeRecords = await this.db.iter(Stores.exchanges).toArray();
const exchanges: ExchangeListItem[] = []; const exchanges: ExchangeListItem[] = [];
await this.db
.mktx((x) => ({
exchanges: x.exchanges,
exchangeDetails: x.exchangeDetails,
}))
.runReadOnly(async (tx) => {
const exchangeRecords = await tx.exchanges.iter().toArray();
for (const r of exchangeRecords) { for (const r of exchangeRecords) {
const dp = r.detailsPointer; const dp = r.detailsPointer;
if (!dp) { if (!dp) {
continue; continue;
} }
const { currency, masterPublicKey } = dp; const { currency, masterPublicKey } = dp;
const exchangeDetails = await this.db.get(Stores.exchangeDetails, [ const exchangeDetails = await getExchangeDetails(tx, r.baseUrl);
r.baseUrl,
currency,
masterPublicKey,
]);
if (!exchangeDetails) { if (!exchangeDetails) {
continue; continue;
} }
exchanges.push({ exchanges.push({
exchangeBaseUrl: r.baseUrl, exchangeBaseUrl: r.baseUrl,
currency, currency,
paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri), paytoUris: exchangeDetails.wireInfo.accounts.map(
(x) => x.payto_uri,
),
}); });
} }
});
return { exchanges }; return { exchanges };
} }
async getCurrencies(): Promise<WalletCurrencyInfo> { async getCurrencies(): Promise<WalletCurrencyInfo> {
const trustedAuditors = await this.db return await this.ws.db
.iter(Stores.auditorTrustStore) .mktx((x) => ({
.toArray(); auditorTrust: x.auditorTrust,
const trustedExchanges = await this.db exchangeTrust: x.exchangeTrust,
.iter(Stores.exchangeTrustStore) }))
.toArray(); .runReadOnly(async (tx) => {
const trustedAuditors = await tx.auditorTrust.iter().toArray();
const trustedExchanges = await tx.exchangeTrust.iter().toArray();
return { return {
trustedAuditors: trustedAuditors.map((x) => ({ trustedAuditors: trustedAuditors.map((x) => ({
currency: x.currency, currency: x.currency,
@ -726,26 +717,7 @@ export class Wallet {
exchangeMasterPub: x.exchangeMasterPub, exchangeMasterPub: x.exchangeMasterPub,
})), })),
}; };
} });
async getReserves(exchangeBaseUrl?: string): Promise<ReserveRecord[]> {
if (exchangeBaseUrl) {
return await this.db
.iter(Stores.reserves)
.filter((r) => r.exchangeBaseUrl === exchangeBaseUrl);
} else {
return await this.db.iter(Stores.reserves).toArray();
}
}
async getCoinsForExchange(exchangeBaseUrl: string): Promise<CoinRecord[]> {
return await this.db
.iter(Stores.coins)
.filter((c) => c.exchangeBaseUrl === exchangeBaseUrl);
}
async getCoins(): Promise<CoinRecord[]> {
return await this.db.iter(Stores.coins).toArray();
} }
/** /**
@ -772,12 +744,6 @@ export class Wallet {
return applyRefund(this.ws, talerRefundUri); return applyRefund(this.ws, talerRefundUri);
} }
async getPurchase(
contractTermsHash: string,
): Promise<PurchaseRecord | undefined> {
return this.db.get(Stores.purchases, contractTermsHash);
}
async acceptTip(talerTipUri: string): Promise<void> { async acceptTip(talerTipUri: string): Promise<void> {
try { try {
return acceptTip(this.ws, talerTipUri); return acceptTip(this.ws, talerTipUri);
@ -799,7 +765,13 @@ export class Wallet {
* confirmation from the bank.). * confirmation from the bank.).
*/ */
public async handleNotifyReserve(): Promise<void> { public async handleNotifyReserve(): Promise<void> {
const reserves = await this.db.iter(Stores.reserves).toArray(); const reserves = await this.ws.db
.mktx((x) => ({
reserves: x.reserves,
}))
.runReadOnly(async (tx) => {
return tx.reserves.iter().toArray();
});
for (const r of reserves) { for (const r of reserves) {
if (r.reserveStatus === ReserveRecordStatus.WAIT_CONFIRM_BANK) { if (r.reserveStatus === ReserveRecordStatus.WAIT_CONFIRM_BANK) {
try { try {
@ -837,67 +809,27 @@ export class Wallet {
} }
} }
async updateReserve(reservePub: string): Promise<ReserveRecord | undefined> {
await forceQueryReserve(this.ws, reservePub);
return await this.ws.db.get(Stores.reserves, reservePub);
}
async getReserve(reservePub: string): Promise<ReserveRecord | undefined> {
return await this.ws.db.get(Stores.reserves, reservePub);
}
async refuseProposal(proposalId: string): Promise<void> { async refuseProposal(proposalId: string): Promise<void> {
return refuseProposal(this.ws, proposalId); return refuseProposal(this.ws, proposalId);
} }
async getPurchaseDetails(proposalId: string): Promise<PurchaseDetails> {
const purchase = await this.db.get(Stores.purchases, proposalId);
if (!purchase) {
throw Error("unknown purchase");
}
const refundsDoneAmounts = Object.values(purchase.refunds)
.filter((x) => x.type === RefundState.Applied)
.map((x) => x.refundAmount);
const refundsPendingAmounts = Object.values(purchase.refunds)
.filter((x) => x.type === RefundState.Pending)
.map((x) => x.refundAmount);
const totalRefundAmount = Amounts.sum([
...refundsDoneAmounts,
...refundsPendingAmounts,
]).amount;
const refundsDoneFees = Object.values(purchase.refunds)
.filter((x) => x.type === RefundState.Applied)
.map((x) => x.refundFee);
const refundsPendingFees = Object.values(purchase.refunds)
.filter((x) => x.type === RefundState.Pending)
.map((x) => x.refundFee);
const totalRefundFees = Amounts.sum([
...refundsDoneFees,
...refundsPendingFees,
]).amount;
const totalFees = totalRefundFees;
return {
contractTerms: JSON.parse(purchase.download.contractTermsRaw),
hasRefund: purchase.timestampLastRefundStatus !== undefined,
totalRefundAmount: totalRefundAmount,
totalRefundAndRefreshFees: totalFees,
};
}
benchmarkCrypto(repetitions: number): Promise<BenchmarkResult> { benchmarkCrypto(repetitions: number): Promise<BenchmarkResult> {
return this.ws.cryptoApi.benchmark(repetitions); return this.ws.cryptoApi.benchmark(repetitions);
} }
async setCoinSuspended(coinPub: string, suspended: boolean): Promise<void> { async setCoinSuspended(coinPub: string, suspended: boolean): Promise<void> {
await this.db.runWithWriteTransaction([Stores.coins], async (tx) => { await this.db
const c = await tx.get(Stores.coins, coinPub); .mktx((x) => ({
coins: x.coins,
}))
.runReadWrite(async (tx) => {
const c = await tx.coins.get(coinPub);
if (!c) { if (!c) {
logger.warn(`coin ${coinPub} not found, won't suspend`); logger.warn(`coin ${coinPub} not found, won't suspend`);
return; return;
} }
c.suspended = suspended; c.suspended = suspended;
await tx.put(Stores.coins, c); await tx.coins.put(c);
}); });
} }
@ -905,10 +837,17 @@ export class Wallet {
* Dump the public information of coins we have in an easy-to-process format. * Dump the public information of coins we have in an easy-to-process format.
*/ */
async dumpCoins(): Promise<CoinDumpJson> { async dumpCoins(): Promise<CoinDumpJson> {
const coins = await this.db.iter(Stores.coins).toArray();
const coinsJson: CoinDumpJson = { coins: [] }; const coinsJson: CoinDumpJson = { coins: [] };
await this.ws.db
.mktx((x) => ({
coins: x.coins,
denominations: x.denominations,
withdrawalGroups: x.withdrawalGroups,
}))
.runReadOnly(async (tx) => {
const coins = await tx.coins.iter().toArray();
for (const c of coins) { for (const c of coins) {
const denom = await this.db.get(Stores.denominations, [ const denom = await tx.denominations.get([
c.exchangeBaseUrl, c.exchangeBaseUrl,
c.denomPubHash, c.denomPubHash,
]); ]);
@ -923,10 +862,7 @@ export class Wallet {
} }
let withdrawalReservePub: string | undefined; let withdrawalReservePub: string | undefined;
if (cs.type == CoinSourceType.Withdraw) { if (cs.type == CoinSourceType.Withdraw) {
const ws = await this.db.get( const ws = await tx.withdrawalGroups.get(cs.withdrawalGroupId);
Stores.withdrawalGroups,
cs.withdrawalGroupId,
);
if (!ws) { if (!ws) {
console.error("no withdrawal session found for coin"); console.error("no withdrawal session found for coin");
continue; continue;
@ -945,6 +881,7 @@ export class Wallet {
coin_suspended: c.suspended, coin_suspended: c.suspended,
}); });
} }
});
return coinsJson; return coinsJson;
} }
@ -963,6 +900,55 @@ export class Wallet {
); );
} }
async updateReserve(reservePub: string): Promise<ReserveRecord | undefined> {
await forceQueryReserve(this.ws, reservePub);
return await this.ws.db
.mktx((x) => ({
reserves: x.reserves,
}))
.runReadOnly(async (tx) => {
return tx.reserves.get(reservePub);
});
}
async getCoins(): Promise<CoinRecord[]> {
return await this.db
.mktx((x) => ({
coins: x.coins,
}))
.runReadOnly(async (tx) => {
return tx.coins.iter().toArray();
});
}
async getReservesForExchange(
exchangeBaseUrl?: string,
): Promise<ReserveRecord[]> {
return await this.db
.mktx((x) => ({
reserves: x.reserves,
}))
.runReadOnly(async (tx) => {
if (exchangeBaseUrl) {
return await tx.reserves
.iter()
.filter((r) => r.exchangeBaseUrl === exchangeBaseUrl);
} else {
return await tx.reserves.iter().toArray();
}
});
}
async getReserve(reservePub: string): Promise<ReserveRecord | undefined> {
return await this.db
.mktx((x) => ({
reserves: x.reserves,
}))
.runReadOnly(async (tx) => {
return tx.reserves.get(reservePub);
});
}
async runIntegrationtest(args: IntegrationTestArgs): Promise<void> { async runIntegrationtest(args: IntegrationTestArgs): Promise<void> {
return runIntegrationTest(this.ws.http, this, args); return runIntegrationTest(this.ws.http, this, args);
} }
@ -1144,17 +1130,20 @@ export class Wallet {
case "forceRefresh": { case "forceRefresh": {
const req = codecForForceRefreshRequest().decode(payload); const req = codecForForceRefreshRequest().decode(payload);
const coinPubs = req.coinPubList.map((x) => ({ coinPub: x })); const coinPubs = req.coinPubList.map((x) => ({ coinPub: x }));
const refreshGroupId = await this.db.runWithWriteTransaction( const refreshGroupId = await this.db
[Stores.refreshGroups, Stores.denominations, Stores.coins], .mktx((x) => ({
async (tx) => { refreshGroups: x.refreshGroups,
denominations: x.denominations,
coins: x.coins,
}))
.runReadWrite(async (tx) => {
return await createRefreshGroup( return await createRefreshGroup(
this.ws, this.ws,
tx, tx,
coinPubs, coinPubs,
RefreshReason.Manual, RefreshReason.Manual,
); );
}, });
);
return { return {
refreshGroupId, refreshGroupId,
}; };

View File

@ -30,10 +30,10 @@ import {
OpenedPromise, OpenedPromise,
openPromise, openPromise,
openTalerDatabase, openTalerDatabase,
Database,
Stores,
makeErrorDetails, makeErrorDetails,
deleteTalerDatabase, deleteTalerDatabase,
DbAccess,
WalletStoresV1,
} from "@gnu-taler/taler-wallet-core"; } from "@gnu-taler/taler-wallet-core";
import { import {
classifyTalerUri, classifyTalerUri,
@ -52,7 +52,7 @@ import { BrowserCryptoWorkerFactory } from "./browserCryptoWorkerFactory";
*/ */
let currentWallet: Wallet | undefined; let currentWallet: Wallet | undefined;
let currentDatabase: Database<typeof Stores> | undefined; let currentDatabase: DbAccess<typeof WalletStoresV1> | undefined;
/** /**
* Last version if an outdated DB, if applicable. * Last version if an outdated DB, if applicable.