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) => {
await withWallet(args, async (wallet) => {
const reserves = await wallet.getReserves();
const reserves = await wallet.getReservesForExchange();
console.log(JSON.stringify(reserves, undefined, 2));
});
});

View File

@ -1,16 +1,19 @@
import {
openDatabase,
Database,
Store,
Index,
AnyStoreMap,
describeStore,
describeContents,
describeIndex,
DbAccess,
StoreDescriptor,
StoreWithIndexes,
IndexDescriptor,
} from "./util/query";
import {
IDBFactory,
IDBDatabase,
IDBObjectStore,
IDBTransaction,
IDBKeyPath,
IDBObjectStoreParameters,
} from "@gnu-taler/idb-bridge";
import { Logger } from "@gnu-taler/taler-util";
import {
@ -55,7 +58,7 @@ export const WALLET_DB_MINOR_VERSION = 1;
const logger = new Logger("db.ts");
function upgradeFromStoreMap(
storeMap: AnyStoreMap,
storeMap: any,
db: IDBDatabase,
oldVersion: number,
newVersion: number,
@ -63,15 +66,17 @@ function upgradeFromStoreMap(
): void {
if (oldVersion === 0) {
for (const n in storeMap) {
if ((storeMap as any)[n] instanceof Store) {
const si: Store<string, any> = (storeMap as any)[n];
const s = db.createObjectStore(si.name, si.storeParams);
for (const indexName in si as any) {
if ((si as any)[indexName] instanceof Index) {
const ii: Index<string, string, any, any> = (si as any)[indexName];
s.createIndex(ii.indexName, ii.keyPath, ii.options);
}
}
const swi: StoreWithIndexes<StoreDescriptor<unknown>, any> = storeMap[n];
const storeDesc: StoreDescriptor<unknown> = swi.store;
const s = db.createObjectStore(storeDesc.name, {
autoIncrement: storeDesc.autoIncrement,
keyPath: storeDesc.keyPath,
});
for (const indexName in swi.indexMap as any) {
const indexDesc: IndexDescriptor = swi.indexMap[indexName];
s.createIndex(indexDesc.name, indexDesc.keyPath, {
multiEntry: indexDesc.multiEntry,
});
}
}
return;
@ -80,30 +85,7 @@ function upgradeFromStoreMap(
return;
}
logger.info(`upgrading database from ${oldVersion} to ${newVersion}`);
for (const n in Stores) {
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);
}
}
}
}
}
throw Error("upgrade not supported");
}
function onTalerDbUpgradeNeeded(
@ -112,7 +94,13 @@ function onTalerDbUpgradeNeeded(
newVersion: number,
upgradeTransaction: IDBTransaction,
) {
upgradeFromStoreMap(Stores, db, oldVersion, newVersion, upgradeTransaction);
upgradeFromStoreMap(
WalletStoresV1,
db,
oldVersion,
newVersion,
upgradeTransaction,
);
}
function onMetaDbUpgradeNeeded(
@ -122,7 +110,7 @@ function onMetaDbUpgradeNeeded(
upgradeTransaction: IDBTransaction,
) {
upgradeFromStoreMap(
MetaStores,
walletMetadataStore,
db,
oldVersion,
newVersion,
@ -137,7 +125,7 @@ function onMetaDbUpgradeNeeded(
export async function openTalerDatabase(
idbFactory: IDBFactory,
onVersionChange: () => void,
): Promise<Database<typeof Stores>> {
): Promise<DbAccess<typeof WalletStoresV1>> {
const metaDbHandle = await openDatabase(
idbFactory,
TALER_META_DB_NAME,
@ -146,23 +134,24 @@ export async function openTalerDatabase(
onMetaDbUpgradeNeeded,
);
const metaDb = new Database(metaDbHandle, MetaStores);
const metaDb = new DbAccess(metaDbHandle, walletMetadataStore);
let currentMainVersion: string | undefined;
await metaDb.runWithWriteTransaction([MetaStores.metaConfig], async (tx) => {
const dbVersionRecord = await tx.get(
MetaStores.metaConfig,
CURRENT_DB_CONFIG_KEY,
);
if (!dbVersionRecord) {
currentMainVersion = TALER_DB_NAME;
await tx.put(MetaStores.metaConfig, {
key: CURRENT_DB_CONFIG_KEY,
value: TALER_DB_NAME,
});
} else {
currentMainVersion = dbVersionRecord.value;
}
});
await metaDb
.mktx((x) => ({
metaConfig: x.metaConfig,
}))
.runReadWrite(async (tx) => {
const dbVersionRecord = await tx.metaConfig.get(CURRENT_DB_CONFIG_KEY);
if (!dbVersionRecord) {
currentMainVersion = TALER_DB_NAME;
await tx.metaConfig.put({
key: CURRENT_DB_CONFIG_KEY,
value: TALER_DB_NAME,
});
} else {
currentMainVersion = dbVersionRecord.value;
}
});
if (currentMainVersion !== TALER_DB_NAME) {
// In the future, the migration logic will be implemented here.
@ -177,11 +166,12 @@ export async function openTalerDatabase(
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 {
@ -634,7 +624,7 @@ export interface ExchangeRecord {
/**
* Status of updating the info about the exchange.
*
*
* FIXME: Adapt this to recent changes regarding how
* updating exchange details works.
*/
@ -1683,289 +1673,167 @@ export interface TombstoneRecord {
id: string;
}
class ExchangesStore extends Store<"exchanges", ExchangeRecord> {
constructor() {
super("exchanges", { keyPath: "baseUrl" });
}
}
class ExchangeDetailsStore extends Store<
"exchangeDetails",
ExchangeDetailsRecord
> {
constructor() {
super("exchangeDetails", {
export const WalletStoresV1 = {
coins: describeStore(
describeContents<CoinRecord>("coins", {
keyPath: "coinPub",
}),
{
byBaseUrl: describeIndex("byBaseUrl", "exchangeBaseUrl"),
byDenomPubHash: describeIndex("byDenomPubHash", "denomPubHash"),
byCoinEvHash: describeIndex("byCoinEvHash", "coinEvHash"),
},
),
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"],
});
}
}
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",
}),
{},
),
proposals: describeStore(
describeContents<ProposalRecord>("proposals", { keyPath: "proposalId" }),
{
byUrlAndOrderId: describeIndex("byUrlAndOrderId", [
"merchantBaseUrl",
"orderId",
]),
},
),
refreshGroups: describeStore(
describeContents<RefreshGroupRecord>("refreshGroups", {
keyPath: "refreshGroupId",
},
}),
{},
),
recoupGroups: new Store<"recoupGroups", RecoupGroupRecord>("recoupGroups", {
keyPath: "recoupGroupId",
}),
reserves: new ReservesStore(),
purchases: new PurchasesStore(),
tips: new TipsStore(),
withdrawalGroups: new WithdrawalGroupsStore(),
planchets: new PlanchetsStore(),
bankWithdrawUris: new BankWithdrawUrisStore(),
backupProviders: new BackupProvidersStore(),
depositGroups: new DepositGroupsStore(),
tombstones: new TombstonesStore(),
ghostDepositGroups: new Store<"ghostDepositGroups", GhostDepositGroupRecord>(
"ghostDepositGroups",
recoupGroups: describeStore(
describeContents<RecoupGroupRecord>("recoupGroups", {
keyPath: "recoupGroupId",
}),
{},
),
reserves: describeStore(
describeContents<ReserveRecord>("reserves", { keyPath: "reservePub" }),
{
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>> {
constructor() {
super("metaConfig", { keyPath: "key" });
}
}
export const MetaStores = {
metaConfig: new MetaConfigStore(),
export const walletMetadataStore = {
metaConfig: describeStore(
describeContents<ConfigRecord<any>>("metaConfig", { keyPath: "key" }),
{},
),
};

View File

@ -57,7 +57,6 @@ import {
} from "./state";
import { Amounts, getTimestampNow } from "@gnu-taler/taler-util";
import {
Stores,
CoinSourceType,
CoinStatus,
RefundState,
@ -66,29 +65,28 @@ import {
} from "../../db.js";
import { encodeCrock, stringToBytes, getRandomBytes } from "../../index.js";
import { canonicalizeBaseUrl, canonicalJson } from "@gnu-taler/taler-util";
import { getExchangeDetails } from "../exchanges.js";
export async function exportBackup(
ws: InternalWalletState,
): Promise<WalletBackupContentV1> {
await provideBackupState(ws);
return ws.db.runWithWriteTransaction(
[
Stores.config,
Stores.exchanges,
Stores.exchangeDetails,
Stores.coins,
Stores.denominations,
Stores.purchases,
Stores.proposals,
Stores.refreshGroups,
Stores.backupProviders,
Stores.tips,
Stores.recoupGroups,
Stores.reserves,
Stores.withdrawalGroups,
],
async (tx) => {
return ws.db
.mktx((x) => ({
config: x.config,
exchanges: x.exchanges,
exchangeDetails: x.exchangeDetails,
coins: x.coins,
denominations: x.denominations,
purchases: x.purchases,
proposals: x.proposals,
refreshGroups: x.refreshGroups,
backupProviders: x.backupProviders,
tips: x.tips,
recoupGroups: x.recoupGroups,
reserves: x.reserves,
withdrawalGroups: x.withdrawalGroups,
}))
.runReadWrite(async (tx) => {
const bs = await getWalletBackupState(ws, tx);
const backupExchangeDetails: BackupExchangeDetails[] = [];
@ -108,7 +106,7 @@ export async function exportBackup(
[reservePub: string]: BackupWithdrawalGroup[];
} = {};
await tx.iter(Stores.withdrawalGroups).forEachAsync(async (wg) => {
await tx.withdrawalGroups.iter().forEachAsync(async (wg) => {
const withdrawalGroups = (withdrawalGroupsByReserve[
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 = {
initial_selected_denoms: reserve.initialDenomSel.selectedDenoms.map(
(x) => ({
@ -149,7 +147,7 @@ export async function exportBackup(
backupReserves.push(backupReserve);
});
await tx.iter(Stores.tips).forEach((tip) => {
await tx.tips.iter().forEach((tip) => {
backupTips.push({
exchange_base_url: tip.exchangeBaseUrl,
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({
recoup_group_id: recoupGroup.recoupGroupId,
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;
if (bp.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;
switch (coin.coinSource.type) {
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[
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;
if (!dp) {
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.
const wi = ex.wireInfo;
@ -323,7 +321,7 @@ export async function exportBackup(
const purchaseProposalIdSet = new Set<string>();
await tx.iter(Stores.purchases).forEach((purch) => {
await tx.purchases.iter().forEach((purch) => {
const refunds: BackupRefundItem[] = [];
purchaseProposalIdSet.add(purch.proposalId);
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)) {
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[] = [];
for (let i = 0; i < rg.oldCoinPubs.length; i++) {
@ -482,13 +480,12 @@ export async function exportBackup(
hash(stringToBytes(canonicalJson(backupBlob))),
);
bs.lastBackupNonce = encodeCrock(getRandomBytes(32));
await tx.put(Stores.config, {
await tx.config.put({
key: WALLET_BACKUP_STATE_KEY,
value: bs,
});
}
return backupBlob;
},
);
});
}

View File

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

View File

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

View File

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

View File

@ -17,10 +17,10 @@
/**
* Imports.
*/
import { AmountJson, BalancesResponse, Amounts } from "@gnu-taler/taler-util";
import { Stores, CoinStatus } from "../db.js";
import { TransactionHandle } from "../index.js";
import { Logger } from "@gnu-taler/taler-util";
import { AmountJson, BalancesResponse, Amounts, Logger } from "@gnu-taler/taler-util";
import { CoinStatus, WalletStoresV1 } from "../db.js";
import { GetReadOnlyAccess } from "../util/query.js";
import { InternalWalletState } from "./state.js";
const logger = new Logger("withdraw.ts");
@ -36,13 +36,12 @@ interface WalletBalance {
*/
export async function getBalancesInsideTransaction(
ws: InternalWalletState,
tx: TransactionHandle<
| typeof Stores.reserves
| typeof Stores.coins
| typeof Stores.reserves
| typeof Stores.refreshGroups
| typeof Stores.withdrawalGroups
>,
tx: GetReadOnlyAccess<{
reserves: typeof WalletStoresV1.reserves;
coins: typeof WalletStoresV1.coins;
refreshGroups: typeof WalletStoresV1.refreshGroups;
withdrawalGroups: typeof WalletStoresV1.withdrawalGroups;
}>,
): Promise<BalancesResponse> {
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.
await tx.iter(Stores.reserves).forEach((r) => {
await tx.reserves.iter().forEach((r) => {
const b = initBalance(r.currency);
if (!r.initialWithdrawalStarted) {
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
// already be in a refresh session.
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
// in coins being added to the wallet.
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) {
return;
}
@ -147,18 +146,17 @@ export async function getBalances(
): Promise<BalancesResponse> {
logger.trace("starting to compute balance");
const wbal = await ws.db.runWithReadTransaction(
[
Stores.coins,
Stores.refreshGroups,
Stores.reserves,
Stores.purchases,
Stores.withdrawalGroups,
],
async (tx) => {
const wbal = await ws.db
.mktx((x) => ({
coins: x.coins,
refreshGroups: x.refreshGroups,
reserves: x.reserves,
purchases: x.purchases,
withdrawalGroups: x.withdrawalGroups,
}))
.runReadOnly(async (tx) => {
return getBalancesInsideTransaction(ws, tx);
},
);
});
logger.trace("finished computing wallet balance");

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -22,7 +22,7 @@ import {
DenominationRecord,
RefreshGroupRecord,
RefreshPlanchet,
Stores,
WalletStoresV1,
} from "../db.js";
import {
codecForExchangeMeltResponse,
@ -38,7 +38,6 @@ import { amountToPretty } from "@gnu-taler/taler-util";
import { readSuccessResponseJsonOrThrow } from "../util/http";
import { checkDbInvariant } from "../util/invariants";
import { Logger } from "@gnu-taler/taler-util";
import { TransactionHandle } from "../util/query";
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries";
import {
Duration,
@ -57,6 +56,8 @@ import { updateExchangeFromUrl } from "./exchanges";
import { EXCHANGE_COINS_LOCK, InternalWalletState } from "./state";
import { isWithdrawableDenom, selectWithdrawalDenominations } from "./withdraw";
import { RefreshNewDenomInfo } from "../crypto/cryptoTypes.js";
import { GetReadWriteAccess } from "../util/query.js";
import { Wallet } from "../wallet.js";
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(
ws: InternalWalletState,
@ -105,45 +106,68 @@ async function refreshCreateSession(
logger.trace(
`creating refresh session for coin ${coinIndex} in refresh group ${refreshGroupId}`,
);
const refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId);
if (!refreshGroup) {
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) {
return;
}
if (refreshGroup.finishedPerCoin[coinIndex]) {
return;
}
const existingRefreshSession =
refreshGroup.refreshSessionPerCoin[coinIndex];
if (existingRefreshSession) {
return;
}
const oldCoinPub = refreshGroup.oldCoinPubs[coinIndex];
const coin = await tx.coins.get(oldCoinPub);
if (!coin) {
throw Error("Can't refresh, coin not found");
}
return { refreshGroup, coin };
});
if (!d) {
return;
}
if (refreshGroup.finishedPerCoin[coinIndex]) {
return;
}
const existingRefreshSession = refreshGroup.refreshSessionPerCoin[coinIndex];
if (existingRefreshSession) {
return;
}
const oldCoinPub = refreshGroup.oldCoinPubs[coinIndex];
const coin = await ws.db.get(Stores.coins, oldCoinPub);
if (!coin) {
throw Error("Can't refresh, coin not found");
}
const { refreshGroup, coin } = d;
const { exchange } = await updateExchangeFromUrl(ws, coin.exchangeBaseUrl);
if (!exchange) {
throw Error("db inconsistent: exchange of coin not found");
}
const oldDenom = await ws.db.get(Stores.denominations, [
exchange.baseUrl,
coin.denomPubHash,
]);
const { availableAmount, availableDenoms } = await ws.db
.mktx((x) => ({
denominations: x.denominations,
}))
.runReadOnly(async (tx) => {
const oldDenom = await tx.denominations.get([
exchange.baseUrl,
coin.denomPubHash,
]);
if (!oldDenom) {
throw Error("db inconsistent: denomination for coin not found");
}
if (!oldDenom) {
throw Error("db inconsistent: denomination for coin not found");
}
const availableDenoms: DenominationRecord[] = await ws.db
.iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl)
.toArray();
const availableDenoms: DenominationRecord[] = await tx.denominations.indexes.byExchangeBaseUrl
.iter(exchange.baseUrl)
.toArray();
const availableAmount = Amounts.sub(
refreshGroup.inputPerCoin[coinIndex],
oldDenom.feeRefresh,
).amount;
const availableAmount = Amounts.sub(
refreshGroup.inputPerCoin[coinIndex],
oldDenom.feeRefresh,
).amount;
return { availableAmount, availableDenoms };
});
const newCoinDenoms = selectWithdrawalDenominations(
availableAmount,
@ -156,10 +180,13 @@ async function refreshCreateSession(
availableAmount,
)} too small`,
);
await ws.db.runWithWriteTransaction(
[Stores.coins, Stores.refreshGroups],
async (tx) => {
const rg = await tx.get(Stores.refreshGroups, refreshGroupId);
await ws.db
.mktx((x) => ({
coins: x.coins,
refreshGroups: x.refreshGroups,
}))
.runReadWrite(async (tx) => {
const rg = await tx.refreshGroups.get(refreshGroupId);
if (!rg) {
return;
}
@ -175,9 +202,8 @@ async function refreshCreateSession(
rg.timestampFinished = getTimestampNow();
rg.retryInfo = initRetryInfo(false);
}
await tx.put(Stores.refreshGroups, rg);
},
);
await tx.refreshGroups.put(rg);
});
ws.notify({ type: NotificationType.RefreshUnwarranted });
return;
}
@ -185,10 +211,13 @@ async function refreshCreateSession(
const sessionSecretSeed = encodeCrock(getRandomBytes(64));
// Store refresh session for this coin in the database.
await ws.db.runWithWriteTransaction(
[Stores.refreshGroups, Stores.coins],
async (tx) => {
const rg = await tx.get(Stores.refreshGroups, refreshGroupId);
await ws.db
.mktx((x) => ({
refreshGroups: x.refreshGroups,
coins: x.coins,
}))
.runReadWrite(async (tx) => {
const rg = await tx.refreshGroups.get(refreshGroupId);
if (!rg) {
return;
}
@ -204,9 +233,8 @@ async function refreshCreateSession(
})),
amountRefreshOutput: newCoinDenoms.totalCoinValue,
};
await tx.put(Stores.refreshGroups, rg);
},
);
await tx.refreshGroups.put(rg);
});
logger.info(
`created refresh session for coin #${coinIndex} in ${refreshGroupId}`,
);
@ -222,48 +250,63 @@ async function refreshMelt(
refreshGroupId: string,
coinIndex: number,
): Promise<void> {
const refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId);
if (!refreshGroup) {
return;
}
const refreshSession = refreshGroup.refreshSessionPerCoin[coinIndex];
if (!refreshSession) {
return;
}
if (refreshSession.norevealIndex !== undefined) {
return;
}
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) {
return;
}
const refreshSession = refreshGroup.refreshSessionPerCoin[coinIndex];
if (!refreshSession) {
return;
}
if (refreshSession.norevealIndex !== undefined) {
return;
}
const oldCoin = await ws.db.get(
Stores.coins,
refreshGroup.oldCoinPubs[coinIndex],
);
checkDbInvariant(!!oldCoin, "melt coin doesn't exist");
const oldDenom = await ws.db.get(Stores.denominations, [
oldCoin.exchangeBaseUrl,
oldCoin.denomPubHash,
]);
checkDbInvariant(!!oldDenom, "denomination for melted coin doesn't exist");
const oldCoin = await tx.coins.get(refreshGroup.oldCoinPubs[coinIndex]);
checkDbInvariant(!!oldCoin, "melt coin doesn't exist");
const oldDenom = await tx.denominations.get([
oldCoin.exchangeBaseUrl,
oldCoin.denomPubHash,
]);
checkDbInvariant(
!!oldDenom,
"denomination for melted coin doesn't exist",
);
const newCoinDenoms: RefreshNewDenomInfo[] = [];
const newCoinDenoms: RefreshNewDenomInfo[] = [];
for (const dh of refreshSession.newDenoms) {
const newDenom = await ws.db.get(Stores.denominations, [
oldCoin.exchangeBaseUrl,
dh.denomPubHash,
]);
checkDbInvariant(
!!newDenom,
"new denomination for refresh not in database",
);
newCoinDenoms.push({
count: dh.count,
denomPub: newDenom.denomPub,
feeWithdraw: newDenom.feeWithdraw,
value: newDenom.value,
for (const dh of refreshSession.newDenoms) {
const newDenom = await tx.denominations.get([
oldCoin.exchangeBaseUrl,
dh.denomPubHash,
]);
checkDbInvariant(
!!newDenom,
"new denomination for refresh not in database",
);
newCoinDenoms.push({
count: dh.count,
denomPub: newDenom.denomPub,
feeWithdraw: newDenom.feeWithdraw,
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({
kappa: 3,
meltCoinDenomPubHash: oldCoin.denomPubHash,
@ -303,20 +346,28 @@ async function refreshMelt(
refreshSession.norevealIndex = norevealIndex;
await ws.db.mutate(Stores.refreshGroups, refreshGroupId, (rg) => {
const rs = rg.refreshSessionPerCoin[coinIndex];
if (rg.timestampFinished) {
return;
}
if (!rs) {
return;
}
if (rs.norevealIndex !== undefined) {
return;
}
rs.norevealIndex = norevealIndex;
return rg;
});
await ws.db
.mktx((x) => ({
refreshGroups: x.refreshGroups,
}))
.runReadWrite(async (tx) => {
const rg = await tx.refreshGroups.get(refreshGroupId);
if (!rg) {
return;
}
if (rg.timestampFinished) {
return;
}
const rs = rg.refreshSessionPerCoin[coinIndex];
if (!rs) {
return;
}
if (rs.norevealIndex !== undefined) {
return;
}
rs.norevealIndex = norevealIndex;
await tx.refreshGroups.put(rg);
});
ws.notify({
type: NotificationType.RefreshMelted,
@ -328,49 +379,78 @@ async function refreshReveal(
refreshGroupId: string,
coinIndex: number,
): Promise<void> {
const refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId);
if (!refreshGroup) {
return;
}
const refreshSession = refreshGroup.refreshSessionPerCoin[coinIndex];
if (!refreshSession) {
return;
}
const norevealIndex = refreshSession.norevealIndex;
if (norevealIndex === undefined) {
throw Error("can't reveal without melting first");
}
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) {
return;
}
const refreshSession = refreshGroup.refreshSessionPerCoin[coinIndex];
if (!refreshSession) {
return;
}
const norevealIndex = refreshSession.norevealIndex;
if (norevealIndex === undefined) {
throw Error("can't reveal without melting first");
}
const oldCoin = await ws.db.get(
Stores.coins,
refreshGroup.oldCoinPubs[coinIndex],
);
checkDbInvariant(!!oldCoin, "melt coin doesn't exist");
const oldDenom = await ws.db.get(Stores.denominations, [
oldCoin.exchangeBaseUrl,
oldCoin.denomPubHash,
]);
checkDbInvariant(!!oldDenom, "denomination for melted coin doesn't exist");
const oldCoin = await tx.coins.get(refreshGroup.oldCoinPubs[coinIndex]);
checkDbInvariant(!!oldCoin, "melt coin doesn't exist");
const oldDenom = await tx.denominations.get([
oldCoin.exchangeBaseUrl,
oldCoin.denomPubHash,
]);
checkDbInvariant(
!!oldDenom,
"denomination for melted coin doesn't exist",
);
const newCoinDenoms: RefreshNewDenomInfo[] = [];
const newCoinDenoms: RefreshNewDenomInfo[] = [];
for (const dh of refreshSession.newDenoms) {
const newDenom = await ws.db.get(Stores.denominations, [
oldCoin.exchangeBaseUrl,
dh.denomPubHash,
]);
checkDbInvariant(
!!newDenom,
"new denomination for refresh not in database",
);
newCoinDenoms.push({
count: dh.count,
denomPub: newDenom.denomPub,
feeWithdraw: newDenom.feeWithdraw,
value: newDenom.value,
for (const dh of refreshSession.newDenoms) {
const newDenom = await tx.denominations.get([
oldCoin.exchangeBaseUrl,
dh.denomPubHash,
]);
checkDbInvariant(
!!newDenom,
"new denomination for refresh not in database",
);
newCoinDenoms.push({
count: dh.count,
denomPub: newDenom.denomPub,
feeWithdraw: newDenom.feeWithdraw,
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({
kappa: 3,
meltCoinDenomPubHash: oldCoin.denomPubHash,
@ -389,14 +469,6 @@ async function refreshReveal(
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 newDenomsFlat: string[] = [];
const linkSigs: string[] = [];
@ -406,9 +478,9 @@ async function refreshReveal(
for (let j = 0; j < dsel.count; j++) {
const newCoinIndex = linkSigs.length;
const linkSig = await ws.cryptoApi.signCoinLink(
meltCoinRecord.coinPriv,
oldCoin.coinPriv,
dsel.denomPubHash,
meltCoinRecord.coinPub,
oldCoin.coinPub,
derived.transferPubs[norevealIndex],
planchets[newCoinIndex].coinEv,
);
@ -447,10 +519,17 @@ async function refreshReveal(
for (let i = 0; i < refreshSession.newDenoms.length; i++) {
for (let j = 0; j < refreshSession.newDenoms[i].count; j++) {
const newCoinIndex = coins.length;
const denom = await ws.db.get(Stores.denominations, [
oldCoin.exchangeBaseUrl,
refreshSession.newDenoms[i].denomPubHash,
]);
// 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,
refreshSession.newDenoms[i].denomPubHash,
]);
});
if (!denom) {
console.error("denom not found");
continue;
@ -483,10 +562,13 @@ async function refreshReveal(
}
}
await ws.db.runWithWriteTransaction(
[Stores.coins, Stores.refreshGroups],
async (tx) => {
const rg = await tx.get(Stores.refreshGroups, refreshGroupId);
await ws.db
.mktx((x) => ({
coins: x.coins,
refreshGroups: x.refreshGroups,
}))
.runReadWrite(async (tx) => {
const rg = await tx.refreshGroups.get(refreshGroupId);
if (!rg) {
logger.warn("no refresh session found");
return;
@ -508,11 +590,10 @@ async function refreshReveal(
rg.retryInfo = initRetryInfo(false);
}
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)");
ws.notify({
type: NotificationType.RefreshRevealed,
@ -524,19 +605,23 @@ async function incrementRefreshRetry(
refreshGroupId: string,
err: TalerErrorDetails | undefined,
): Promise<void> {
await ws.db.runWithWriteTransaction([Stores.refreshGroups], async (tx) => {
const r = await tx.get(Stores.refreshGroups, refreshGroupId);
if (!r) {
return;
}
if (!r.retryInfo) {
return;
}
r.retryInfo.retryCounter++;
updateRetryInfoTimeout(r.retryInfo);
r.lastError = err;
await tx.put(Stores.refreshGroups, r);
});
await ws.db
.mktx((x) => ({
refreshGroups: x.refreshGroups,
}))
.runReadWrite(async (tx) => {
const r = await tx.refreshGroups.get(refreshGroupId);
if (!r) {
return;
}
if (!r.retryInfo) {
return;
}
r.retryInfo.retryCounter++;
updateRetryInfoTimeout(r.retryInfo);
r.lastError = err;
await tx.refreshGroups.put(r);
});
if (err) {
ws.notify({ type: NotificationType.RefreshOperationError, error: err });
}
@ -562,14 +647,19 @@ export async function processRefreshGroup(
async function resetRefreshGroupRetry(
ws: InternalWalletState,
refreshSessionId: string,
refreshGroupId: string,
): Promise<void> {
await ws.db.mutate(Stores.refreshGroups, refreshSessionId, (x) => {
if (x.retryInfo.active) {
x.retryInfo = initRetryInfo();
}
return x;
});
await ws.db
.mktx((x) => ({
refreshGroups: x.refreshGroups,
}))
.runReadWrite(async (tx) => {
const x = await tx.refreshGroups.get(refreshGroupId);
if (x && x.retryInfo.active) {
x.retryInfo = initRetryInfo();
await tx.refreshGroups.put(x);
}
});
}
async function processRefreshGroupImpl(
@ -580,13 +670,20 @@ async function processRefreshGroupImpl(
if (forceNow) {
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) {
return;
}
if (refreshGroup.timestampFinished) {
return;
}
// Process refresh sessions of the group in parallel.
const ps = refreshGroup.oldCoinPubs.map((x, i) =>
processRefreshSession(ws, refreshGroupId, i),
);
@ -602,7 +699,11 @@ async function processRefreshSession(
logger.trace(
`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) {
return;
}
@ -611,7 +712,11 @@ async function processRefreshSession(
}
if (!refreshGroup.refreshSessionPerCoin[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) {
return;
}
@ -646,11 +751,11 @@ async function processRefreshSession(
*/
export async function createRefreshGroup(
ws: InternalWalletState,
tx: TransactionHandle<
| typeof Stores.denominations
| typeof Stores.coins
| typeof Stores.refreshGroups
>,
tx: GetReadWriteAccess<{
denominations: typeof WalletStoresV1.denominations;
coins: typeof WalletStoresV1.coins;
refreshGroups: typeof WalletStoresV1.refreshGroups;
}>,
oldCoinPubs: CoinPublicKey[],
reason: RefreshReason,
): Promise<RefreshGroupId> {
@ -667,8 +772,8 @@ export async function createRefreshGroup(
if (denomsPerExchange[exchangeBaseUrl]) {
return denomsPerExchange[exchangeBaseUrl];
}
const allDenoms = await tx
.iterIndexed(Stores.denominations.exchangeBaseUrlIndex, exchangeBaseUrl)
const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
.iter(exchangeBaseUrl)
.filter((x) => {
return isWithdrawableDenom(x);
});
@ -677,9 +782,9 @@ export async function createRefreshGroup(
};
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");
const denom = await tx.get(Stores.denominations, [
const denom = await tx.denominations.get([
coin.exchangeBaseUrl,
coin.denomPubHash,
]);
@ -691,7 +796,7 @@ export async function createRefreshGroup(
inputPerCoin.push(refreshAmount);
coin.currentAmount = Amounts.getZero(refreshAmount.currency);
coin.status = CoinStatus.Dormant;
await tx.put(Stores.coins, coin);
await tx.coins.put(coin);
const denoms = await getDenoms(coin.exchangeBaseUrl);
const cost = getTotalRefreshCost(denoms, denom, refreshAmount);
const output = Amounts.sub(refreshAmount, cost).amount;
@ -718,7 +823,7 @@ export async function createRefreshGroup(
refreshGroup.timestampFinished = getTimestampNow();
}
await tx.put(Stores.refreshGroups, refreshGroup);
await tx.refreshGroups.put(refreshGroup);
logger.trace(`created refresh group ${refreshGroupId}`);
@ -760,20 +865,20 @@ export async function autoRefresh(
exchangeBaseUrl: string,
): Promise<void> {
await updateExchangeFromUrl(ws, exchangeBaseUrl, true);
await ws.db.runWithWriteTransaction(
[
Stores.coins,
Stores.denominations,
Stores.refreshGroups,
Stores.exchanges,
],
async (tx) => {
const exchange = await tx.get(Stores.exchanges, exchangeBaseUrl);
await ws.db
.mktx((x) => ({
coins: x.coins,
denominations: x.denominations,
refreshGroups: x.refreshGroups,
exchanges: x.exchanges,
}))
.runReadWrite(async (tx) => {
const exchange = await tx.exchanges.get(exchangeBaseUrl);
if (!exchange) {
return;
}
const coins = await tx
.iterIndexed(Stores.coins.exchangeBaseUrlIndex, exchangeBaseUrl)
const coins = await tx.coins.indexes.byBaseUrl
.iter(exchangeBaseUrl)
.toArray();
const refreshCoins: CoinPublicKey[] = [];
for (const coin of coins) {
@ -783,7 +888,7 @@ export async function autoRefresh(
if (coin.suspended) {
continue;
}
const denom = await tx.get(Stores.denominations, [
const denom = await tx.denominations.get([
exchangeBaseUrl,
coin.denomPubHash,
]);
@ -800,8 +905,8 @@ export async function autoRefresh(
await createRefreshGroup(ws, tx, refreshCoins, RefreshReason.Scheduled);
}
const denoms = await tx
.iterIndexed(Stores.denominations.exchangeBaseUrlIndex, exchangeBaseUrl)
const denoms = await tx.denominations.indexes.byExchangeBaseUrl
.iter(exchangeBaseUrl)
.toArray();
let minCheckThreshold = timestampAddDuration(
getTimestampNow(),
@ -817,7 +922,6 @@ export async function autoRefresh(
minCheckThreshold = timestampMin(minCheckThreshold, checkThreshold);
}
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";
import { Logger } from "@gnu-taler/taler-util";
import { readSuccessResponseJsonOrThrow } from "../util/http";
import { TransactionHandle } from "../util/query";
import { URL } from "../util/url";
import { updateRetryInfoTimeout, initRetryInfo } from "../util/retries";
import { checkDbInvariant } from "../util/invariants";
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 { GetReadWriteAccess } from "../util/query.js";
import { Wallet } from "../wallet.js";
const logger = new Logger("refund.ts");
@ -66,19 +74,23 @@ async function incrementPurchaseQueryRefundRetry(
proposalId: string,
err: TalerErrorDetails | undefined,
): Promise<void> {
await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => {
const pr = await tx.get(Stores.purchases, proposalId);
if (!pr) {
return;
}
if (!pr.refundStatusRetryInfo) {
return;
}
pr.refundStatusRetryInfo.retryCounter++;
updateRetryInfoTimeout(pr.refundStatusRetryInfo);
pr.lastRefundStatusError = err;
await tx.put(Stores.purchases, pr);
});
await ws.db
.mktx((x) => ({
purchases: x.purchases,
}))
.runReadWrite(async (tx) => {
const pr = await tx.purchases.get(proposalId);
if (!pr) {
return;
}
if (!pr.refundStatusRetryInfo) {
return;
}
pr.refundStatusRetryInfo.retryCounter++;
updateRetryInfoTimeout(pr.refundStatusRetryInfo);
pr.lastRefundStatusError = err;
await tx.purchases.put(pr);
});
if (err) {
ws.notify({
type: NotificationType.RefundStatusOperationError,
@ -92,7 +104,10 @@ function getRefundKey(d: MerchantCoinRefundStatus): string {
}
async function applySuccessfulRefund(
tx: TransactionHandle<typeof Stores.coins | typeof Stores.denominations>,
tx: GetReadWriteAccess<{
coins: typeof WalletStoresV1.coins;
denominations: typeof WalletStoresV1.denominations;
}>,
p: PurchaseRecord,
refreshCoinsMap: Record<string, { coinPub: string }>,
r: MerchantCoinRefundSuccessStatus,
@ -100,12 +115,12 @@ async function applySuccessfulRefund(
// FIXME: check signature before storing it as valid!
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) {
logger.warn("coin not found, can't apply refund");
return;
}
const denom = await tx.get(Stores.denominations, [
const denom = await tx.denominations.get([
coin.exchangeBaseUrl,
coin.denomPubHash,
]);
@ -119,13 +134,10 @@ async function applySuccessfulRefund(
coin.currentAmount = Amounts.add(coin.currentAmount, refundAmount).amount;
coin.currentAmount = Amounts.sub(coin.currentAmount, refundFee).amount;
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
.iterIndexed(
Stores.denominations.exchangeBaseUrlIndex,
coin.exchangeBaseUrl,
)
const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
.iter(coin.exchangeBaseUrl)
.toArray();
const amountLeft = Amounts.sub(
@ -153,18 +165,21 @@ async function applySuccessfulRefund(
}
async function storePendingRefund(
tx: TransactionHandle<typeof Stores.denominations | typeof Stores.coins>,
tx: GetReadWriteAccess<{
denominations: typeof WalletStoresV1.denominations;
coins: typeof WalletStoresV1.coins;
}>,
p: PurchaseRecord,
r: MerchantCoinRefundFailureStatus,
): Promise<void> {
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) {
logger.warn("coin not found, can't apply refund");
return;
}
const denom = await tx.get(Stores.denominations, [
const denom = await tx.denominations.get([
coin.exchangeBaseUrl,
coin.denomPubHash,
]);
@ -173,11 +188,8 @@ async function storePendingRefund(
throw Error("inconsistent database");
}
const allDenoms = await tx
.iterIndexed(
Stores.denominations.exchangeBaseUrlIndex,
coin.exchangeBaseUrl,
)
const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
.iter(coin.exchangeBaseUrl)
.toArray();
const amountLeft = Amounts.sub(
@ -205,19 +217,22 @@ async function storePendingRefund(
}
async function storeFailedRefund(
tx: TransactionHandle<typeof Stores.coins | typeof Stores.denominations>,
tx: GetReadWriteAccess<{
coins: typeof WalletStoresV1.coins;
denominations: typeof WalletStoresV1.denominations;
}>,
p: PurchaseRecord,
refreshCoinsMap: Record<string, { coinPub: string }>,
r: MerchantCoinRefundFailureStatus,
): Promise<void> {
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) {
logger.warn("coin not found, can't apply refund");
return;
}
const denom = await tx.get(Stores.denominations, [
const denom = await tx.denominations.get([
coin.exchangeBaseUrl,
coin.denomPubHash,
]);
@ -226,11 +241,8 @@ async function storeFailedRefund(
throw Error("inconsistent database");
}
const allDenoms = await tx
.iterIndexed(
Stores.denominations.exchangeBaseUrlIndex,
coin.exchangeBaseUrl,
)
const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
.iter(coin.exchangeBaseUrl)
.toArray();
const amountLeft = Amounts.sub(
@ -260,12 +272,12 @@ async function storeFailedRefund(
// Refund failed because the merchant didn't even try to deposit
// the coin yet, so we try to refresh.
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) {
logger.warn("coin not found, can't apply refund");
return;
}
const denom = await tx.get(Stores.denominations, [
const denom = await tx.denominations.get([
coin.exchangeBaseUrl,
coin.denomPubHash,
]);
@ -287,7 +299,7 @@ async function storeFailedRefund(
).amount;
}
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);
const now = getTimestampNow();
await ws.db.runWithWriteTransaction(
[
Stores.purchases,
Stores.coins,
Stores.denominations,
Stores.refreshGroups,
],
async (tx) => {
const p = await tx.get(Stores.purchases, proposalId);
await ws.db
.mktx((x) => ({
purchases: x.purchases,
coins: x.coins,
denominations: x.denominations,
refreshGroups: x.refreshGroups,
}))
.runReadWrite(async (tx) => {
const p = await tx.purchases.get(proposalId);
if (!p) {
logger.error("purchase not found, not adding refunds");
return;
@ -409,9 +421,8 @@ async function acceptRefunds(
logger.trace("refund query not done");
}
await tx.put(Stores.purchases, p);
},
);
await tx.purchases.put(p);
});
ws.notify({
type: NotificationType.RefundQueried,
@ -444,10 +455,16 @@ export async function applyRefund(
throw Error("invalid refund URI");
}
let purchase = await ws.db.getIndexed(Stores.purchases.orderIdIndex, [
parseResult.merchantBaseUrl,
parseResult.orderId,
]);
let purchase = await ws.db
.mktx((x) => ({
purchases: x.purchases,
}))
.runReadOnly(async (tx) => {
return tx.purchases.indexes.byMerchantUrlAndOrderId.get([
parseResult.merchantBaseUrl,
parseResult.orderId,
]);
});
if (!purchase) {
throw Error(
@ -458,10 +475,12 @@ export async function applyRefund(
const proposalId = purchase.proposalId;
logger.info("processing purchase for refund");
const success = await ws.db.runWithWriteTransaction(
[Stores.purchases],
async (tx) => {
const p = await tx.get(Stores.purchases, proposalId);
const success = await ws.db
.mktx((x) => ({
purchases: x.purchases,
}))
.runReadWrite(async (tx) => {
const p = await tx.purchases.get(proposalId);
if (!p) {
logger.error("no purchase found for refund URL");
return false;
@ -469,10 +488,9 @@ export async function applyRefund(
p.refundQueryRequested = true;
p.lastRefundStatusError = undefined;
p.refundStatusRetryInfo = initRetryInfo();
await tx.put(Stores.purchases, p);
await tx.purchases.put(p);
return true;
},
);
});
if (success) {
ws.notify({
@ -481,7 +499,13 @@ export async function applyRefund(
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) {
throw Error("purchase no longer exists");
@ -559,12 +583,17 @@ async function resetPurchaseQueryRefundRetry(
ws: InternalWalletState,
proposalId: string,
): Promise<void> {
await ws.db.mutate(Stores.purchases, proposalId, (x) => {
if (x.refundStatusRetryInfo.active) {
x.refundStatusRetryInfo = initRetryInfo();
}
return x;
});
await ws.db
.mktx((x) => ({
purchases: x.purchases,
}))
.runReadWrite(async (tx) => {
const x = await tx.purchases.get(proposalId);
if (x && x.refundStatusRetryInfo.active) {
x.refundStatusRetryInfo = initRetryInfo();
await tx.purchases.put(x);
}
});
}
async function processPurchaseQueryRefundImpl(
@ -575,7 +604,13 @@ async function processPurchaseQueryRefundImpl(
if (forceNow) {
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) {
return;
}
@ -589,7 +624,6 @@ async function processPurchaseQueryRefundImpl(
`orders/${purchase.download.contractData.orderId}/refund`,
purchase.download.contractData.merchantBaseUrl,
);
logger.trace(`making refund request to ${requestUrl.href}`);
@ -620,18 +654,25 @@ async function processPurchaseQueryRefundImpl(
);
const abortingCoins: AbortingCoin[] = [];
for (let i = 0; i < purchase.payCoinSelection.coinPubs.length; i++) {
const coinPub = purchase.payCoinSelection.coinPubs[i];
const coin = await ws.db.get(Stores.coins, coinPub);
checkDbInvariant(!!coin, "expected coin to be present");
abortingCoins.push({
coin_pub: coinPub,
contribution: Amounts.stringify(
purchase.payCoinSelection.coinContributions[i],
),
exchange_url: coin.exchangeBaseUrl,
await ws.db
.mktx((x) => ({
coins: x.coins,
}))
.runReadOnly(async (tx) => {
for (let i = 0; i < purchase.payCoinSelection.coinPubs.length; i++) {
const coinPub = purchase.payCoinSelection.coinPubs[i];
const coin = await tx.coins.get(coinPub);
checkDbInvariant(!!coin, "expected coin to be present");
abortingCoins.push({
coin_pub: coinPub,
contribution: Amounts.stringify(
purchase.payCoinSelection.coinContributions[i],
),
exchange_url: coin.exchangeBaseUrl,
});
}
});
}
const abortReq: AbortRequest = {
h_contract: purchase.download.contractData.contractTermsHash,
@ -678,26 +719,30 @@ export async function abortFailedPayWithRefund(
ws: InternalWalletState,
proposalId: string,
): Promise<void> {
await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => {
const purchase = await tx.get(Stores.purchases, proposalId);
if (!purchase) {
throw Error("purchase not found");
}
if (purchase.timestampFirstSuccessfulPay) {
// No point in aborting it. We don't even report an error.
logger.warn(`tried to abort successful payment`);
return;
}
if (purchase.abortStatus !== AbortStatus.None) {
return;
}
purchase.refundQueryRequested = true;
purchase.paymentSubmitPending = false;
purchase.abortStatus = AbortStatus.AbortRefund;
purchase.lastPayError = undefined;
purchase.payRetryInfo = initRetryInfo(false);
await tx.put(Stores.purchases, purchase);
});
await ws.db
.mktx((x) => ({
purchases: x.purchases,
}))
.runReadWrite(async (tx) => {
const purchase = await tx.purchases.get(proposalId);
if (!purchase) {
throw Error("purchase not found");
}
if (purchase.timestampFirstSuccessfulPay) {
// No point in aborting it. We don't even report an error.
logger.warn(`tried to abort successful payment`);
return;
}
if (purchase.abortStatus !== AbortStatus.None) {
return;
}
purchase.refundQueryRequested = true;
purchase.paymentSubmitPending = false;
purchase.abortStatus = AbortStatus.AbortRefund;
purchase.lastPayError = undefined;
purchase.payRetryInfo = initRetryInfo(false);
await tx.purchases.put(purchase);
});
processPurchaseQueryRefund(ws, proposalId, true).catch((e) => {
logger.trace(`error during refund processing after abort pay: ${e}`);
});

View File

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

View File

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

View File

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

View File

@ -19,7 +19,6 @@
*/
import { InternalWalletState } from "./state";
import {
Stores,
WalletRefundItem,
RefundState,
ReserveRecordStatus,
@ -85,296 +84,300 @@ export async function getTransactions(
): Promise<TransactionsResponse> {
const transactions: Transaction[] = [];
await ws.db.runWithReadTransaction(
[
Stores.coins,
Stores.denominations,
Stores.exchanges,
Stores.exchangeDetails,
Stores.proposals,
Stores.purchases,
Stores.refreshGroups,
Stores.reserves,
Stores.tips,
Stores.withdrawalGroups,
Stores.planchets,
Stores.recoupGroups,
Stores.depositGroups,
Stores.tombstones,
],
// Report withdrawals that are currently in progress.
async (tx) => {
tx.iter(Stores.withdrawalGroups).forEachAsync(async (wsr) => {
if (
shouldSkipCurrency(
transactionsRequest,
wsr.rawWithdrawalAmount.currency,
)
) {
return;
}
if (shouldSkipSearch(transactionsRequest, [])) {
return;
}
const r = await tx.get(Stores.reserves, wsr.reservePub);
if (!r) {
return;
}
let amountRaw: AmountJson | undefined = undefined;
if (wsr.withdrawalGroupId === r.initialWithdrawalGroupId) {
amountRaw = r.instructedAmount;
} else {
amountRaw = wsr.denomsSel.totalWithdrawCost;
}
let withdrawalDetails: WithdrawalDetails;
if (r.bankInfo) {
withdrawalDetails = {
type: WithdrawalType.TalerBankIntegrationApi,
confirmed: true,
bankConfirmationUrl: r.bankInfo.confirmUrl,
};
} else {
const exchangeDetails = await getExchangeDetails(
tx,
wsr.exchangeBaseUrl,
);
if (!exchangeDetails) {
// FIXME: report somehow
await ws.db
.mktx((x) => ({
coins: x.coins,
denominations: x.denominations,
exchanges: x.exchanges,
exchangeDetails: x.exchangeDetails,
proposals: x.proposals,
purchases: x.purchases,
refreshGroups: x.refreshGroups,
reserves: x.reserves,
tips: x.tips,
withdrawalGroups: x.withdrawalGroups,
planchets: x.planchets,
recoupGroups: x.recoupGroups,
depositGroups: x.depositGroups,
tombstones: x.tombstones,
}))
.runReadOnly(
// Report withdrawals that are currently in progress.
async (tx) => {
tx.withdrawalGroups.iter().forEachAsync(async (wsr) => {
if (
shouldSkipCurrency(
transactionsRequest,
wsr.rawWithdrawalAmount.currency,
)
) {
return;
}
withdrawalDetails = {
type: WithdrawalType.ManualTransfer,
exchangePaytoUris:
exchangeDetails.wireInfo?.accounts.map((x) => x.payto_uri) ?? [],
};
}
transactions.push({
type: TransactionType.Withdrawal,
amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
amountRaw: Amounts.stringify(amountRaw),
withdrawalDetails,
exchangeBaseUrl: wsr.exchangeBaseUrl,
pending: !wsr.timestampFinish,
timestamp: wsr.timestampStart,
transactionId: makeEventId(
TransactionType.Withdrawal,
wsr.withdrawalGroupId,
),
...(wsr.lastError ? { error: wsr.lastError } : {}),
});
});
// Report pending withdrawals based on reserves that
// were created, but where the actual withdrawal group has
// not started yet.
tx.iter(Stores.reserves).forEachAsync(async (r) => {
if (shouldSkipCurrency(transactionsRequest, r.currency)) {
return;
}
if (shouldSkipSearch(transactionsRequest, [])) {
return;
}
if (r.initialWithdrawalStarted) {
return;
}
if (r.reserveStatus === ReserveRecordStatus.BANK_ABORTED) {
return;
}
let withdrawalDetails: WithdrawalDetails;
if (r.bankInfo) {
withdrawalDetails = {
type: WithdrawalType.TalerBankIntegrationApi,
confirmed: false,
bankConfirmationUrl: r.bankInfo.confirmUrl,
};
} else {
withdrawalDetails = {
type: WithdrawalType.ManualTransfer,
exchangePaytoUris: await getFundingPaytoUris(tx, r.reservePub),
};
}
transactions.push({
type: TransactionType.Withdrawal,
amountRaw: Amounts.stringify(r.instructedAmount),
amountEffective: Amounts.stringify(r.initialDenomSel.totalCoinValue),
exchangeBaseUrl: r.exchangeBaseUrl,
pending: true,
timestamp: r.timestampCreated,
withdrawalDetails: withdrawalDetails,
transactionId: makeEventId(
TransactionType.Withdrawal,
r.initialWithdrawalGroupId,
),
...(r.lastError ? { error: r.lastError } : {}),
});
});
tx.iter(Stores.depositGroups).forEachAsync(async (dg) => {
const amount = Amounts.parseOrThrow(dg.contractTermsRaw.amount);
if (shouldSkipCurrency(transactionsRequest, amount.currency)) {
return;
}
transactions.push({
type: TransactionType.Deposit,
amountRaw: Amounts.stringify(dg.effectiveDepositAmount),
amountEffective: Amounts.stringify(dg.totalPayCost),
pending: !dg.timestampFinished,
timestamp: dg.timestampCreated,
targetPaytoUri: dg.wire.payto_uri,
transactionId: makeEventId(
TransactionType.Deposit,
dg.depositGroupId,
),
depositGroupId: dg.depositGroupId,
...(dg.lastError ? { error: dg.lastError } : {}),
});
});
tx.iter(Stores.purchases).forEachAsync(async (pr) => {
if (
shouldSkipCurrency(
transactionsRequest,
pr.download.contractData.amount.currency,
)
) {
return;
}
const contractData = pr.download.contractData;
if (shouldSkipSearch(transactionsRequest, [contractData.summary])) {
return;
}
const proposal = await tx.get(Stores.proposals, pr.proposalId);
if (!proposal) {
return;
}
const info: OrderShortInfo = {
merchant: contractData.merchant,
orderId: contractData.orderId,
products: contractData.products,
summary: contractData.summary,
summary_i18n: contractData.summaryI18n,
contractTermsHash: contractData.contractTermsHash,
};
if (contractData.fulfillmentUrl !== "") {
info.fulfillmentUrl = contractData.fulfillmentUrl;
}
const paymentTransactionId = makeEventId(
TransactionType.Payment,
pr.proposalId,
);
const err = pr.lastPayError ?? pr.lastRefundStatusError;
transactions.push({
type: TransactionType.Payment,
amountRaw: Amounts.stringify(contractData.amount),
amountEffective: Amounts.stringify(pr.totalPayCost),
status: pr.timestampFirstSuccessfulPay
? PaymentStatus.Paid
: PaymentStatus.Accepted,
pending:
!pr.timestampFirstSuccessfulPay &&
pr.abortStatus === AbortStatus.None,
timestamp: pr.timestampAccept,
transactionId: paymentTransactionId,
proposalId: pr.proposalId,
info: info,
...(err ? { error: err } : {}),
});
const refundGroupKeys = new Set<string>();
for (const rk of Object.keys(pr.refunds)) {
const refund = pr.refunds[rk];
const groupKey = `${refund.executionTime.t_ms}`;
refundGroupKeys.add(groupKey);
}
for (const groupKey of refundGroupKeys.values()) {
const refundTombstoneId = makeEventId(
TombstoneTag.DeleteRefund,
pr.proposalId,
groupKey,
);
const tombstone = await tx.get(Stores.tombstones, refundTombstoneId);
if (tombstone) {
continue;
if (shouldSkipSearch(transactionsRequest, [])) {
return;
}
const refundTransactionId = makeEventId(
TransactionType.Refund,
pr.proposalId,
groupKey,
);
let r0: WalletRefundItem | undefined;
let amountRaw = Amounts.getZero(contractData.amount.currency);
let amountEffective = Amounts.getZero(contractData.amount.currency);
for (const rk of Object.keys(pr.refunds)) {
const refund = pr.refunds[rk];
const myGroupKey = `${refund.executionTime.t_ms}`;
if (myGroupKey !== groupKey) {
continue;
}
if (!r0) {
r0 = refund;
}
if (refund.type === RefundState.Applied) {
amountRaw = Amounts.add(amountRaw, refund.refundAmount).amount;
amountEffective = Amounts.add(
amountEffective,
Amounts.sub(
refund.refundAmount,
refund.refundFee,
refund.totalRefreshCostBound,
).amount,
).amount;
}
const r = await tx.reserves.get(wsr.reservePub);
if (!r) {
return;
}
if (!r0) {
throw Error("invariant violated");
let amountRaw: AmountJson | undefined = undefined;
if (wsr.withdrawalGroupId === r.initialWithdrawalGroupId) {
amountRaw = r.instructedAmount;
} else {
amountRaw = wsr.denomsSel.totalWithdrawCost;
}
let withdrawalDetails: WithdrawalDetails;
if (r.bankInfo) {
withdrawalDetails = {
type: WithdrawalType.TalerBankIntegrationApi,
confirmed: true,
bankConfirmationUrl: r.bankInfo.confirmUrl,
};
} else {
const exchangeDetails = await getExchangeDetails(
tx,
wsr.exchangeBaseUrl,
);
if (!exchangeDetails) {
// FIXME: report somehow
return;
}
withdrawalDetails = {
type: WithdrawalType.ManualTransfer,
exchangePaytoUris:
exchangeDetails.wireInfo?.accounts.map((x) => x.payto_uri) ??
[],
};
}
transactions.push({
type: TransactionType.Refund,
info,
refundedTransactionId: paymentTransactionId,
transactionId: refundTransactionId,
timestamp: r0.obtainedTime,
amountEffective: Amounts.stringify(amountEffective),
type: TransactionType.Withdrawal,
amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
amountRaw: Amounts.stringify(amountRaw),
pending: false,
withdrawalDetails,
exchangeBaseUrl: wsr.exchangeBaseUrl,
pending: !wsr.timestampFinish,
timestamp: wsr.timestampStart,
transactionId: makeEventId(
TransactionType.Withdrawal,
wsr.withdrawalGroupId,
),
...(wsr.lastError ? { error: wsr.lastError } : {}),
});
}
});
tx.iter(Stores.tips).forEachAsync(async (tipRecord) => {
if (
shouldSkipCurrency(
transactionsRequest,
tipRecord.tipAmountRaw.currency,
)
) {
return;
}
if (!tipRecord.acceptedTimestamp) {
return;
}
transactions.push({
type: TransactionType.Tip,
amountEffective: Amounts.stringify(tipRecord.tipAmountEffective),
amountRaw: Amounts.stringify(tipRecord.tipAmountRaw),
pending: !tipRecord.pickedUpTimestamp,
timestamp: tipRecord.acceptedTimestamp,
transactionId: makeEventId(
TransactionType.Tip,
tipRecord.walletTipId,
),
merchantBaseUrl: tipRecord.merchantBaseUrl,
error: tipRecord.lastError,
});
});
},
);
// Report pending withdrawals based on reserves that
// were created, but where the actual withdrawal group has
// not started yet.
tx.reserves.iter().forEachAsync(async (r) => {
if (shouldSkipCurrency(transactionsRequest, r.currency)) {
return;
}
if (shouldSkipSearch(transactionsRequest, [])) {
return;
}
if (r.initialWithdrawalStarted) {
return;
}
if (r.reserveStatus === ReserveRecordStatus.BANK_ABORTED) {
return;
}
let withdrawalDetails: WithdrawalDetails;
if (r.bankInfo) {
withdrawalDetails = {
type: WithdrawalType.TalerBankIntegrationApi,
confirmed: false,
bankConfirmationUrl: r.bankInfo.confirmUrl,
};
} else {
withdrawalDetails = {
type: WithdrawalType.ManualTransfer,
exchangePaytoUris: await getFundingPaytoUris(tx, r.reservePub),
};
}
transactions.push({
type: TransactionType.Withdrawal,
amountRaw: Amounts.stringify(r.instructedAmount),
amountEffective: Amounts.stringify(
r.initialDenomSel.totalCoinValue,
),
exchangeBaseUrl: r.exchangeBaseUrl,
pending: true,
timestamp: r.timestampCreated,
withdrawalDetails: withdrawalDetails,
transactionId: makeEventId(
TransactionType.Withdrawal,
r.initialWithdrawalGroupId,
),
...(r.lastError ? { error: r.lastError } : {}),
});
});
tx.depositGroups.iter().forEachAsync(async (dg) => {
const amount = Amounts.parseOrThrow(dg.contractTermsRaw.amount);
if (shouldSkipCurrency(transactionsRequest, amount.currency)) {
return;
}
transactions.push({
type: TransactionType.Deposit,
amountRaw: Amounts.stringify(dg.effectiveDepositAmount),
amountEffective: Amounts.stringify(dg.totalPayCost),
pending: !dg.timestampFinished,
timestamp: dg.timestampCreated,
targetPaytoUri: dg.wire.payto_uri,
transactionId: makeEventId(
TransactionType.Deposit,
dg.depositGroupId,
),
depositGroupId: dg.depositGroupId,
...(dg.lastError ? { error: dg.lastError } : {}),
});
});
tx.purchases.iter().forEachAsync(async (pr) => {
if (
shouldSkipCurrency(
transactionsRequest,
pr.download.contractData.amount.currency,
)
) {
return;
}
const contractData = pr.download.contractData;
if (shouldSkipSearch(transactionsRequest, [contractData.summary])) {
return;
}
const proposal = await tx.proposals.get(pr.proposalId);
if (!proposal) {
return;
}
const info: OrderShortInfo = {
merchant: contractData.merchant,
orderId: contractData.orderId,
products: contractData.products,
summary: contractData.summary,
summary_i18n: contractData.summaryI18n,
contractTermsHash: contractData.contractTermsHash,
};
if (contractData.fulfillmentUrl !== "") {
info.fulfillmentUrl = contractData.fulfillmentUrl;
}
const paymentTransactionId = makeEventId(
TransactionType.Payment,
pr.proposalId,
);
const err = pr.lastPayError ?? pr.lastRefundStatusError;
transactions.push({
type: TransactionType.Payment,
amountRaw: Amounts.stringify(contractData.amount),
amountEffective: Amounts.stringify(pr.totalPayCost),
status: pr.timestampFirstSuccessfulPay
? PaymentStatus.Paid
: PaymentStatus.Accepted,
pending:
!pr.timestampFirstSuccessfulPay &&
pr.abortStatus === AbortStatus.None,
timestamp: pr.timestampAccept,
transactionId: paymentTransactionId,
proposalId: pr.proposalId,
info: info,
...(err ? { error: err } : {}),
});
const refundGroupKeys = new Set<string>();
for (const rk of Object.keys(pr.refunds)) {
const refund = pr.refunds[rk];
const groupKey = `${refund.executionTime.t_ms}`;
refundGroupKeys.add(groupKey);
}
for (const groupKey of refundGroupKeys.values()) {
const refundTombstoneId = makeEventId(
TombstoneTag.DeleteRefund,
pr.proposalId,
groupKey,
);
const tombstone = await tx.tombstones.get(refundTombstoneId);
if (tombstone) {
continue;
}
const refundTransactionId = makeEventId(
TransactionType.Refund,
pr.proposalId,
groupKey,
);
let r0: WalletRefundItem | undefined;
let amountRaw = Amounts.getZero(contractData.amount.currency);
let amountEffective = Amounts.getZero(contractData.amount.currency);
for (const rk of Object.keys(pr.refunds)) {
const refund = pr.refunds[rk];
const myGroupKey = `${refund.executionTime.t_ms}`;
if (myGroupKey !== groupKey) {
continue;
}
if (!r0) {
r0 = refund;
}
if (refund.type === RefundState.Applied) {
amountRaw = Amounts.add(amountRaw, refund.refundAmount).amount;
amountEffective = Amounts.add(
amountEffective,
Amounts.sub(
refund.refundAmount,
refund.refundFee,
refund.totalRefreshCostBound,
).amount,
).amount;
}
}
if (!r0) {
throw Error("invariant violated");
}
transactions.push({
type: TransactionType.Refund,
info,
refundedTransactionId: paymentTransactionId,
transactionId: refundTransactionId,
timestamp: r0.obtainedTime,
amountEffective: Amounts.stringify(amountEffective),
amountRaw: Amounts.stringify(amountRaw),
pending: false,
});
}
});
tx.tips.iter().forEachAsync(async (tipRecord) => {
if (
shouldSkipCurrency(
transactionsRequest,
tipRecord.tipAmountRaw.currency,
)
) {
return;
}
if (!tipRecord.acceptedTimestamp) {
return;
}
transactions.push({
type: TransactionType.Tip,
amountEffective: Amounts.stringify(tipRecord.tipAmountEffective),
amountRaw: Amounts.stringify(tipRecord.tipAmountRaw),
pending: !tipRecord.pickedUpTimestamp,
timestamp: tipRecord.acceptedTimestamp,
transactionId: makeEventId(
TransactionType.Tip,
tipRecord.walletTipId,
),
merchantBaseUrl: tipRecord.merchantBaseUrl,
error: tipRecord.lastError,
});
});
},
);
const txPending = transactions.filter((x) => x.pending);
const txNotPending = transactions.filter((x) => !x.pending);
@ -406,110 +409,126 @@ export async function deleteTransaction(
if (type === TransactionType.Withdrawal) {
const withdrawalGroupId = rest[0];
await ws.db.runWithWriteTransaction(
[Stores.withdrawalGroups, Stores.reserves, Stores.tombstones],
async (tx) => {
const withdrawalGroupRecord = await tx.get(
Stores.withdrawalGroups,
await ws.db
.mktx((x) => ({
withdrawalGroups: x.withdrawalGroups,
reserves: x.reserves,
tombstones: x.tombstones,
}))
.runReadWrite(async (tx) => {
const withdrawalGroupRecord = await tx.withdrawalGroups.get(
withdrawalGroupId,
);
if (withdrawalGroupRecord) {
await tx.delete(Stores.withdrawalGroups, withdrawalGroupId);
await tx.put(Stores.tombstones, {
await tx.withdrawalGroups.delete(withdrawalGroupId);
await tx.tombstones.put({
id: TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId,
});
return;
}
const reserveRecord: ReserveRecord | undefined = await tx.getIndexed(
Stores.reserves.byInitialWithdrawalGroupId,
const reserveRecord:
| ReserveRecord
| undefined = await tx.reserves.indexes.byInitialWithdrawalGroupId.get(
withdrawalGroupId,
);
if (reserveRecord && !reserveRecord.initialWithdrawalStarted) {
const reservePub = reserveRecord.reservePub;
await tx.delete(Stores.reserves, reservePub);
await tx.put(Stores.tombstones, {
await tx.reserves.delete(reservePub);
await tx.tombstones.put({
id: TombstoneTag.DeleteReserve + ":" + reservePub,
});
}
},
);
});
} else if (type === TransactionType.Payment) {
const proposalId = rest[0];
await ws.db.runWithWriteTransaction(
[Stores.proposals, Stores.purchases, Stores.tombstones],
async (tx) => {
await ws.db
.mktx((x) => ({
proposals: x.proposals,
purchases: x.purchases,
tombstones: x.tombstones,
}))
.runReadWrite(async (tx) => {
let found = false;
const proposal = await tx.get(Stores.proposals, proposalId);
const proposal = await tx.proposals.get(proposalId);
if (proposal) {
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) {
found = true;
await tx.delete(Stores.proposals, proposalId);
await tx.proposals.delete(proposalId);
}
if (found) {
await tx.put(Stores.tombstones, {
await tx.tombstones.put({
id: TombstoneTag.DeletePayment + ":" + proposalId,
});
}
},
);
});
} else if (type === TransactionType.Refresh) {
const refreshGroupId = rest[0];
await ws.db.runWithWriteTransaction(
[Stores.refreshGroups, Stores.tombstones],
async (tx) => {
const rg = await tx.get(Stores.refreshGroups, refreshGroupId);
await ws.db
.mktx((x) => ({
refreshGroups: x.refreshGroups,
tombstones: x.tombstones,
}))
.runReadWrite(async (tx) => {
const rg = await tx.refreshGroups.get(refreshGroupId);
if (rg) {
await tx.delete(Stores.refreshGroups, refreshGroupId);
await tx.put(Stores.tombstones, {
await tx.refreshGroups.delete(refreshGroupId);
await tx.tombstones.put({
id: TombstoneTag.DeleteRefreshGroup + ":" + refreshGroupId,
});
}
},
);
});
} else if (type === TransactionType.Tip) {
const tipId = rest[0];
await ws.db.runWithWriteTransaction(
[Stores.tips, Stores.tombstones],
async (tx) => {
const tipRecord = await tx.get(Stores.tips, tipId);
await ws.db
.mktx((x) => ({
tips: x.tips,
tombstones: x.tombstones,
}))
.runReadWrite(async (tx) => {
const tipRecord = await tx.tips.get(tipId);
if (tipRecord) {
await tx.delete(Stores.tips, tipId);
await tx.put(Stores.tombstones, {
await tx.tips.delete(tipId);
await tx.tombstones.put({
id: TombstoneTag.DeleteTip + ":" + tipId,
});
}
},
);
});
} else if (type === TransactionType.Deposit) {
const depositGroupId = rest[0];
await ws.db.runWithWriteTransaction(
[Stores.depositGroups, Stores.tombstones],
async (tx) => {
const tipRecord = await tx.get(Stores.depositGroups, depositGroupId);
await ws.db
.mktx((x) => ({
depositGroups: x.depositGroups,
tombstones: x.tombstones,
}))
.runReadWrite(async (tx) => {
const tipRecord = await tx.depositGroups.get(depositGroupId);
if (tipRecord) {
await tx.delete(Stores.depositGroups, depositGroupId);
await tx.put(Stores.tombstones, {
await tx.depositGroups.delete(depositGroupId);
await tx.tombstones.put({
id: TombstoneTag.DeleteDepositGroup + ":" + depositGroupId,
});
}
},
);
});
} else if (type === TransactionType.Refund) {
const proposalId = rest[0];
const executionTimeStr = rest[1];
await ws.db.runWithWriteTransaction(
[Stores.proposals, Stores.purchases, Stores.tombstones],
async (tx) => {
const purchase = await tx.get(Stores.purchases, proposalId);
await ws.db
.mktx((x) => ({
proposals: x.proposals,
purchases: x.purchases,
tombstones: x.tombstones,
}))
.runReadWrite(async (tx) => {
const purchase = await tx.purchases.get(proposalId);
if (purchase) {
// This should just influence the history view,
// but won't delete any actual refund information.
await tx.put(Stores.tombstones, {
await tx.tombstones.put({
id: makeEventId(
TombstoneTag.DeleteRefund,
proposalId,
@ -517,8 +536,7 @@ export async function deleteTransaction(
),
});
}
},
);
});
} else {
throw Error(`can't delete a '${type}' transaction`);
}

View File

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

View File

@ -33,6 +33,7 @@ import {
IDBVersionChangeEvent,
Event,
IDBCursor,
IDBKeyPath,
} from "@gnu-taler/idb-bridge";
import { Logger } from "@gnu-taler/taler-util";
@ -43,25 +44,6 @@ const logger = new Logger("query.ts");
*/
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.
*/
@ -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>;
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.
*/
@ -519,152 +266,334 @@ export function openDatabase(
});
}
export class Database<StoreMap extends AnyStoreMap> {
constructor(private db: IDBDatabase, stores: StoreMap) {}
export interface IndexDescriptor {
name: string;
keyPath: IDBKeyPath | IDBKeyPath[];
multiEntry?: boolean;
}
static deleteDatabase(idbFactory: IDBFactory, dbName: string): Promise<void> {
const req = idbFactory.deleteDatabase(dbName)
return requestToPromise(req)
}
export interface StoreDescriptor<RecordType> {
_dummy: undefined & RecordType;
name: string;
keyPath?: IDBKeyPath | IDBKeyPath[];
autoIncrement?: boolean;
}
async exportDatabase(): Promise<any> {
const db = this.db;
const dump = {
name: db.name,
stores: {} as { [s: string]: any },
version: db.version,
export interface StoreOptions {
keyPath?: IDBKeyPath | IDBKeyPath[];
autoIncrement?: boolean;
}
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) => {
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);
};
return new Promise((resolve, reject) => {
const tx = db.transaction(Array.from(db.objectStoreNames));
tx.addEventListener("complete", () => {
resolve(dump);
});
// tslint:disable-next-line:prefer-for-of
for (let i = 0; i < db.objectStoreNames.length; i++) {
const name = db.objectStoreNames[i];
const storeDump = {} as { [s: string]: any };
dump.stores[name] = storeDump;
tx.objectStore(name)
.openCursor()
.addEventListener("success", (e: Event) => {
const cursor = (e.target as any).result;
if (cursor) {
storeDump[cursor.key] = cursor.value;
cursor.continue();
}
});
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)");
}
});
}
importDatabase(dump: any): Promise<void> {
const db = this.db;
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);
}
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();
}
}
tx.addEventListener("complete", () => {
resolve();
})
.catch((e) => {
console.error("fatal: aborting transaction failed", e);
});
});
}
});
}
async get<N extends keyof StoreMap, S extends StoreMap[N]>(
store: S,
key: IDBValidKey,
): Promise<StoreContent<S> | undefined> {
const tx = this.db.transaction([store.name], "readonly");
const req = tx.objectStore(store.name).get(key);
const v = await requestToPromise(req);
await transactionToPromise(tx);
return v;
function makeReadContext(
tx: IDBTransaction,
storePick: { [n: string]: StoreWithIndexes<any, any> },
): any {
const ctx: { [s: string]: StoreReadOnlyAccessor<any, any> } = {};
for (const storeAlias in storePick) {
const indexes: { [s: string]: IndexReadOnlyAccessor<any> } = {};
const swi = storePick[storeAlias];
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);
},
};
}
return ctx;
}
async getIndexed<Ind extends Index<string, string, any, any>>(
index: Ind,
key: IDBValidKey,
): Promise<IndexRecord<Ind> | undefined> {
const tx = this.db.transaction([index.storeName], "readonly");
const req = tx.objectStore(index.storeName).index(index.indexName).get(key);
const v = await requestToPromise(req);
await transactionToPromise(tx);
return v;
}
async put<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
.objectStore(index.storeName)
.index(index.indexName)
.openCursor(query);
return new ResultStream<IndexRecord<Ind>>(req);
}
async runWithReadTransaction<
T,
N extends keyof StoreMap,
StoreTypes extends StoreMap[N]
>(
stores: StoreTypes[],
f: (t: TransactionHandle<StoreTypes>) => Promise<T>,
): Promise<T> {
return runWithTransaction<T, StoreTypes>(this.db, stores, f, "readonly");
}
async runWithWriteTransaction<
T,
N extends keyof StoreMap,
StoreTypes extends StoreMap[N]
>(
stores: StoreTypes[],
f: (t: TransactionHandle<StoreTypes>) => Promise<T>,
): Promise<T> {
return runWithTransaction<T, StoreTypes>(this.db, stores, f, "readwrite");
function makeWriteContext(
tx: IDBTransaction,
storePick: { [n: string]: StoreWithIndexes<any, any> },
): any {
const ctx: { [s: string]: StoreReadWriteAccessor<any, any> } = {};
for (const storeAlias in storePick) {
const indexes: { [s: string]: IndexReadWriteAccessor<any> } = {};
const swi = storePick[storeAlias];
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";
import {
acceptExchangeTermsOfService,
getExchangeDetails,
getExchangePaytoUri,
updateExchangeFromUrl,
} from "./operations/exchanges";
@ -111,7 +112,7 @@ import {
RefundState,
ReserveRecord,
ReserveRecordStatus,
Stores,
WalletStoresV1,
} from "./db.js";
import { NotificationType, WalletNotification } from "@gnu-taler/taler-util";
import {
@ -179,10 +180,10 @@ import { AsyncOpMemoSingle } from "./util/asyncMemo";
import { HttpRequestLibrary } from "./util/http";
import { Logger } from "@gnu-taler/taler-util";
import { AsyncCondition } from "./util/promiseUtils";
import { Database } from "./util/query";
import { Duration, durationMin } from "@gnu-taler/taler-util";
import { TimerGroup } from "./util/timer";
import { getExchangeTrust } from "./operations/currencies.js";
import { DbAccess } from "./util/query.js";
const builtinAuditors: AuditorTrustRecord[] = [
{
@ -205,12 +206,12 @@ export class Wallet {
private stopped = false;
private memoRunRetryLoop = new AsyncOpMemoSingle<void>();
get db(): Database<typeof Stores> {
get db(): DbAccess<typeof WalletStoresV1> {
return this.ws.db;
}
constructor(
db: Database<typeof Stores>,
db: DbAccess<typeof WalletStoresV1>,
http: HttpRequestLibrary,
cryptoWorkerFactory: CryptoWorkerFactory,
) {
@ -481,22 +482,21 @@ export class Wallet {
* already been applied.
*/
async fillDefaults(): Promise<void> {
await this.db.runWithWriteTransaction(
[Stores.config, Stores.auditorTrustStore],
async (tx) => {
await this.db
.mktx((x) => ({ config: x.config, auditorTrustStore: x.auditorTrust }))
.runReadWrite(async (tx) => {
let applied = false;
await tx.iter(Stores.config).forEach((x) => {
await tx.config.iter().forEach((x) => {
if (x.key == "currencyDefaultsApplied" && x.value == true) {
applied = true;
}
});
if (!applied) {
for (const c of builtinAuditors) {
await tx.put(Stores.auditorTrustStore, c);
await tx.auditorTrustStore.put(c);
}
}
},
);
});
}
/**
@ -553,10 +553,13 @@ export class Wallet {
amount,
exchange: exchangeBaseUrl,
});
const exchangePaytoUris = await this.db.runWithReadTransaction(
[Stores.exchanges, Stores.reserves],
(tx) => getFundingPaytoUris(tx, resp.reservePub),
);
const exchangePaytoUris = await this.db
.mktx((x) => ({
exchanges: x.exchanges,
exchangeDetails: x.exchangeDetails,
reserves: x.reserves,
}))
.runReadWrite((tx) => getFundingPaytoUris(tx, resp.reservePub));
return {
reservePub: resp.reservePub,
exchangePaytoUris,
@ -627,29 +630,26 @@ export class Wallet {
async refresh(oldCoinPub: string): Promise<void> {
try {
const refreshGroupId = await this.db.runWithWriteTransaction(
[Stores.refreshGroups, Stores.denominations, Stores.coins],
async (tx) => {
const refreshGroupId = await this.db
.mktx((x) => ({
refreshGroups: x.refreshGroups,
denominations: x.denominations,
coins: x.coins,
}))
.runReadWrite(async (tx) => {
return await createRefreshGroup(
this.ws,
tx,
[{ coinPub: oldCoinPub }],
RefreshReason.Manual,
);
},
);
});
await processRefreshGroup(this.ws, refreshGroupId.refreshGroupId);
} catch (e) {
this.latch.trigger();
}
}
async findExchange(
exchangeBaseUrl: string,
): Promise<ExchangeRecord | undefined> {
return await this.db.get(Stores.exchanges, exchangeBaseUrl);
}
async getPendingOperations({
onlyDue = false,
} = {}): Promise<PendingOperationsResponse> {
@ -665,87 +665,59 @@ export class Wallet {
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> {
const exchangeRecords = await this.db.iter(Stores.exchanges).toArray();
const exchanges: ExchangeListItem[] = [];
for (const r of exchangeRecords) {
const dp = r.detailsPointer;
if (!dp) {
continue;
}
const { currency, masterPublicKey } = dp;
const exchangeDetails = await this.db.get(Stores.exchangeDetails, [
r.baseUrl,
currency,
masterPublicKey,
]);
if (!exchangeDetails) {
continue;
}
exchanges.push({
exchangeBaseUrl: r.baseUrl,
currency,
paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri),
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) {
const dp = r.detailsPointer;
if (!dp) {
continue;
}
const { currency, masterPublicKey } = dp;
const exchangeDetails = await getExchangeDetails(tx, r.baseUrl);
if (!exchangeDetails) {
continue;
}
exchanges.push({
exchangeBaseUrl: r.baseUrl,
currency,
paytoUris: exchangeDetails.wireInfo.accounts.map(
(x) => x.payto_uri,
),
});
}
});
}
return { exchanges };
}
async getCurrencies(): Promise<WalletCurrencyInfo> {
const trustedAuditors = await this.db
.iter(Stores.auditorTrustStore)
.toArray();
const trustedExchanges = await this.db
.iter(Stores.exchangeTrustStore)
.toArray();
return {
trustedAuditors: trustedAuditors.map((x) => ({
currency: x.currency,
auditorBaseUrl: x.auditorBaseUrl,
auditorPub: x.auditorPub,
})),
trustedExchanges: trustedExchanges.map((x) => ({
currency: x.currency,
exchangeBaseUrl: x.exchangeBaseUrl,
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();
return await this.ws.db
.mktx((x) => ({
auditorTrust: x.auditorTrust,
exchangeTrust: x.exchangeTrust,
}))
.runReadOnly(async (tx) => {
const trustedAuditors = await tx.auditorTrust.iter().toArray();
const trustedExchanges = await tx.exchangeTrust.iter().toArray();
return {
trustedAuditors: trustedAuditors.map((x) => ({
currency: x.currency,
auditorBaseUrl: x.auditorBaseUrl,
auditorPub: x.auditorPub,
})),
trustedExchanges: trustedExchanges.map((x) => ({
currency: x.currency,
exchangeBaseUrl: x.exchangeBaseUrl,
exchangeMasterPub: x.exchangeMasterPub,
})),
};
});
}
/**
@ -772,12 +744,6 @@ export class Wallet {
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> {
try {
return acceptTip(this.ws, talerTipUri);
@ -799,7 +765,13 @@ export class Wallet {
* confirmation from the bank.).
*/
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) {
if (r.reserveStatus === ReserveRecordStatus.WAIT_CONFIRM_BANK) {
try {
@ -837,114 +809,79 @@ 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> {
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> {
return this.ws.cryptoApi.benchmark(repetitions);
}
async setCoinSuspended(coinPub: string, suspended: boolean): Promise<void> {
await this.db.runWithWriteTransaction([Stores.coins], async (tx) => {
const c = await tx.get(Stores.coins, coinPub);
if (!c) {
logger.warn(`coin ${coinPub} not found, won't suspend`);
return;
}
c.suspended = suspended;
await tx.put(Stores.coins, c);
});
await this.db
.mktx((x) => ({
coins: x.coins,
}))
.runReadWrite(async (tx) => {
const c = await tx.coins.get(coinPub);
if (!c) {
logger.warn(`coin ${coinPub} not found, won't suspend`);
return;
}
c.suspended = suspended;
await tx.coins.put(c);
});
}
/**
* Dump the public information of coins we have in an easy-to-process format.
*/
async dumpCoins(): Promise<CoinDumpJson> {
const coins = await this.db.iter(Stores.coins).toArray();
const coinsJson: CoinDumpJson = { coins: [] };
for (const c of coins) {
const denom = await this.db.get(Stores.denominations, [
c.exchangeBaseUrl,
c.denomPubHash,
]);
if (!denom) {
console.error("no denom session found for coin");
continue;
}
const cs = c.coinSource;
let refreshParentCoinPub: string | undefined;
if (cs.type == CoinSourceType.Refresh) {
refreshParentCoinPub = cs.oldCoinPub;
}
let withdrawalReservePub: string | undefined;
if (cs.type == CoinSourceType.Withdraw) {
const ws = await this.db.get(
Stores.withdrawalGroups,
cs.withdrawalGroupId,
);
if (!ws) {
console.error("no withdrawal session found for coin");
continue;
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) {
const denom = await tx.denominations.get([
c.exchangeBaseUrl,
c.denomPubHash,
]);
if (!denom) {
console.error("no denom session found for coin");
continue;
}
const cs = c.coinSource;
let refreshParentCoinPub: string | undefined;
if (cs.type == CoinSourceType.Refresh) {
refreshParentCoinPub = cs.oldCoinPub;
}
let withdrawalReservePub: string | undefined;
if (cs.type == CoinSourceType.Withdraw) {
const ws = await tx.withdrawalGroups.get(cs.withdrawalGroupId);
if (!ws) {
console.error("no withdrawal session found for coin");
continue;
}
withdrawalReservePub = ws.reservePub;
}
coinsJson.coins.push({
coin_pub: c.coinPub,
denom_pub: c.denomPub,
denom_pub_hash: c.denomPubHash,
denom_value: Amounts.stringify(denom.value),
exchange_base_url: c.exchangeBaseUrl,
refresh_parent_coin_pub: refreshParentCoinPub,
remaining_value: Amounts.stringify(c.currentAmount),
withdrawal_reserve_pub: withdrawalReservePub,
coin_suspended: c.suspended,
});
}
withdrawalReservePub = ws.reservePub;
}
coinsJson.coins.push({
coin_pub: c.coinPub,
denom_pub: c.denomPub,
denom_pub_hash: c.denomPubHash,
denom_value: Amounts.stringify(denom.value),
exchange_base_url: c.exchangeBaseUrl,
refresh_parent_coin_pub: refreshParentCoinPub,
remaining_value: Amounts.stringify(c.currentAmount),
withdrawal_reserve_pub: withdrawalReservePub,
coin_suspended: c.suspended,
});
}
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> {
return runIntegrationTest(this.ws.http, this, args);
}
@ -1144,17 +1130,20 @@ export class Wallet {
case "forceRefresh": {
const req = codecForForceRefreshRequest().decode(payload);
const coinPubs = req.coinPubList.map((x) => ({ coinPub: x }));
const refreshGroupId = await this.db.runWithWriteTransaction(
[Stores.refreshGroups, Stores.denominations, Stores.coins],
async (tx) => {
const refreshGroupId = await this.db
.mktx((x) => ({
refreshGroups: x.refreshGroups,
denominations: x.denominations,
coins: x.coins,
}))
.runReadWrite(async (tx) => {
return await createRefreshGroup(
this.ws,
tx,
coinPubs,
RefreshReason.Manual,
);
},
);
});
return {
refreshGroupId,
};

View File

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