database access refactor
This commit is contained in:
parent
68dddc848f
commit
5c26461247
@ -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));
|
||||
});
|
||||
});
|
||||
|
@ -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" }),
|
||||
{},
|
||||
),
|
||||
};
|
||||
|
@ -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;
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -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}'`);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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");
|
||||
|
||||
|
@ -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 };
|
||||
});
|
||||
}
|
||||
|
@ -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 };
|
||||
}
|
||||
|
@ -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
@ -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 = {
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
@ -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}`);
|
||||
});
|
||||
|
@ -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 [];
|
||||
|
@ -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,
|
||||
) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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`);
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user